looking for guidance regarding html reporting

This topic contains 22 replies, has 4 voices, and was last updated by  Dave Wyatt 3 years, 1 month ago.

  • Author
    Posts
  • #15988

    michael glenn
    Participant

    please excuse my lack of knowledge. im looking for some guidance. I have numerous scripts I have made that do one thing or another.. ive gotten some great knowledge re; powershell and now im looking to improve on my scripts.
    the script im working with at the moment is working well. it copies files to a list of remote computers and then installs an application via a msi installer. I do test-path commands to make sure the directory has been copied correctly, installation ran correctly by making sure the new application has a path and the executable is there, and then I remove the files from the pc after its done. the whole while im sending feedback back to my console so I know its being done. what im looking to do is make a report, perhaps venture into html reports, but I need to clean up the process of having confirmation that the install ran correctly by doing if statements with test-path commands, in effect making the script better I think. so for example the following code;
    foreach ($computer in $computers) {
    $filepresentA = invoke-command -computer $computer -scriptblock {test-path c:\install-apphere\appname.bat}
    $filepresentB = invoke-command -computer $computer -scriptblock {test-path "c:\install-appname\app.msi"}
    $installapp = {c:\install-appnamehere\app.bat}
    if (($filepresentA )-and ($filepresentB))
    {
    # some kind of output to generate report confirming install directory successfully copied on a pc by pc basis?
    "$computer contains app.bat and app.msi... starting install"
    invoke-command -computername $computer -scriptblock $installapp
    }
    else
    {
    # some kind of output to generate report confirming errors with install directory on a pc by pc basis?
    "$computer does not contain any files.... there was an error... application not installing"
    }
    }

    how would I go about adding to this to make this possible. ive thought of creating csv files with each "computername" and then titles like "installed correctly", and "cleanup successful". also the creation of custom objects, but I am not able to come up with the way to do this..much less , the proper way to go about this.. im lacking the logic, ive researched ways to possibly go about this but need direction.

  • #15991

    Don Jones
    Keymaster

    What is it exactly you're looking to make possible? Writing to a CSV file? Generating a custom object? I'm confused about what the question is, sorry.

  • #16116

    michael glenn
    Participant

    im sorry for the confusion. let me try to clarify.
    im looking to have "feedback" regarding lets say... installing an application(via msi) in a script.(keep in mind this script is running against a list of 30 computers)
    I copy some files over to a remote computer to a temporary directory, then call the bat file to get the msi install started.
    because the script has multiple "stages",
    I perform checks to make sure it (1)copies over correctly, (2)the msi started and ran successfully (leaving behind a log file of the install),(3)the newly installed program has now got a new directory structure that wasn't there before), and(4) the install directory structure has been removed.
    all this works by outputting info to the screen.. which I know is a big no-no.. but Im new at this. im getting things done..its not pretty..but more importantly. im looking to make it better, and grow my knowledge.

    I realize im way over my head in terms of what my current Powershell knowledge is now, but im looking for guidance/direction on how to go about making it better. im thinking an HTML report ,formatted nicely, would be nice.
    to run the script, then take feedback from my checks, and instead of simply writing output to the console that pc1,pc2,pc3,...etc all went through the 4 checks and a successful outcome has taken place, turn in to a nicely formatted report.
    from the reading ive been doing.. im leaning towards writing output to a hash table perhaps? I may be way off in my logic, and this may not even be possible.., or im going about it all wrong with improper logic.
    im not looking for a script.., except perhaps a possible example to help push me in the right direction.. im looking for guidance....ive no problem with reading about various things.. ive gone from no knowledge regarding powershell... to numerous scripts that install applications such as google chrome on remote computers. again. not pretty, but its gets things done. im looking to improve. again... your thoughts are very much appreciated.

    example;
    pc-name check 1 check 2 check 3 check4
    pc-1 yes yes yes yes
    pc2 yes yes no no
    pc-3 no no no no

    c1

  • #16121

    Don Jones
    Keymaster

    I don't think a hashtable would be the best approach. If your plan is to produce an HTML table, you'd want to create a custom object, and give it properties for each column you want the table to have.

  • #16122

    michael glenn
    Participant

    ok... will look in that direction. thanks very much for your input.... gonna be out on vacation for a couple weeks, so will post back then after ive messed with it a bit.

  • #16665

    michael glenn
    Participant
      
     $computers = Get-Content "C:\scripts\class-lists\Test-Lab-Computers.txt" 
     $ChromeOutcome = new-object -typename psobject
     $chromeinstallarray = @() # define the dynamic array
     foreach ($computer in $computers)
            {
               $ChromeOutcome | Add-Member -type NoteProperty -name ComputerName -Value "$computer" -force
                                        
                 if (test-path -literalpath \\$computer\C$\install-chrome) {
                             $ChromeOutcome | Add-Member -type NoteProperty -name InstallDir -Value "yes " -force
                             } else {
                             $ChromeOutcome | Add-Member -type NoteProperty -name InstallDir -Value "no" -force
                               }
    
                 if (invoke-command -erroraction SilentlyContinue -computername $computer -scriptblock {test-path -path "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"}) {
                            $ChromeOutcome | Add-Member -type NoteProperty -name ChromeDirPresent -Value  "yes" -force
                            } else {
                            $ChromeOutcome | Add-Member -type NoteProperty -name ChromeDirPresent -Value  "no" -force
                            }
                     }
    
                            $test_file = (get-item -ErrorAction SilentlyContinue "\\$computer\C$\program files (x86)\google\chrome\Application\master_preferences").LastWriteTime
                            $CorrectDate = "friday, april 26, 2013 10:39:56 am"
                            $File_Present =   test-path -ErrorAction SilentlyContinue  "\\$computer\C$\program files (x86)\google\chrome\Application\master_preferences" 
                                if (( $File_Present ) -and ($test_File = $CorrectDate )) {
                                    $ChromeOutcome | Add-Member -type NoteProperty -name PreferenceFilePresent -Value "yes" -force
                                    } else {
                                    $ChromeOutcome | Add-Member -type NoteProperty -name PreferenceFilePresent -Value "no" -force       
                                    }
             

    ok.. ive got this working where as the first computer called from the get-content command ( of which there are 2 computers in the list at the moment) works correctly, but it doesn't seem to loop to the next computer. what am I missing? what am I not understanding? I thought I had to make this an array. is my thinking wrong?

  • #16666

    Rob Simmers
    Participant

    Get-Content will return an array. If you properly indent your code, your for loop does not contain half of the code you are intending to run:

    $computers = Get-Content "C:\scripts\class-lists\Test-Lab-Computers.txt" 
    $ChromeOutcome = new-object -typename psobject
    $chromeinstallarray = @() # define the dynamic array
    foreach ($computer in $computers) {
        $ChromeOutcome | Add-Member -type NoteProperty -name ComputerName -Value "$computer" -force
     
        if (test-path -literalpath \\$computer\C$\install-chrome) {
            $ChromeOutcome | Add-Member -type NoteProperty -name InstallDir -Value "yes " -force
        }
        else {
            $ChromeOutcome | Add-Member -type NoteProperty -name InstallDir -Value "no" -force
        }
     
        if (invoke-command -erroraction SilentlyContinue -computername $computer -scriptblock {test-path -path "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"}) {
            $ChromeOutcome | Add-Member -type NoteProperty -name ChromeDirPresent -Value  "yes" -force
        } 
        else {
            $ChromeOutcome | Add-Member -type NoteProperty -name ChromeDirPresent -Value  "no" -force
        }
    } #Loop Ends HERE
     
    $test_file = (get-item -ErrorAction SilentlyContinue "\\$computer\C$\program files (x86)\google\chrome\Application\master_preferences").LastWriteTime
    $CorrectDate = "friday, april 26, 2013 10:39:56 am"
    $File_Present =   test-path -ErrorAction SilentlyContinue  "\\$computer\C$\program files (x86)\google\chrome\Application\master_preferences" 
    
    if (( $File_Present ) -and ($test_File = $CorrectDate )) {
        $ChromeOutcome | Add-Member -type NoteProperty -name PreferenceFilePresent -Value "yes" -force
    }
    else {
        $ChromeOutcome | Add-Member -type NoteProperty -name PreferenceFilePresent -Value "no" -force       
    }

    If you are just connecting via UNC, you might want to consider adding a Test-Connection to validate the computer is reachable before you attempt to connect via UNC path. Your "No" is supposed to indicate a path doesn't exist, not a computer that is offline.

  • #16669

    michael glenn
    Participant

    ok...
    I will add the test-connection, thanks for the suggestion. i believe I actually have that in the beginning stage of this script where I actually install chrome... I left that portion out thinking it wasn't part of the cause of the issue im having seeing that its working correctly.
    I fixed the loop ending prematurely. (where you marked #loop ends here) by removing "}" and putting at the end, but the script was still running that for me correctly by showing me a yes or a no in that field.
    I am still having the issue where the 2nd of 2 computers in the list is showing up. the first computer in my get-content call doesn't seem to register.

  • #16679

    Rob Simmers
    Participant

    Each time you loop, you need to add a object. You are basically overwriting the properties each time versus adding an additional object. Note the += :

    $computers = @("Computer1", "Computer2", "Computer3")
    $ChromeOutcome = @()
    foreach ($computer in $computers) {
        $ChromeOutcome += new-object -typename psobject
        $ChromeOutcome | Add-Member -type NoteProperty -name ComputerName -Value $computer -Force
        $ChromeOutcome | Add-Member -type NoteProperty -name Blah -Value $true -Force
        $ChromeOutcome | Add-Member -type NoteProperty -name AnotherBlah -Value $false -Force
    }

    There are a lot of ways to do things, but I think if you review the communities custom objects you'll see they use a logic\syntax like this:

    $computers = @("Computer1", "Computer2", "Computer3")
    $ChromeOutcome = @()
    foreach ($computer in $computers) {
        
        if ($something -eq $true) {$variable1 = $true}else{$variable1 = $false}
        if ($somethingelse -eq $false) {$variable2 = $false}else{$variable2 = $true}
    
    
       $props = @{
            ComputerName=$computer
            Variable1=$variable1
            Variable2=$variable2
        }
    
        $ChromeOutcome += New-Object -TypeName PSObject -Property $props 
    }
    
    $ChromeOutcome

    It's personal preference, but I don't really use Add-Member unless I'm adding a column or manipulating object properties that aren't available with this method (e.g. Alias, PSStandardMembers, etc.). Since the Add-Member requires a -Force parameter or you'll get errors that the property exists, it feels...well forced and klunky...IMHO

  • #16680

    michael glenn
    Participant

    thanks for the assistance.... will look into this.. I noticed the "+=" combo during my research into how to do this, but never tried it in this version of the script script. this was the closest I got to success in all my experiments. will also look into your other suggestions. im always open to others opinions as I do not know everything and im sure there are better ways of doing what im trying to accomplish.
    thanks so much for the assistance.

  • #16861

    michael glenn
    Participant
    $computers = @(Get-Content "C:\scripts\class-lists\test-lab-computers.txt")
    $ChromeOutcome = @()
    foreach ($computer in $computers) {
        $test_file = (get-item -ErrorAction SilentlyContinue "\\$computer\C$\program files\google\chrome\Application\master_preferences").LastWriteTime
        $CorrectDate = "friday, april 26, 2013 10:39:56 am"
        $File_Present =   test-path -ErrorAction SilentlyContinue  "\\$computer\C$\program files\google\chrome\Application\master_preferences" 
          
        $InstallDir = test-path -literalpath \\$computer\C$\install-chrome
        $AppDir = invoke-command -computername $computer -scriptblock {test-path -path "C:\Program Files\Google\Chrome\Application\chrome.exe" }  
        $MasterPref =  (( $File_Present ) -and ($test_File = $CorrectDate ))
        
        if ($InstallDir -eq $true) {$variable1 = $false}else{$variable1 = $true}
        if ($AppDir -eq $false) {$variable2 = $false}else{$variable2 = $true}
        if ($MasterPref -eq $false) {$variable3 = $false}else{$variable3 = $true}
     
       $props = @{
            "Computer Name"=$computer
            "Install Directory Deleted?"=$InstallDir
            "Application Directory Present?"=$AppDir
            "Correct Master_Preference File is Present?"=$MasterPref
        }
     
        $ChromeOutcome += New-Object -TypeName PSObject -Property $props 
    }
     
    $ChromeOutcome 
    
    
    
    
    

    ok... I got this to work... but ive noticed sometimes the (properties) columns move position. how can I define "computer name" takes the first position, and "Install Directory Deleted?" takes the second position, and so on?

  • #16877

    Rob Simmers
    Participant

    Use Select-Object:

     $ChromeOutcome | Select "Computer Name", "Install Directory Deleted",  "Application Directory Present?","Correct Master_Preference File is Present?"
    

    You might want to consider properties that are not sentences for simplicity:

    $ChromeOutcome | Select ComputerName, InstallDirDel,  AppDirPresent, Master_Preference
  • #16878

    Dave Wyatt
    Moderator

    If you need your script to support PowerShell 2.0, then you can use the Select-Object cmdlet to reorder the properties, as Rob pointed out. If you don't need 2.0 compatibility, I prefer to use either the [pscustomobject]@{} or [ordered]@{} syntax:

       $ChromeOutcome += [pscustomobject] @{
            "Computer Name"=$computer
            "Install Directory Deleted?"=$InstallDir
            "Application Directory Present?"=$AppDir
            "Correct Master_Preference File is Present?"=$MasterPref
        }
    

    (I also prefer to avoid using the += operator on arrays like this, but since that's not relevant to the question, I left it as-is.)

  • #16882

    Rob Simmers
    Participant

    I'll bite. How do you avoid using the += operator to append to an object? I've done this before:

    $os = gwmi Win32_OperatingSystem | Select Caption
    $bios = gwmi Win32_BIOS | Select SerialNumber, @{Label="OSCaption";Expression={$os.Caption}}
    $bios

    However, don't see that as relevant in this case, so please share your method of building an object. Thanks.

  • #16883

    Dave Wyatt
    Moderator

    There are a couple of options. Typically I just assign the results of a loop or function to a variable, and if the loop / function happens to produce more than one object, PowerShell automatically converts it to an array. (If you want it to be an array with zero or one elements, you can wrap the loop / function call in @() ). Example modification of the OP's code:

    $computers = @(Get-Content "C:\scripts\class-lists\test-lab-computers.txt")
    $ChromeOutcome = @(
        foreach ($computer in $computers) {
            $test_file = (get-item -ErrorAction SilentlyContinue "\\$computer\C$\program files\google\chrome\Application\master_preferences").LastWriteTime
            $CorrectDate = "friday, april 26, 2013 10:39:56 am"
            $File_Present =   test-path -ErrorAction SilentlyContinue  "\\$computer\C$\program files\google\chrome\Application\master_preferences" 
     
            $InstallDir = test-path -literalpath \\$computer\C$\install-chrome
            $AppDir = invoke-command -computername $computer -scriptblock {test-path -path "C:\Program Files\Google\Chrome\Application\chrome.exe" }  
            $MasterPref =  (( $File_Present ) -and ($test_File = $CorrectDate ))
     
            if ($InstallDir -eq $true) {$variable1 = $false}else{$variable1 = $true}
            if ($AppDir -eq $false) {$variable2 = $false}else{$variable2 = $true}
            if ($MasterPref -eq $false) {$variable3 = $false}else{$variable3 = $true}
     
            [pscustomobject] @{
                "Computer Name"=$computer
                "Install Directory Deleted?"=$InstallDir
                "Application Directory Present?"=$AppDir
                "Correct Master_Preference File is Present?"=$MasterPref
            }
        }
    ) 
    
    $ChromeOutcome
    

    Sometimes it's more flexible to have finer control of the array that you're building. In those cases, I like to use List or ArrayList; they perform much better at scale by keeping the number of array copies to a minimum.

    $computers = @(Get-Content "C:\scripts\class-lists\test-lab-computers.txt")
    $ChromeOutcome = New-Object System.Collections.ArrayList
    
    foreach ($computer in $computers) {
        $test_file = (get-item -ErrorAction SilentlyContinue "\\$computer\C$\program files\google\chrome\Application\master_preferences").LastWriteTime
        $CorrectDate = "friday, april 26, 2013 10:39:56 am"
        $File_Present =   test-path -ErrorAction SilentlyContinue  "\\$computer\C$\program files\google\chrome\Application\master_preferences" 
     
        $InstallDir = test-path -literalpath \\$computer\C$\install-chrome
        $AppDir = invoke-command -computername $computer -scriptblock {test-path -path "C:\Program Files\Google\Chrome\Application\chrome.exe" }  
        $MasterPref =  (( $File_Present ) -and ($test_File = $CorrectDate ))
     
        if ($InstallDir -eq $true) {$variable1 = $false}else{$variable1 = $true}
        if ($AppDir -eq $false) {$variable2 = $false}else{$variable2 = $true}
        if ($MasterPref -eq $false) {$variable3 = $false}else{$variable3 = $true}
     
        $object = [pscustomobject] @{
            "Computer Name"=$computer
            "Install Directory Deleted?"=$InstallDir
            "Application Directory Present?"=$AppDir
            "Correct Master_Preference File is Present?"=$MasterPref
        }
     
        $null = $ChromeOutcome.Add($object)
    }
    
    # In most cases you can just output $ChromeOutcome here; PowerShell will enumerate its elements just like an array.  If you really
    # need to convert it back to an Object[] type, use $ChromeOutcome.ToArray()
    $ChromeOutcome
    

    Edit: On a side note, I made a blog post on this topic a while back: https://powershell.org/2013/09/16/powershell-performance-the-operator-and-when-to-avoid-it/

  • #16892

    michael glenn
    Participant

    thank you gentlemen for the comments. im trying to learn as much as possible while creating and using scripts that make my life easier. but also.. keep in mind.. my end plan is to turn this into an HTML report. so being easy to read is important so the column headers were made to look like " Computer Name" as opposed to "ComputerName" and so forth. Version 2 compatibility is a good thing due to most of the PC's this may run against may still be version 2. we are reimaging computers to bring them up to ver 4.
    so with HTML reporting in mind, are any of your suggestions geared better towards that? and also. considering this may run against 30 computers at a time, are there any performance benefits one way or the other?
    Once again, thank you for the suggestions. it gives me multiple directions to research.

  • #16953

    Rob Simmers
    Participant

    You can change the column names at any point before you "present" the data. For instance:

    #Use a calculated expression
    $processes = Get-Process | Select ProcessName, @{Label="The name of the process that is running!";Expression={$_.ProcessName}}
    #Add a alias to the property
    $processes | Add-Member -MemberType AliasProperty -Name "Another Really Long Property" -Value ProcessName
    #Say you want to generate a report and are going to filter the results to 
    #only show computers that only have WINWORD running (or Install Dir deleted)
    #I'm just saying (personally), I rather NOT do ...
    $processes | Where{$_."Another Really Long Property" -eq "WINWORD"}
    #versus this:
    $processes | Where{$_.ProcessName -eq "WINWORD"}
    
    #For instance, if I wanted a HTML fragement:
    $frag = $processes |Where{$_.ProcessName -eq "WINWORD"} | Select @{Label="The name of the process that is running!";Expression={$_.ProcessName}} | ConvertTo-HTML -Fragment
    
    

    HTML reporting is fine, but Powershell is going to gather all of the data. You need to ensure you have all the data you need from the computers first before reporting it with HTML. If you are going to be running anything against multiple computers, you should leverage Powershell Jobs. Although 30 computers is not a lot, you might see some performance gain running it as a job.

    BTW, reimaging a computer just to put Powershell V4 on computers...you guys should look at hiring a application packager. 😉

    Last notes. This does nothing:

        if ($InstallDir -eq $true) {$variable1 = $false}else{$variable1 = $true}
        if ($AppDir -eq $false) {$variable2 = $false}else{$variable2 = $true}
        if ($MasterPref -eq $false) {$variable3 = $false}else{$variable3 = $true}

    In the example I provided, the variable (e.g. $variable1) was referenced when building the object which you are not doing, so it can be removed. Second, why are you testing a path with UNC on the first line but using Invoke-Command to test another path? You should consider updating the second check to UNC or making the paths local ( "C:\Program Files\Google\Chrome\Application\chrome.exe" ) and then running your script with: Invoke-Command -ComputerName $computer -File "C:\Script\Get-ChromeStats.PS1"

        $InstallDir = test-path -literalpath \\$computer\C$\install-chrome
        $AppDir = invoke-command -computername $computer -scriptblock {test-path -path "C:\Program Files\Google\Chrome\Application\chrome.exe" }  
     
  • #17018

    michael glenn
    Participant

    "BTW, reimaging a computer just to put Powershell V4 on computers…you guys should look at hiring a application packager. 😉 "

    we are a school district.... reimaging is almost a yearly occurrence due to how messed up they get over the course of the year. so its a chance to make updates, tune them up, etc.

  • #17019

    Don Jones
    Keymaster

    DSC will eventually make you very happy ;).

  • #17020

    michael glenn
    Participant

    Second, why are you testing a path with UNC on the first line but using Invoke-Command to test another path?

    $InstallDir = test-path -literalpath \\$computer\C$\install-chrome
        $AppDir = invoke-command -computername $computer -scriptblock {test-path -path "C:\Program Files\Google\Chrome\Application\chrome.exe" }
    
    

    the "Install-chrome" directory structure is a folder I copy over to each pc and run the install local from there. once the install is complete.. I want to clean up the directory and not leave it there... so the check is to confirm I have indeed successfully cleaned up the directory after the install..i have since put in firthe checks to make sure it isn't being deleted until the install has proven successful.

  • #17021

    Rob Simmers
    Participant

    I assume you say "eventually" because DSC's primary focus is server at this point? For something like school systems, implementing a whitelisting product so that only apps an administrator dictates actually execute on the systems prevents re-imaging every year. That would be a ridiculous MOF file to prevent World Of WarCraft from being installed and all the other junk students try. Always trying to prove they can "hack" and blue screening systems. I worked for school systems before and it was a nightmare. I would recommend some products, but don't want to be pitching software sales in a Powershell forum and forum doesn't have private messaging. 🙂

    What I'm saying regarding the Invoke-Command line is your are doing the same thing two ways. Personally, i would just use a UNC check for both items versus doing a remote command for one and a UNC check for the other. Both commands are just returning a boolean True or False. IMHO, doing a Invoke-Command against a remote computer is a bit overkill and I'd just do the Test-Path -Path \\$computer\c$\....

  • #17026

    michael glenn
    Participant

    "DSC will eventually make you very happy ;)."

    sorry.. not familiar with this. DSC stands for?

    and I will look further into making the script better.... for now.. summer time is my crunch time and these scripts are making my life so easy... but of course there is always room for improvement.

  • #17027

    Dave Wyatt
    Moderator

    DSC is Desired State Configuration, a new feature in Windows Management Framework 4.0.

You must be logged in to reply to this topic.