ConvertTo-HTML Challenge

This topic contains 6 replies, has 3 voices, and was last updated by  Daniel Olson 3 weeks, 5 days ago.

  • Author
    Posts
  • #102961

    Daniel Olson
    Participant

    Thank you for taking the time to read through this post and help me out. I have been reading Learn PowerShell 3 In a Month of Lunches (3 times) and finishing the last couple of chapters of Learn PowerShell scripting in a month of Lunches. I'm looking forward to Learn PowerShell Toolmaking in a Month of Lunches. These books have really helped me get a foundation in PowerShell and what I can do with that, including how I think about using PowerShell.

    With that said, a recent issue at work has given me an opportunity to built a set of tools and a controller script to monitor some backup files. As it is only my second set of tools I have developed, I admittedly am still refining some rough edges, but I have it working pretty smooth now except for one function, and that is the conversion of the object to HTML and then sending it to the last tool for email delivery.

    This is the controller script:

    $Backup = Get-CheckBackup -Path "\\Server\d$\Folder\"
    
    $day = Get-Date
    
    $Failed = $Backup | Where-Object {$_.Status -eq "Failed"}
    
    $Successful = $Backup | Where-Object {$_.Status -eq "Successful"}
    
    If ($Failed -ne $Null) {
        $Failed | ConvertTo-Html | Out-File -FilePath "\\Server\D$\Folder\Logs\FailedBackup.txt"
        $FailedBack = Get-Content -Path "\\Server\D$\Folder\Logs\FailedBackup.txt"
        Send-BackupFileFail -To "NetworkMonitoring@Company.org" -Files $FailedBack
    } #If
    
    IF ($Successful -ne $Null) {
        $day | Select-Object -Property Date | ConvertTo-Html | Out-File -FilePath "\\Server\D$\Folder\Logs\SuccessfulBackup.txt" -Append
    
        $Successful | ConvertTo-HTML | Out-File -FilePath "\\Server\D$\Folder\Logs\SuccessfulBackup.txt" -Append
    
        If ($day.DayOfWeek -eq "Sunday") {
            $success = Get-Content -Path "\\Server\d$\Folder\logs\SuccessfulBackup.txt"
    
            Send-IHCRCBackupFileSuccess -To "NetworkMonitoring@Company.org" -Files $success
    
            Remove-Item -Path "\\Server\d$\Folder\logs\SuccessfulBackup.txt"
        } #If
    } #IF 

    The script starts by calling my first tool, which collects the files in the patch, checks their LastWriteDate to a date specified in the function call, in this case, the default of 0 or today, and determines if the backup was successful or failed, and then creates and object which is then placed in an array. This is all saved into a variable in the controller script.

    Get-CheckBackup function

    Function Get-CheckBackup {
         Get-CheckBackup
    
        Status     FullName                    LastWriteTIme
        ------     --------                    -------------
        Failed     C:\Demo\HelloWorld.ps1      2/12/2018 2:30:38 PM
        Successful C:\Demo\NewFile.txt         6/21/2018 10:05:28 AM
        Failed     C:\Demo\TestFailedFile1.txt 6/20/2018 3:50:36 PM
    
        In this example, the function is run using all the defaults, dasy of 0, current path and no recurse.  Files with a lastWriteTime that does not match today's date have a status of "Failed."
    
        .EXAMPLE
    
        PS C:\> Get-CheckBackup -days 0 -Paths "c:\Demo\NewFile.txt"
    
        Status     FullName            LastWriteTIme
        ------     --------            -------------
        Successful C:\Demo\NewFile.txt 6/21/2018 10:05:28 AM
    
        In this example, the days were set to zero, and a path to a specific file is set.  The function returns the status of the file based on the LastWriteTime
        
        .EXAMPLE
    
        PS C:\> Get-CheckBackup -days 1 -Paths "c:\Demo" -Recurse
    
        Status     FullName                                         LastWriteTIme
        ------     --------                                         -------------
        Failed     C:\Demo\HelloWorld.ps1                           2/12/2018 2:30:38 PM
        Successful C:\Demo\NewFile.txt                              6/21/2018 10:05:28 AM
        Successful C:\Demo\TestFailedFile1.txt                      6/20/2018 3:50:36 PM
        Failed     C:\Demo\Demo Plus Folder\Eventlogapplication.htm 3/9/2018 11:40:57 AM
        Successful C:\Demo\Demo Plus Folder\New File Also.txt       6/21/2018 10:06:10 AM
    
        In this example, the days to check against was 1, and a path of "C:\Demo" was set.  This displays all the files in the current and all sub folders, and sets a status of "successful" on files that have a lastwritetime of 1 day or less.
    
        .EXAMPLE
    
        PS C:\> $Days = 2
        PS C:\> $Backups = "C:\test","C:\Demo"
        PS C:\> Get-CheckBackup -Days $Days -Paths $Backups -Recurse
    
        Status     FullName                                         LastWriteTIme
        ------     --------                                         -------------
        Successful C:\test\Demo Test File.txt                       6/21/2018 11:08:12 AM
        Failed     C:\test\New Hire Procedure.docx                  6/6/2017 1:01:15 PM
        Failed     C:\test\Termination Procedure.docx               6/7/2017 3:44:52 PM
        Failed     C:\test\Workstation Retire Procedure.docx        5/24/2017 9:59:57 AM
        Failed     C:\test\PowerShellTest\30days.csv                4/24/2018 4:20:36 PM
        Failed     C:\test\PowerShellTest\Get-FolderSize.ps1        11/8/2017 11:09:46 AM
        Successful C:\test\PowerShellTest\I Updated Today.txt       6/21/2018 11:08:28 AM
        Failed     C:\test\PowerShellTest\Install-WMF5.1.ps1        3/22/2017 1:44:48 PM
        Failed     C:\Demo\HelloWorld.ps1                           2/12/2018 2:30:38 PM
        Successful C:\Demo\NewFile.txt                              6/21/2018 10:05:28 AM
        Successful C:\Demo\TestFailedFile1.txt                      6/20/2018 3:50:36 PM
        Failed     C:\Demo\Demo Plus Folder\Eventlogapplication.htm 3/9/2018 11:40:57 AM
        Successful C:\Demo\Demo Plus Folder\New File Also.txt       6/21/2018 10:06:10 AM
    
        In this example, The days and the path is set by the variables $Days and $Backups.  This lists all the files in the folders and sub folders, marking fiels with a lastWritetime of 2 days or less as "successful," and sets the remaining files status as failed.
        #>
        [CmdletBinding(SupportsShouldProcess)]
    
        param (
    
            [String] $Days = 0,
    
            [parameter(ValueFromPipeline=$true,
                        ValueFromPipelineByPropertyName=$true)]
            [String[]] $Paths = (Get-Location),
    
            [switch] $recurse
        )
    
        BEGIN {
            Write-Verbose "Initializing some variables that are used through function"
            $day = (Get-Date).AddDays(-$days).ToString('MM/dd/yyyy')
            $Log = @()
        } #Begin
    
        PROCESS {
            ForEach ($path in $paths) {
                Write-Verbose "Collecting the files to verify the backup status"
                If ($recurse -eq $True) {
                    $Files = Get-ChildItem -Path $path -File -Recurse
                } #If
                else {
                    $files = Get-ChildItem -Path $path -File
                } #Else
    
                ForEach ($File in $Files) {
                    Wite-Verbose "Collecting the files that failed their backup"
                    $succeed = $file.LastWriteTime -lt $day
                    Write-Verbose "Collecting properties about the failed backup files"
                    If ($succeed -eq $true) {
                        $props = @{'FullName'=$File.FullName
                                   'LastWriteTIme'=$file.LastWriteTime
                                   'Status'='Failed'}
                        $obj = New-Object -TypeName PSObject -Property $props
                    } #If
                    Write-Verbose "Collecting files which had a successful backup"
                    elseif ($succeed -eq $false) {
                        Write-Verbose "Collecting properties about the successful backup files"
                        $props = @{'FullName'=$File.FullName
                                   'LastWriteTIme'=$file.LastWriteTime
                                   'Status'='Successful'}
                        $obj = New-Object -TypeName PSObject -Property $props
                    } #Elseif 
                    Write-Verbose "Adding each record to an array"
                    $log += $obj
                } #ForEach Comapre
            } #ForEach Get Files
        } #Process
    
        END {
            Write-Verbose "Dispalying the results of data collection"
            Write-Output $log
        } #End
    } #Function 

    The controller script then separates the objects into the variable $Failed for files that did not backup up properly, and $Successful for successful backups. This is where I run into the problem. If I pipe the variable to ConvertTo-HTML | Send-Email tool I built, or even just try and pipe the object, it works, but it creates a separate email for each and every object, even blank lines.

    The only solution I have found is to convert the objects in the variable using ConvertTo-HTML then pipe it to OutFile and save the file. After that is done, I have to use Get-Content to populate another variable, and I can then use that Variable to populate the backup file path in the email body to show what file/s either missed their backup or were successful.

    Send-BackupFileFail function

     Function Send-BackupFileFail {
    
    
    [CmdletBinding(SupportsShouldProcess)]
    
    param (
    
        [parameter(Mandatory=$true, 
                   HelpMessage = "Enter the email address or addresses to send message to",
                   ValueFromPipelineByPropertyName=$True)]
        [validatenotnullorempty()]
        [Alias('Send')]
        [String[]] $To,
    
        [parameter(Mandatory=$True,
                   ValueFromPipeline=$True,
                   ValueFromPipelineByPropertyName=$True)]
        [validatenotnullorempty()]
        [String[]] $Files
    
    )
    
    BEGIN {}
    
    
    PROCESS {
        Write-Verbose "Sending the email notfication with backup files that failed"
        Send-MailMessage -To $To `
            -Subject "Failed Bakup Files - $(Get-Date -DisplayHint Date)" `
            -SmtpServer "Email.Company.org" `
            -from "Monitoring@Company.org" `
            -BodyAsHtml `
            -Body "The following files have not completed a successful backukp since the corresponding date and time.
              
            $Files
            
            Perform any necessary troubleshooting to bring the backups up to date.
            
            The Company IT Team
            Company
            Ext:  1234
            Email:  ALL_IT_STAFF@Company.org
            www.Company.org"
        } #Process
    } #Function 

    Send-BackupFileSuccess function

     Function Send-BackupFileSuccess {
    
    
    [CmdletBinding(SupportsShouldProcess)]
    
    param (
    
        [parameter(Mandatory=$true, 
                   HelpMessage = "Enter the email address or addresses to send message to",
                   ValueFromPipelineByPropertyName=$True)]
        [validatenotnullorempty()]
        [Alias('Send')]
        [String[]] $To,
    
        [parameter(Mandatory=$True,
                   ValueFromPipeline=$True,
                   ValueFromPipelineByPropertyName=$True)]
        [validatenotnullorempty()]
        [String[]] $Files
    
    )
    
    BEGIN {}
    
    
    PROCESS {
        Write-Verbose "Sending the email notification of successful backup files"
        Send-MailMessage -To $To `
            -Subject "Successful Backup Files Report" `
            -SmtpServer "email.company.org" `
            -from "Monitoring@company.org" `
            -BodyAsHtml `
            -Body "The following files have Successfully completed the backukp process for the the dates and times listed.
              
            $Files
            
            The Company IT Team
            Companyr
            Ext:  1234
            Email:  ALL_IT_STAFF@Company.org
            www.Company.org"
        } #Process
    } #Function 

    Is there something a missed? Is there a better way to insert these objects into the email body?

    Thank you in advance for taking the time to look over the code and helping me to refine this process.

    Daniel Olson

  • #103027

    Don Jones
    Keymaster

    The Toolmaking book is no longer published; "Scripting" replaced it. See donjones.com/powershell for the book sequence these days.

    When you're piping objects to a function, the function only gets one item at a time. What you'd normally do with the receiving function in your case is to use a BEGIN block to set up some variables to accumulate the eventual email, build the email in the PROCESS block, and send it in the END block. PROCESS is what gets one object at a time in whatever variable is accepting the pipeline input.

    E.g,

    BEGIN { $body = "" }
    PROCESS { $body += "whatever" }
    END { # send it }
    

    Your first function is, I think, incorrectly accumulating output in an array rather than outputting each one to the pipeline as it goes. See https://devops-collective-inc.gitbooks.io/the-big-book-of-powershell-gotchas/content/manuscript/accumulating-output-in-a-function.html. Unless you need to accumulate a batch of stuff and DO SOMETHING WITH IT before outputting, the pattern is to output-as-you-go and let the pipeline parallelize itself.

    • #103028

      Daniel Olson
      Participant

      Don,

      Thank for your valuable time and input. That information will help me a great deal. I kind of jumped in the "deep end" and each time I refine and improve the script, I learn a little more.

      A quick side note, thank you and Jeff Hicks for publishing your Month of Lunches books, and the videos you have published. They have been a wonderful teaching tool and have given me the confidence to learn PowerShell.

  • #103030

    Don Jones
    Keymaster

    Oh, you're very welcome – thank you for buying them ;). Jeff's got a great one – https://leanpub.com/psprimer – you might want to check out. Kind of a self-test way to figure out "what do I learn next?"

    • #103042

      Daniel Olson
      Participant

      Don –

      Thank you for suggesting The PowerShell Practice Primer by Jeff Hicks. That book is something that will help me tremendously. I also noticed The PowerShell Scripting and Toolmaking Book at the bottom. I look forward to getting both of those books in the very near future.

  • #103037

    Rob Simmers
    Participant

    Check out some updates to your function to make you aware of some other ways to do things:

    Function Get-CheckBackup {
        [CmdletBinding(SupportsShouldProcess=$true)]
        param (
            [String] $Days = 0,
            [parameter(ValueFromPipeline=$true,
                        ValueFromPipelineByPropertyName=$true)]
            [String[]] $Path = (Get-Location), #Use singular, try to use other cmdlets like Get-ChildItem as reference
            [switch] $Recurse
        )
    
        BEGIN {
            Write-Verbose "Initializing some variables that are used through function"
            $day = (Get-Date).AddDays(-$days).ToString('MM/dd/yyyy') #not being used
        } #Begin
    
        PROCESS {
            $results = ForEach ($p in $path) { #Collect the results 
                Write-Verbose "Collecting the files to verify the backup status"
                #check out about_Splatting
                $fileParams = @{
                    Path = $p
                    File = $true
                }
                #splatting allows you to dyamically build params. They are stored in hash table
                If ($recurse -eq $True) {$fileParams.Add("Recurse", $true)}
                
                $files = Get-ChildItem @fileParams
    
                ForEach ($File in $Files) {
     
                    If ($file.LastWriteTime -lt $day) { #This is going to return True\False
                        Write-Verbose "Collecting properties about the failed backup files"                  
                        New-Object -TypeName PSObject -Property @{
                            'FullName'     =$File.FullName
                            'LastWriteTIme'=$file.LastWriteTime
                            'Status'       ='Failed'
                        }
                    } #If
                    else {
                        Write-Verbose "Collecting properties about the successful backup files"
                        New-Object -TypeName PSObject -Property @{
                            'FullName'     =$File.FullName
                            'LastWriteTIme'=$file.LastWriteTime
                            'Status'       ='Successful'
                        }
                    } #Elseif 
                } #ForEach Comapre
            } #ForEach Get Files
        } #Process
    
        END {
            Write-Verbose "Displaying the results of data collection"
            $results
        } #End
    } #Function 
    

    The best way I've found to create an object from loops is to set a variable ($results = foreach...) and avoid += to do append operations. Anything that is outputted is automatically appended to $results. Splatting allows you build params dynamically and I think keeps the params looking neat in your code.

    • #103043

      Daniel Olson
      Participant

      Rob Simmers –

      Thank you for your input. It is very helpful and educational. Your suggestion on the Path variable name was very helpful. When I was writing it, I was trying to figure out how to make it singular on the parameter, yet have the ForEach read (Single in multiple) and just couldn't figure out the best way to do it to make sense. I will look into splatting in more detail. I just started to play with them last week when I read about them in Powershell Scripting in a month of lunches, and this will help me get a good grasp on them.

You must be logged in to reply to this topic.