How Do I Consolidate Console Output in PowerShell and Send as HTML Formatted Email

Good-day folks,

I am attempting to adapt this script (originally authored by Sitaram Pamarthi), for the primary purpose of automating a mundane administrative task of managing local admin passwords on the Windows systems I have under my control. So far the script is working - in that I am able to change the password and get the email output (albeit in text format) - but it's not quite there yet.

This is an excerpt from my script:

Complete script is available here for full context.

$Computers = Get-Content -Path $InputFile

#Now let's loop through each computer and change the specified account password.
ForEach ($Computer in $Computers) {

    $Computer   =   $Computer.ToUpper()
    $IsOnline   =   "OFFLINE"
    $Status     =   "SUCCESS"

    Write-Verbose "Working on $Computer"
    If((Test-Connection -ComputerName $Computer -count 1 -ErrorAction 0)) {
        $IsOnline = "ONLINE"
        Write-Verbose "`t$Computer is Online"
    } Else { Write-Verbose "`t$Computer is OFFLINE" }

    Try {
        $Account = [ADSI]("WinNT://$Computer/$TargetAccount,user")
        $Account.psbase.invoke("setpassword",$Pwd1_Text)
        Write-Verbose "`tPassword Change completed successfully"
    }
    Catch {
        $Status = "FAILED"
        Write-Verbose "`tFailed to Change the password for $TargetAccount on $Computer. Error: $_"
        $StatsError = "$_"
    }

    $Obj = New-Object -TypeName PSObject -Property @{
        Date = "$(Get-Date)"
        ComputerName = $Computer
        IsOnline = $IsOnline
        PasswordChangeStatus = $Status
        DetailedStatus = $StatsError
    }

    $Obj | Format-Table ComputerName, Date, IsOnline, PasswordChangeStatus, DetailedStatus -AutoSize
    $Obj | Select-Object "ComputerName", "Date", "IsOnline", "PasswordChangeStatus", "DetailedStatus" | Export-Csv -Append -Path ".\output.csv" -NoTypeInformation

    If($Status -eq "FAILED" -or $IsOnline -eq "OFFLINE") {
        $Stream.Writeline("$Computer `t $IsOnline `t $Status")
    }
}

#Finally, let's close the Stream Writer, write the stream to the text file
#and notify the user of the location of the FailedComputers log file
$Stream.Close()
Write-Host "`n`nFailed computers list is saved to $FailedComputers"
$ScriptEndDate = $(Get-Date)

#Send an email log of what was done
$MsgBody = @"

Dear IT Admins,

Please be advised that the local admin account password for [ $TargetAccount ] has been changed
successfully on the following hosts:

Action Initiated by:  `t$([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)
Script Started:  `t$ScriptStartDate
Script Ended:  `t$ScriptEndDate

"@
Send-MailMessage -Body $MsgBody -Verbose

# Finally, let's clear the default parameters we set earlier
$PSDefaultParameterValues.Clear()

#END SCRIPT

The script currently outputs to the PowerShell console as follows:

ComputerName Date                IsOnline PasswordChangeStatus DetailedStatus
------------ ----                -------- -------------------- --------------
HOME2        02/10/2019 06:18:20 ONLINE   FAILED               Exception calling "Invoke" with "2" argument(s): "Access is denied....



ComputerName Date                IsOnline PasswordChangeStatus DetailedStatus
------------ ----                -------- -------------------- --------------
HOME3        02/10/2019 06:18:22 ONLINE   FAILED               Exception calling "Invoke" with "2" argument(s): "Access is denied....



ComputerName Date                IsOnline PasswordChangeStatus DetailedStatus
------------ ----                -------- -------------------- --------------
HOME4        02/10/2019 06:18:29 OFFLINE  FAILED               Exception calling "Invoke" with "2" argument(s): "The network path was not found....

SO MY QUESTION - How can I get my script to consolidate the output as follows (with a single header row for both ONLINE and OFFLINE hosts) so that I can include it in the email body?

ComputerName Date                IsOnline PasswordChangeStatus DetailedStatus
------------ ----                -------- -------------------- --------------
HOME2        02/10/2019 06:18:20 ONLINE   FAILED               Exception calling "Invoke" with "2" argument(s): "Access is denied....
HOME3        02/10/2019 06:18:22 ONLINE   FAILED               Exception calling "Invoke" with "2" argument(s): "Access is denied....
HOME4        02/10/2019 06:18:29 OFFLINE  FAILED               Exception calling "Invoke" with "2" argument(s): "The network path was not found....

I believe that I may have to convert the console output into a table somehow and then include CSS in a header variable that can be passed to the Send-MailMessage cmdlet in order to apply some styling to the email message. But I have yet to figure out how to accomplish this, so any help from this platform would be very much appreciated.

Thank you all!

1 answer

  • answered 2019-02-11 10:14 Mötz

    A bit late to the game, but I would say that you should collect / store all your PSObjects in an ArrayList, and then apply your Format-Table. This example works as expected - I'm simply testing if the computer is online and commented your change password code.

    $Computers = @('SERVERA', 'SERVERB')
    
    [System.Collections.ArrayList] $res = New-Object -TypeName "System.Collections.ArrayList"
    
    #Now let's loop through each computer and change the specified account password.
    ForEach ($Computer in $Computers) {
    
        $Computer   =   $Computer.ToUpper()
        $IsOnline   =   "OFFLINE"
        $Status     =   "SUCCESS"
    
        Write-Verbose "Working on $Computer"
        If((Test-Connection -ComputerName $Computer -count 1 -ErrorAction 0)) {
            $IsOnline = "ONLINE"
            Write-Verbose "`t$Computer is Online"
        } Else { Write-Verbose "`t$Computer is OFFLINE" }
    
        <#
        Try {
            $Account = [ADSI]("WinNT://$Computer/$TargetAccount,user")
            $Account.psbase.invoke("setpassword",$Pwd1_Text)
            Write-Verbose "`tPassword Change completed successfully"
        }
        Catch {
            $Status = "FAILED"
            Write-Verbose "`tFailed to Change the password for $TargetAccount on $Computer. Error: $_"
            $StatsError = "$_"
        }
        #>
    
        $Obj = New-Object -TypeName PSObject -Property @{
            Date = "$(Get-Date)"
            ComputerName = $Computer
            IsOnline = $IsOnline
            PasswordChangeStatus = $Status
            DetailedStatus = $StatsError
        }
    
        $res.Add($Obj)
    
        #$Obj | Format-Table ComputerName, Date, IsOnline, PasswordChangeStatus, DetailedStatus -AutoSize
        #$Obj | Select-Object "ComputerName", "Date", "IsOnline", "PasswordChangeStatus", "DetailedStatus" | Export-Csv -Append -Path ".\output.csv" -NoTypeInformation
    
        If($Status -eq "FAILED" -or $IsOnline -eq "OFFLINE") {
            $Stream.Writeline("$Computer `t $IsOnline `t $Status")
        }
    }
    
    $res.ToArray() | Format-Table ComputerName, Date, IsOnline, PasswordChangeStatus, DetailedStatus -AutoSize
    
    #This will output 1 file, with all the computers in it, with only 1 set of headers.
    $res.ToArray() | Select-Object "ComputerName", "Date", "IsOnline", "PasswordChangeStatus", "DetailedStatus" | Export-Csv -Append -Path ".\output.csv" -NoTypeInformation
    

    It produces a result like this

    ComputerName Date                IsOnline PasswordChangeStatus DetailedStatus
    ------------ ----                -------- -------------------- --------------
    SERVERA      02/11/2019 11:05:57 ONLINE   SUCCESS
    SERVERB      02/11/2019 11:05:57 ONLINE   SUCCESS
    

    I guessing that because you keep executing the Format-Table cmdlet, the console doesn't know that you want to output the same object type. So because you are calling it multiple times, is has to make sure that the output table is being explicit parsed to the console. While when you have things in an array and then only call the Format-Table once - you get the expected result as shown.

    Please note that I also moved the Export-Csv out of your foreach and that also works like expected.