Noveris.VMwareReport.psm1
################ # Global settings $ErrorActionPreference = "Stop" $InformationPreference = "Continue" Set-StrictMode -Version 2 enum VMwareReportStatus { None = 0 Ok Warning Error } Class VMwareReportConfig { [string[]]$Reports [string]$Target [string]$Username [string]$Password [string]$SmtpServer [string]$SmtpSender [string[]]$Recipients [string[]]$IssueRecipients [string]$Site [PSCustomObject]$Settings VMwareReportConfig() { $this.Reports = [string[]]@() $this.Target = "" $this.Username = "" $this.Password = "" $this.SmtpServer = "" $this.SmtpSender = "" $this.Recipients = [string[]]@() $this.IssueRecipients = [string[]]@() $this.Site = "" $this.Settings = [PSCustomObject]@{} } } Class VMwareReportSummaryNotice { [VMwareReportStatus]$Status [string]$Report [string]$Description VMwareReportSummaryNotice() { $this.Status = [VMwareReportStatus]::None $this.Report = "" $this.Description = "" } } <# #> Function Set-VMwareReportConfig { [CmdletBinding()] param( [Parameter(mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$ConfigPath, [Parameter(mandatory=$false)] [ValidateNotNullOrEmpty()] [string[]]$Reports, [Parameter(mandatory=$false)] [ValidateNotNullOrEmpty()] [string]$Target, [Parameter(mandatory=$false)] [ValidateNotNull()] [PSCredential]$Credential, [Parameter(mandatory=$false)] [ValidateNotNullOrEmpty()] [string]$SmtpServer, [Parameter(mandatory=$false)] [ValidateNotNullOrEmpty()] [string]$SmtpSender, [Parameter(mandatory=$false)] [ValidateNotNullOrEmpty()] [string[]]$Recipients, [Parameter(mandatory=$false)] [ValidateNotNullOrEmpty()] [string[]]$IssueRecipients, [Parameter(mandatory=$false)] [AllowEmptyString()] [ValidateNotNull()] [string]$Site = "" ) process { [VMwareReportConfig]$config = New-Object VMwareReportConfig if (Test-Path -PathType Leaf $ConfigPath) { $config = [VMwareReportConfig](Get-Content $ConfigPath -Encoding UTF8 | ConvertFrom-Json) } if ($PSBoundParameters.Keys -contains "Reports") { $config.Reports = $Reports } if ($PSBoundParameters.Keys -contains "Target") { $config.Target = $Target } if ($PSBoundParameters.Keys -contains "Credential") { $config.Username = $Credential.Username $config.Password = $Credential.Password | ConvertFrom-SecureString } if ($PSBoundParameters.Keys -contains "SmtpServer") { $config.SmtpServer = $SmtpServer } if ($PSBoundParameters.Keys -contains "SmtpSender") { $config.SmtpSender = $SmtpSender } if ($PSBoundParameters.Keys -contains "Recipients") { $config.Recipients = $Recipients } if ($PSBoundParameters.Keys -contains "IssueRecipients") { $config.IssueRecipients = $IssueRecipients } if ($PSBoundParameters.Keys -contains "Site") { $config.Site = $Site } $config | ConvertTo-Json | Out-File -Encoding UTF8 $ConfigPath } } Function New-VMwareReportSummaryNotice { [CmdletBinding()] param( [Parameter(mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$Report, [Parameter(mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$Description, [Parameter(mandatory=$false)] [ValidateNotNull()] [VMwareReportStatus]$Status = [VMwareReportStatus]::Ok ) process { $notice = New-Object VMwareReportSummaryNotice $notice.Status = $Status $notice.Report = $Report $notice.Description = $Description $notice } } Function New-VMwareReportCell { [CmdletBinding()] param( [Parameter(mandatory=$false)] [AllowNull()] [AllowEmptyString()] [string]$Content = "", [Parameter(mandatory=$false)] [ValidateNotNull()] [VMwareReportStatus]$Status, [Parameter(mandatory=$false)] [ValidateNotNull()] [int]$ColumnSpan ) process { $header = "<td " if ($PSBoundParameters.Keys -contains "Status") { if ($Status -eq [VMwareReportStatus]::Error) { $header += " bgcolor=`"#FF0000`" " } elseif ($Status -eq [VMwareReportStatus]::Warning) { $header += " bgcolor=`"#FFCC00`" " } elseif ($Status -eq [VMwareReportStatus]::Ok) { $header += " bgcolor=`"#009900`" " } } if ($PSBoundParameters.Keys -contains "ColumnSpan") { $header += " colspan=`"$ColumnSpan`" " } $header + ">" + $Content + "</td>" } } <# #> Function New-VMwareReportSnapshotSummary { [CmdletBinding()] param( [Parameter(mandatory=$true)] [ValidateNotNull()] [VMwareReportConfig]$Config ) process { "<table><tr><th>VM</th><th>Consolidation Required</th><th>Snapshots</th></tr>" # Check for machines with snapshots or requiring consolidation $vmCount = 0 foreach ($vm in Get-VM -Server $config.Target) { Write-Information ("Getting snapshot information for: " + $vm.Name) $snapshots = $vm | Get-Snapshot $consolidate = $vm.ExtensionData.Runtime.consolidationNeeded if ($consolidate -eq $false -and ($snapshots | Measure-Object).Count -lt 1) { continue } $vmCount++ "<tr>" # Display VM name New-VMwareReportCell -Content $vm.Name # Consolidate status $cellStatus = [VMwareReportStatus]::None if ($consolidate -eq $true) { $cellStatus = [VMwareReportStatus]::Warning } New-VMwareReportCell -Status $cellStatus -Content $consolidate # Snapshot status $cellStatus = [VMwareReportStatus]::None $content = "None" if (($snapshots | Measure-Object).Count -gt 0) { $cellStatus = [VMwareReportStatus]::Warning $content = $snapshots | ForEach-Object { $snap = $_ ("<b>Snapshot: </b>" + $snap.Description + "<br>") ("<b>Size (MB): </b>" + ([int] $snap.SizeMB) + "<br>") ("<b>Created: </b>" + $snap.Created + "<br>") "<br>" } | Out-String } New-VMwareReportCell -Status $cellStatus -Content $content "</tr>" } if ($vmCount -eq 0) { "<tr>" New-VMwareReportCell -ColumnSpan 3 -Content "None" "</tr>" } else { New-VMwareReportSummaryNotice -Status Warning -Report "Snapshot Summary" -Description "Some VMs require consolidation or have snapshots" } "</table>" } } <# #> Function New-VMwareReportvCenterHealth { [CmdletBinding()] param( [Parameter(mandatory=$true)] [ValidateNotNull()] [VMwareReportConfig]$Config ) process { } } <# #> Function New-VMwareReportHostHealth { [CmdletBinding()] param( [Parameter(mandatory=$true)] [ValidateNotNull()] [VMwareReportConfig]$Config ) process { # Get a list of hosts to work with $vmhosts = Get-VMHost -Server $Config.Target # Build the table header "<table><tr><th>Condition</th>" $vmhosts | ForEach-Object { $name = $_.Name if (($config.Settings | Get-Member).Name -contains "StripHostSuffix" -and $config.Settings.StripHostSuffix -ne $null) { [string]$strip = $config.Settings.StripHostSuffix.ToString() $name = $name -replace $strip, "" } $name } | ForEach-Object { ("<th>{0}</th>" -f $_) } "</tr>" # Section: Connection State $disconnected = 0 $maintenance = 0 "<tr>" New-VMwareReportCell -Content "<b>Connection State</b>" foreach ($vmhost in $vmhosts) { $status = [VMwareReportStatus]::Error $content = "Unknown" switch ($vmhost.ConnectionState) { "Connected" { $status = [VMwareReportStatus]::Ok $content = "Connected" break } "Maintenance" { $maintenance++ $status = [VMwareReportStatus]::Warning $content = "Maintenance" break } default { $disconnected++ $status = [VMwareReportStatus]::Error $content = ("Not connected: " + $vmhost.ConnectionState.ToString()) break } } New-VMwareReportCell -Status $status -Content $content } "</tr>" if ($maintenance -gt 0) { New-VMwareReportSummaryNotice -Status Warning -Report "Host Health" -Description "Some hosts are in maintenance mode" } if ($disconnected -gt 0) { New-VMwareReportSummaryNotice -Status Error -Report "Host Health" -Description "Some hosts are not connected" } # Section: VM Counts "<tr>" $allVMs = Get-VM -Server $Config.Target $vmCount = ($allVMs | Measure-Object).Count $vmhostCount = ($vmhosts | Measure-Object).Count $vmPerHost = 0 $avgPerHost = 0 if ($vmhostCount -gt 0 -and $vmCount -gt 0) { $avgPerHost = 1 / $vmhostCount * 100 $vmPerHost = $vmCount / $vmhostCount } $content = "<b>Virtual Machines</b>" $content += "<br>VMs Per Host Count (Balanced): " + $vmPerHost.ToString("0.00") $content += "<br>VM Per Host % (Balanced): " + $avgPerHost.ToString("0.00") $content += "<br>Total VMs: " + $vmCount New-VMwareReportCell -Content $content foreach ($vmhost in $vmhosts) { $hostVMCount = ($vmhost | Get-VM | Measure-Object).Count $hostAvg = $hostVMCount / $vmCount * 100 New-VMwareReportCell -Content ("VMs: {0}<br>Avg: {1}" -f $hostVMCount, $hostAvg.ToString("0.00")) } "</tr>" # Section: CPU Utilisation $cpuTotalMhz = ($vmhosts | Measure-Object -sum -property CpuTotalMhz).Sum $cpuTotalGhz = $cpuTotalMhz / 1024 $cpuUsageAvgMhz = ($vmhosts | Measure-Object -average -property CpuUsageMhz).Average $cpuUsageAvgGhz = $cpuUsageAvgMhz / 1024 $cpuUsageMhz = ($vmhosts | Measure-Object -sum -property CpuUsageMhz).Sum $cpuUsageGhz = $cpuUsageMhz / 1024 "<tr>" $content = ("<b>CPU</b><br>CPU Usage (Ghz): [{0}/{1}]" -f $cpuUsageGhz.ToString("0.00"), $cpuTotalGhz.ToString("0.00")) $usagePct = 0 if ($cpuTotalGhz -gt 0) { $usagePct = (($cpuUsageGhz / $cpuTotalGhz) * 100) } $content += ("<br>CPU Usage %: {0}" -f $usagePct.ToString("0.00")) New-VMwareReportCell -Content $content foreach ($vmhost in $vmhosts) { $hostUsageMhz = $vmhost.CpuUsageMhz $hostTotalMhz = $vmhost.CpuTotalMhz $hostUsageGhz = $hostUsageMhz / 1024 $hostTotalGhz = $hostTotalMhz / 1024 # Calculate utilisation pct $utl = $hostUsageMhz / $hostTotalMhz * 100 $status = [VMwareReportStatus]::Ok if ($utl -gt 90) { $status = [VMwareReportStatus]::Error } elseif ($utl -gt 80) { $status = [VMwareReportStatus]::Warn } $content = ("Host Utilisation (Ghz): [{0}/{1}] = {2}%" -f $hostUsageGhz.ToString("0.00"), $hostTotalGhz.ToString("0.00"), $utl.ToString("0.00")) $loadContribution = 0 if ($cpuUsageGhz -gt 0) { $loadContribution = (($hostUsageGhz / $cpuUsageGhz) * 100) } $content += ("<br>Load Contribution: [{0}/{1}] = {2}%" -f $hostUsageGhz.ToString("0.00"), $cpuUsageGhz.ToString("0.00"), $loadContribution.ToString("0.00")) New-VMwareReportCell -Status $status -Content $content } "</tr>" # Section: Time synchronisation & { "<tr>" New-VMwareReportCell -Content ("<b>Time Synchronisation<br></b>Reference System: {0}" -f [System.Net.DNS]::GetHostname()) $timeErrorHosts = 0 $timeWarnHosts = 0 foreach ($vmhost in $vmhosts) { $esxcli = $vmhost | Get-EsxCli -V2 $hostTime = [DateTime]::Parse($esxcli.system.time.get.Invoke()).ToUniversalTime() $sysTime = [DateTime]::UtcNow $diffMins = ($hostTime - $sysTime).TotalMinutes $status = [VMwareReportStatus]::Ok if ([Math]::Abs($diffMins) -gt 4) { $status = [VMwareReportStatus]::Error $timeErrorHosts++ } elseif ([Math]::Abs($diffMins) -gt 1) { $status = [VMwareReportStatus]::Warning $timeWarnHosts++ } $mod = "+" if ($diffMins -lt 0) { $mod = "" } New-VMwareReportCell -Status $status -Content ("{0}{1} min" -f $mod, $diffMins.ToString("0.00")) } if ($timeErrorHosts -gt 0) { New-VMwareReportSummaryNotice -Status Error -Report "Host Health" -Description "Some hosts have significant time drift" } if ($timeWarnHosts -gt 0) { New-VMwareReportSummaryNotice -Status Warning -Report "Host Health" -Description "Some hosts have minor time drift" } "</tr>" } # End table "</table>" } } <# #> Function New-VMwareReportDatastoreHealth { [CmdletBinding()] param( [Parameter(mandatory=$true)] [ValidateNotNull()] [VMwareReportConfig]$Config ) process { } } <# #> Function New-VMwareReportNetworkHealth { [CmdletBinding()] param( [Parameter(mandatory=$true)] [ValidateNotNull()] [VMwareReportConfig]$Config ) process { } } <# #> Function New-VMwareReportvCenterSecurity { [CmdletBinding()] param( [Parameter(mandatory=$true)] [ValidateNotNull()] [VMwareReportConfig]$Config ) process { } } <# #> Function New-VMwareReportHostSecurity { [CmdletBinding()] param( [Parameter(mandatory=$true)] [ValidateNotNull()] [VMwareReportConfig]$Config ) process { } } <# #> Function New-VMwareReportVMSecurity { [CmdletBinding()] param( [Parameter(mandatory=$true)] [ValidateNotNull()] [VMwareReportConfig]$Config ) process { } } <# #> Function New-VMwareReport { [CmdletBinding()] param( [Parameter(mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$ConfigPath, [Parameter(mandatory=$false)] [switch]$DisplayContent = $false ) process { # Test for existance of config file if (!(Test-Path -PathType Leaf $ConfigPath)) { Write-Error "ConfigPath ($ConfigPath) does not exist" } # Read config as json and convert to VMwareReportConfig object Write-Information "Reading configuration" [VMwareReportConfig]$config = $null try { $config = [VMwareReportConfig](Get-Content $ConfigPath -Encoding UTF8 | ConvertFrom-Json) } catch { Write-Information "Failed to read the VMwareReport configuration file. See error below." Write-Information ("Error: " + $_.ToString()) throw $_ } # decrypt credential string Write-Information "Decrypting password" $secureString = $config.Password | ConvertTo-SecureString $byteStr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString) $password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($byteStr) # Display active configuration Write-Information "Current configuration:" $config | ConvertTo-Json # Import VMware.VimAutomation.Core module try { Write-Information "Importing VMware.VimAutomation.Core" #Install-Module VMware.PowerCli -Scope CurrentUser -Force -Confirm:$false -EA Ignore Import-Module VMware.VimAutomation.Core -EA Stop -WA Ignore | Out-Null } catch { Write-Information "Failed to import VMware.VimAutomation.Core module. This may not be installed. See error below." Write-Information ("Error: " + $_.ToString()) throw $_ } # Set PowerCli session configuration try { Write-Information "Setting PowerCli session configuration" Set-PowerCliConfiguration -Scope Session -DefaultVIServerMode Single -ParticipateInCeip $false -DisplayDeprecationWarnings $false -InvalidCertificateAction Ignore -Confirm:$false | Out-Null } catch { Write-Information "Error setting session configuration for PowerCli. See error below." Write-Information ("Error: " + $_.ToString()) throw $_ } # Connect to target server try { Write-Information "Connecting to target server" Connect-VIserver -Server $config.Target -User $config.Username -Password $password } catch { Write-Information "Error connecting to target server. See error below." Write-Information ("Error: " + $_.ToString()) throw $_ } # Create a new report frame $warnings = 0 $errors = 0 Write-Information "Generating new report frame" $content = New-VMwareReportFrame -Script { # List of all possible reports and scriptblock to generate that report $allReports = [ordered]@{ SnapshotSummary = { New-VMwareReportSnapshotSummary -Config $_ } vCenterHealth = { New-VMwareReportvCenterHealth -Config $_ } HostHealth = { New-VMwareReportHostHealth -Config $_ } DatastoreHealth = { New-VMwareReportDatastoreHealth -Config $_ } NetworkHealth = { New-VMwareReportNetworkHealth -Config $_ } vCenterSecurity = { New-VMwareReportvCenterSecurity -Config $_ } HostSecurity = { New-VMwareReportHostSecurity -Config $_ } VMSecurity = { New-VMwareReportVMSecurity -Config $_ } } # Determine ordering of reports in output $effectiveReports = @() if (($config.Reports | Measure-Object).Count -eq 0) { $effectiveReports = $allReports.Keys | ForEach-Object { $_ } } else { foreach ($report in $config.Reports) { if ($allReports.Keys -contains $report -and $effectiveReports -notcontains $report) { $effectiveReports += $report } if ($report -eq "*") { $allReports.Keys | ForEach-Object { if ($effectiveReports -notcontains $_) { $effectiveReports += $_ } } } } } Write-Information ("Effective Reports: " + $effectiveReports) # Run each of the reports specified $effectiveReports | ForEach-Object { try { Write-Information ("Running report: " + $_) ForEach-Object -InputObject $config -Process $allReports[$_] } catch { Write-Information ("Error generating report. Exception: " + $_.ToString()) "Error generating report." } "<br><p>" } } | ForEach-Object { # Check if it is a string or status object if ([VMwareReportSummaryNotice].IsAssignableFrom($_.GetType())) { [VMwareReportSummaryNotice]$notice = $_ if ($notice.Status -eq [VMwareReportStatus]::Warning) { $warnings++ } elseif ($notice.Status -eq [VMwareReportStatus]::Error) { $errors++ } } else { $_ } } | Out-String if ($DisplayContent) { Write-Information "Displaying Content:" $content } # Disconnect from target server try { Write-Information "Disconnecting from target server" Disconnect-VIServer -Server $config.Target -Confirm:$false -Force } catch { Write-Information "Error disconnecting from target server. See error below. Will stil continue/non-terminating" Write-Information ("Error: " + $_.ToString()) } # Build a list of recipients to email results to Write-Information "Building email recipient list" $recipients = New-Object 'System.Collections.Generic.Hashset[string]' $config.Recipients | ForEach-Object { $recipients.Add($_) | Out-Null } # Add issue recipients, if there are any errors or warnings if ($warnings -gt 0 -or $errors -gt 0) { Write-Information "Warnings or errors within report. Adding issue recipients." $config.IssueRecipients | ForEach-Object { $recipients.Add($_) | Out-Null } } # Send notification to recipients $dateStr = [DateTime]::Now.ToString("yyyyMMdd HHmm") $recipients | ForEach-Object { Write-Information "Sending notification to $_" $subject = "${dateStr}: " if (![string]::IsNullOrEmpty($config.Site)) { $subject += ("{0} - " -f $config.Site) } $subject += "VMware Status Report" $attempts = 3 while ($attempts -gt 0) { try { Send-MailMessage -To $_ -Subject $subject -Body $content -SmtpServer $config.SmtpServer -From $config.SmtpSender -BodyAsHtml break } catch { Write-Information ("Failure to send message. Exception: " + $_.ToString()) Write-Information "Retrying in 5 seconds." Start-Sleep 5 } $attempts-- } if ($attempts -lt 1) { Write-Information "Failed to send message to $_" } } } } Function New-VMwareReportFrame { [CmdletBinding()] param( [Parameter(mandatory=$true)] [ValidateNotNull()] [ScriptBlock]$Script ) process { # Write header to output "<head> <style> table { font-family: arial, sans-serif; border-collapse: collapse; width: 100%; } td, th { border: 1px solid #dddddd; text-align: left; padding: 8px; } tr:nth-child(even) { background-color: #dddddd; } </style> </head> <body>" try { $summaries = New-Object 'System.Collections.Generic.Hashset[VMwareReportSummaryNotice]' $content = & $Script | ForEach-Object { # Check if there is a summary status and save that separately to be written shortly if ([VMwareReportSummaryNotice].IsAssignableFrom($_.GetType())) { $summaries.Add($_) | Out-Null } $_ } # Write summary table "<table><tr><th>Report</th><th>Description</th></tr>" if ($summaries.Count -gt 0) { $summaries | ForEach-Object { "<tr>" New-VMwareReportCell -Content $_.Report New-VMwareReportCell -Status $_.Status -Content $_.Description "</tr>" } } else { New-VMwareReportCell -ColumnSpan 2 -Content "No Summaries" } "</table><br><p>" # Write report content $content } catch { "<b>Error during report processing</b>" ("Error: " + $_.ToString()) ($_.Exception | Format-List | Out-String -Stream | ForEach-Object { "{0}<br>" -f $_}) } "</body></html>" } } |