helpers/proxmox/ProxmoxHelpers.ps1
|
function Initialize-SSLBypass { if ($PSVersionTable.PSEdition -eq 'Core') { $PSDefaultParameterValues["Invoke-RestMethod:SkipCertificateCheck"] = $true $PSDefaultParameterValues["Invoke-WebRequest:SkipCertificateCheck"] = $true } else { # Compiled callback -- avoids scriptblock delegate marshaling failures # under rapid sequential requests in PS 5.1 if (-not ([System.Management.Automation.PSTypeName]'SSLValidator').Type) { Add-Type -TypeDefinition @" using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; public static class SSLValidator { private static bool OnValidateCertificate( object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { return true; } public static void OverrideValidation() { ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(OnValidateCertificate); ServicePointManager.Expect100Continue = false; ServicePointManager.DefaultConnectionLimit = 64; ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; } } "@ } [SSLValidator]::OverrideValidation() } Write-Warning "Ignoring SSL certificate validation errors. Use this option with caution." } function Invoke-ProxmoxAPI { param( [string]$Uri, [string]$Cookie, [string]$ApiToken, [string]$Method = 'Get', [hashtable]$Body ) $params = @{ Uri = $Uri; Method = $Method } if ($Body) { $params.Body = $Body } if ($ApiToken) { $authValue = "PVEAPIToken=$ApiToken" if ($PSVersionTable.PSEdition -eq 'Core') { $params.Headers = @{ Authorization = $authValue } $params.SkipHeaderValidation = $true } else { # PS 5.1: WebHeaderCollection rejects the token format via -Headers; # use a WebSession and inject the header directly $parsed = [System.Uri]$Uri $hostKey = $parsed.Host $tokenKey = "token:$hostKey" if (-not $script:_PmxSessions) { $script:_PmxSessions = @{} } if (-not $script:_PmxSessions[$tokenKey]) { $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession $script:_PmxSessions[$tokenKey] = $session } $script:_PmxSessions[$tokenKey].Headers['Authorization'] = $authValue $params.WebSession = $script:_PmxSessions[$tokenKey] } } elseif ($PSVersionTable.PSEdition -eq 'Core') { $params.Headers = @{ Cookie = $Cookie } } else { # PS 5.1: Cookie is a restricted header; use a cached WebSession $parsed = [System.Uri]$Uri $hostKey = $parsed.Host if (-not $script:_PmxSessions) { $script:_PmxSessions = @{} } if (-not $script:_PmxSessions[$hostKey] -or $script:_PmxSessionCookie[$hostKey] -ne $Cookie) { $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession $parts = $Cookie -split '=', 2 $session.Cookies.Add((New-Object System.Net.Cookie($parts[0], $parts[1], '/', $hostKey))) if (-not $script:_PmxSessionCookie) { $script:_PmxSessionCookie = @{} } $script:_PmxSessions[$hostKey] = $session $script:_PmxSessionCookie[$hostKey] = $Cookie } $params.WebSession = $script:_PmxSessions[$hostKey] } $maxRetries = if ($PSVersionTable.PSEdition -eq 'Core') { 0 } else { 2 } for ($attempt = 0; $attempt -le $maxRetries; $attempt++) { try { return (Invoke-RestMethod @params) } catch { $isClosed = $_.Exception.Message -match 'underlying connection was closed|unexpected error occurred on a send' if ($isClosed -and $attempt -lt $maxRetries) { # Reset the connection pool for this endpoint and retry try { $sp = [System.Net.ServicePointManager]::FindServicePoint([System.Uri]$Uri) $sp.CloseConnectionGroup('') } catch {} Start-Sleep -Milliseconds (300 * ($attempt + 1)) } else { throw } } } } function Connect-ProxmoxServer { <# .SYNOPSIS Authenticates to a Proxmox VE server and returns an auth cookie or validates an API token. .DESCRIPTION Supports two authentication methods: 1. Username + Password: Posts credentials to the Proxmox API ticket endpoint and returns a PVEAuthCookie string for use in subsequent API calls. 2. API Token: Validates the token by calling /api2/json/version and returns the token string for use in subsequent API calls via the Authorization header. .PARAMETER Server The base URI of the Proxmox VE server (e.g. https://192.168.1.100:8006). .PARAMETER Username The Proxmox username (e.g. root@pam). Used with Password auth. .PARAMETER Password The password for authentication. Used with Username. .PARAMETER ApiToken A Proxmox API token in the format user@realm!tokenid=uuid-secret. .EXAMPLE $cookie = Connect-ProxmoxServer -Server "https://192.168.1.100:8006" -Username "root@pam" -Password "MyPassword" Authenticates with username/password and returns the PVEAuthCookie. .EXAMPLE $token = Connect-ProxmoxServer -Server "https://192.168.1.100:8006" -ApiToken "root@pam!mytoken=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" Validates the API token and returns the token string for subsequent calls. #> param( [string]$Server, [string]$Username, [string]$Password, [string]$ApiToken ) if ($ApiToken) { # Validate the API token by calling the version endpoint $versionUri = "$Server/api2/json/version" $authValue = "PVEAPIToken=$ApiToken" if ($PSVersionTable.PSEdition -eq 'Core') { $version = Invoke-RestMethod -Method Get -Uri $versionUri -Headers @{ Authorization = $authValue } -SkipHeaderValidation -ErrorAction Stop } else { # PS 5.1: WebHeaderCollection rejects the token format; use HttpWebRequest directly $req = [System.Net.HttpWebRequest]::Create($versionUri) $req.Method = 'GET' $req.Headers.Add('Authorization', $authValue) try { $resp = $req.GetResponse() $reader = New-Object System.IO.StreamReader($resp.GetResponseStream()) $json = $reader.ReadToEnd() $reader.Close(); $resp.Close() $version = $json | ConvertFrom-Json } catch { throw "API token authentication failed: $($_.Exception.Message)" } } if (-not $version.data.version) { throw "API token authentication failed: no version data returned." } return $ApiToken } $login = Invoke-RestMethod -Method Post -Uri "$Server/api2/json/access/ticket" -Body @{ username = $Username password = $Password } if (-not $login.data.ticket) { throw "Authentication failed: no ticket returned." } return "PVEAuthCookie=$($login.data.ticket)" } function Get-ProxmoxNodes { <# .SYNOPSIS Returns all nodes in the Proxmox cluster. .PARAMETER Server The base URI of the Proxmox VE server. .PARAMETER Cookie The PVEAuthCookie from Connect-ProxmoxServer. .PARAMETER ApiToken A Proxmox API token string (user@realm!tokenid=secret). Used instead of Cookie. .EXAMPLE $nodes = Get-ProxmoxNodes -Server "https://192.168.1.100:8006" -Cookie $cookie Returns all nodes in the Proxmox cluster. .EXAMPLE $nodes = Get-ProxmoxNodes -Server "https://pve:8006" -ApiToken $token Returns all nodes using API token authentication. #> param( [string]$Server, [string]$Cookie, [string]$ApiToken ) (Invoke-ProxmoxAPI -Uri "$Server/api2/json/nodes" -Cookie $Cookie -ApiToken $ApiToken).data } function Get-ProxmoxVMs { <# .SYNOPSIS Returns all QEMU VMs on a specific Proxmox node. .PARAMETER Server The base URI of the Proxmox VE server. .PARAMETER Cookie The PVEAuthCookie from Connect-ProxmoxServer. .PARAMETER ApiToken A Proxmox API token string (user@realm!tokenid=secret). Used instead of Cookie. .PARAMETER Node The name of the Proxmox node. .EXAMPLE $vms = Get-ProxmoxVMs -Server "https://192.168.1.100:8006" -Cookie $cookie -Node "pve1" Returns all VMs on the pve1 node. .EXAMPLE $vms = Get-ProxmoxVMs -Server "https://pve:8006" -ApiToken $token -Node "pve1" Returns all VMs using API token authentication. #> param( [string]$Server, [string]$Cookie, [string]$Node, [string]$ApiToken ) (Invoke-ProxmoxAPI -Uri "$Server/api2/json/nodes/$Node/qemu" -Cookie $Cookie -ApiToken $ApiToken).data } function Get-ProxmoxNodeDetail { <# .SYNOPSIS Returns detailed status information for a Proxmox node. .DESCRIPTION Retrieves node status, network configuration, and version information including CPU, memory, swap, disk, and load averages. .PARAMETER Server The base URI of the Proxmox VE server. .PARAMETER Cookie The PVEAuthCookie from Connect-ProxmoxServer. .PARAMETER ApiToken A Proxmox API token string (user@realm!tokenid=secret). Used instead of Cookie. .PARAMETER Node The name of the Proxmox node. .EXAMPLE Get-ProxmoxNodeDetail -Server "https://192.168.1.100:8006" -Cookie $cookie -Node "pve1" Returns CPU, memory, disk, and version details for the pve1 node. .EXAMPLE Get-ProxmoxNodeDetail -Server "https://pve:8006" -ApiToken $token -Node "pve1" Returns node details using API token authentication. #> param( [string]$Server, [string]$Cookie, [string]$Node, [string]$ApiToken ) $status = (Invoke-ProxmoxAPI -Uri "$Server/api2/json/nodes/$Node/status" -Cookie $Cookie -ApiToken $ApiToken).data # Get the node's IP from its network interfaces try { $network = (Invoke-ProxmoxAPI -Uri "$Server/api2/json/nodes/$Node/network" -Cookie $Cookie -ApiToken $ApiToken).data $ip = ($network | Where-Object { $_.address -and $_.type -eq 'bridge' -or $_.type -eq 'eth' } | Select-Object -First 1).address if (-not $ip) { $ip = ($network | Where-Object { $_.address } | Select-Object -First 1).address } if (-not $ip) { $ip = "N/A" } } catch { $ip = "N/A" } # Get Proxmox version info try { $version = (Invoke-ProxmoxAPI -Uri "$Server/api2/json/nodes/$Node/version" -Cookie $Cookie -ApiToken $ApiToken).data } catch { $version = $null } [PSCustomObject]@{ Type = "Host" NodeName = $Node NodeID = if ($status.'boot-info'.uuid) { "$($status.'boot-info'.uuid)" } else { "N/A" } IPAddress = $ip Status = if ($status.uptime -gt 0) { "running" } else { "offline" } Uptime = "$($status.uptime)" PVEVersion = if ($version) { "$($version.version)-$($version.release)" } else { "N/A" } KernelVersion = if ($status.kversion) { "$($status.kversion)" } else { "N/A" } CPUModel = if ($status.cpuinfo.model) { "$($status.cpuinfo.model)" } else { "N/A" } CPUSockets = "$($status.cpuinfo.sockets)" CPUCores = "$($status.cpuinfo.cores)" CPUThreads = "$($status.cpuinfo.cpus)" CPUPercent = "{0:N1}%" -f ($status.cpu * 100) RAM_Used = "$([math]::Round($status.memory.used / 1MB)) MB" RAM_Total = "$([math]::Round($status.memory.total / 1MB)) MB" RAM_Free = "$([math]::Round($status.memory.free / 1MB)) MB" Swap_Used = "$([math]::Round($status.swap.used / 1MB)) MB" Swap_Total = "$([math]::Round($status.swap.total / 1MB)) MB" RootFS_Used = "$([math]::Round($status.rootfs.used / 1GB)) GB" RootFS_Total = "$([math]::Round($status.rootfs.total / 1GB)) GB" RootFS_Free = "$([math]::Round($status.rootfs.free / 1GB)) GB" LoadAvg1 = "$($status.loadavg[0])" LoadAvg5 = "$($status.loadavg[1])" LoadAvg15 = "$($status.loadavg[2])" } } function Get-ProxmoxVMDetail { <# .SYNOPSIS Returns detailed status and configuration for a specific Proxmox VM. .DESCRIPTION Retrieves VM configuration and live status including CPU, memory, disk, network I/O, and attempts to get the IP address via the QEMU guest agent. .PARAMETER Server The base URI of the Proxmox VE server. .PARAMETER Cookie The PVEAuthCookie from Connect-ProxmoxServer. .PARAMETER ApiToken A Proxmox API token string (user@realm!tokenid=secret). Used instead of Cookie. .PARAMETER Node The name of the Proxmox node hosting the VM. .PARAMETER VMID The numeric VM ID. .EXAMPLE Get-ProxmoxVMDetail -Server "https://192.168.1.100:8006" -Cookie $cookie -Node "pve1" -VMID 100 Returns detailed status for VM 100 on node pve1. .EXAMPLE Get-ProxmoxVMDetail -Server "https://pve:8006" -ApiToken $token -Node "pve1" -VMID 100 Returns VM details using API token authentication. #> param( [string]$Server, [string]$Cookie, [string]$Node, [int]$VMID, [string]$ApiToken ) $config = Invoke-ProxmoxAPI -Uri "$Server/api2/json/nodes/$Node/qemu/$VMID/config" -Cookie $Cookie -ApiToken $ApiToken $status = Invoke-ProxmoxAPI -Uri "$Server/api2/json/nodes/$Node/qemu/$VMID/status/current" -Cookie $Cookie -ApiToken $ApiToken # Try to get IP via guest agent try { $netInfo = Invoke-ProxmoxAPI -Uri "$Server/api2/json/nodes/$Node/qemu/$VMID/agent/network-get-interfaces" ` -Cookie $Cookie -ApiToken $ApiToken $ip = $netInfo.data.result | ForEach-Object { $_.'ip-addresses' | Where-Object { $_.'ip-address' -and $_.'ip-address' -notlike '127.*' -and $_.'ip-address-type' -eq 'ipv4' } } | Select-Object -ExpandProperty 'ip-address' -First 1 if (-not $ip) { $ip = "N/A" } } catch { $ip = "N/A" } [PSCustomObject]@{ VMID = "$VMID" Name = if ($config.data.name) { "$($config.data.name)" } else { "N/A" } Node = $Node Status = if ($status.data.status) { "$($status.data.status)" } else { "N/A" } QMPStatus = if ($status.data.qmpstatus) { "$($status.data.qmpstatus)" } else { "N/A" } IPAddress = $ip Uptime = "$($status.data.uptime)" CPUPercent = "{0:N1}%" -f ($status.data.cpu * 100) CPUs = "$($status.data.cpus)" CPUSockets = if ($config.data.sockets) { "$($config.data.sockets)" } else { "1" } CPUCores = if ($config.data.cores) { "$($config.data.cores)" } else { "$($status.data.cpus)" } RAM_Used = "$([math]::Round($status.data.mem / 1MB)) MB" RAM_Total = "$([math]::Round($status.data.maxmem / 1MB)) MB" Disk_Used = "$([math]::Round($status.data.disk / 1MB)) MB" Disk_Total = "$([math]::Round($status.data.maxdisk / 1MB)) MB" Disk_Read = "$([math]::Round($status.data.diskread / 1MB)) MB" Disk_Write = "$([math]::Round($status.data.diskwrite / 1MB)) MB" NetIn_KB = "$([math]::Round($status.data.netin / 1KB)) KB" NetOut_KB = "$([math]::Round($status.data.netout / 1KB)) KB" Tags = if ($status.data.tags) { "$($status.data.tags)" } else { "N/A" } HAGroup = if ($status.data.ha.group) { "$($status.data.ha.group)" } else { "N/A" } HAState = if ($status.data.ha.state) { "$($status.data.ha.state)" } else { "N/A" } HAManaged = if ($status.data.ha.managed) { "$($status.data.ha.managed)" } else { "N/A" } } } function Get-ProxmoxDashboard { <# .SYNOPSIS Builds a flat dashboard view combining Proxmox nodes and their VMs. .DESCRIPTION Queries the Proxmox VE API for node list, node details, and VM details, then returns a unified collection of objects suitable for an interactive Bootstrap Table dashboard. Each row represents a VM enriched with its parent node context including CPU, RAM, PVE version, and HA state. .PARAMETER Server The base URI of the Proxmox VE server (e.g. https://pve:8006). .PARAMETER Cookie The PVEAuthCookie string obtained from Connect-ProxmoxServer. .PARAMETER ApiToken A Proxmox API token string (user@realm!tokenid=secret). Used instead of Cookie. .EXAMPLE $cookie = Connect-ProxmoxServer -Server "https://pve:8006" -Username "root@pam" -Password "pass" Get-ProxmoxDashboard -Server "https://pve:8006" -Cookie $cookie Returns a flat dashboard view of all VMs across the specified Proxmox node. .EXAMPLE $data = Get-ProxmoxDashboard -Server $server -Cookie $cookie $data | Where-Object { $_.Status -eq "running" } Retrieves the dashboard and filters for running VMs. .EXAMPLE $cookie = Connect-ProxmoxServer -Server "https://pve:8006" -Username "root@pam" -Password $pass $data = Get-ProxmoxDashboard -Server "https://pve:8006" -Cookie $cookie Export-ProxmoxDashboardHtml -DashboardData $data -OutputPath "C:\Reports\proxmox.html" Start-Process "C:\Reports\proxmox.html" End-to-end: authenticate, gather data, export HTML, and open in browser. .OUTPUTS PSCustomObject[] Each object contains: Type (Host or VM), Name, Status, IPAddress, Node, CPU (combined percent + core/vCPU count), RAM (used / total), Disk (used / total for hosts, total size for VMs), NetworkIn, NetworkOut, Uptime, Tags, HAState. Nodes appear as Type="Host" rows; VMs appear as Type="VM (VMID)" rows. .NOTES Author : jason@wug.ninja Version : 1.0.0 Date : 2025-07-15 Requires: PowerShell 5.1+, network access to Proxmox VE API (port 8006). .LINK https://github.com/jayyx2/WhatsUpGoldPS #> param( [Parameter(Mandatory)][string]$Server, [string]$Cookie, [string]$ApiToken ) $nodes = Get-ProxmoxNodes -Server $Server -Cookie $Cookie -ApiToken $ApiToken $results = @() foreach ($node in $nodes) { $nodeName = $node.node $nodeDetail = Get-ProxmoxNodeDetail -Server $Server -Cookie $Cookie -ApiToken $ApiToken -Node $nodeName # Add the node itself as a row $nodeRamUsed = [double]($nodeDetail.RAM_Used -replace '[^\d.]') $nodeRamTotal = [double]($nodeDetail.RAM_Total -replace '[^\d.]') $nodeRamPct = if ($nodeRamTotal -gt 0) { '{0:N1}%' -f ($nodeRamUsed / $nodeRamTotal * 100) } else { '0.0%' } $results += [PSCustomObject]@{ Type = "Host" Name = $nodeName Status = $nodeDetail.Status IPAddress = $nodeDetail.IPAddress Node = $nodeName CPU = "$($nodeDetail.CPUPercent) ($($nodeDetail.CPUSockets)s/$($nodeDetail.CPUCores)c/$($nodeDetail.CPUThreads)t)" RAM = "$nodeRamPct ($($nodeDetail.RAM_Used) / $($nodeDetail.RAM_Total))" Disk = "$($nodeDetail.RootFS_Used) / $($nodeDetail.RootFS_Total)" NetworkIn = "N/A" NetworkOut = "N/A" Uptime = $nodeDetail.Uptime Tags = "N/A" HAState = "N/A" } $vms = Get-ProxmoxVMs -Server $Server -Cookie $Cookie -ApiToken $ApiToken -Node $nodeName foreach ($vm in $vms) { $vmDetail = Get-ProxmoxVMDetail -Server $Server -Cookie $Cookie -ApiToken $ApiToken -Node $nodeName -VMID $vm.vmid $vmRamUsed = [double]($vmDetail.RAM_Used -replace '[^\d.]') $vmRamTotal = [double]($vmDetail.RAM_Total -replace '[^\d.]') $vmRamPct = if ($vmRamTotal -gt 0) { '{0:N1}%' -f ($vmRamUsed / $vmRamTotal * 100) } else { '0.0%' } $results += [PSCustomObject]@{ Type = "VM ($($vmDetail.VMID))" Name = $vmDetail.Name Status = $vmDetail.Status IPAddress = $vmDetail.IPAddress Node = $nodeName CPU = "$($vmDetail.CPUPercent) ($($vmDetail.CPUSockets)s/$($vmDetail.CPUCores)c)" RAM = "$vmRamPct ($($vmDetail.RAM_Used) / $($vmDetail.RAM_Total))" Disk = $vmDetail.Disk_Total NetworkIn = "$($vmDetail.NetIn_KB)" NetworkOut = "$($vmDetail.NetOut_KB)" Uptime = $vmDetail.Uptime Tags = $vmDetail.Tags HAState = $vmDetail.HAState } } } return $results } function Export-ProxmoxDashboardHtml { <# .SYNOPSIS Renders Proxmox dashboard data into a self-contained HTML file. .DESCRIPTION Takes the output of Get-ProxmoxDashboard and generates a Bootstrap-based HTML report with sortable, searchable, and exportable tables. The report uses Bootstrap 5 and Bootstrap-Table for interactive filtering, sorting, column toggling, and CSV/JSON export. .PARAMETER DashboardData Array of PSCustomObject from Get-ProxmoxDashboard containing VM and node details. .PARAMETER OutputPath File path for the output HTML file. Parent directory must exist. .PARAMETER ReportTitle Title shown in the report header. Defaults to "Proxmox Dashboard". .PARAMETER TemplatePath Optional path to a custom HTML template. If omitted, uses the Proxmox-Dashboard-Template.html in the same directory as this script. .EXAMPLE $data = Get-ProxmoxDashboard -Server $server -Cookie $cookie Export-ProxmoxDashboardHtml -DashboardData $data -OutputPath "C:\Reports\proxmox.html" Exports the dashboard data to an HTML file using the default template. .EXAMPLE Export-ProxmoxDashboardHtml -DashboardData $data -OutputPath "$env:TEMP\proxmox.html" -ReportTitle "Lab Proxmox" Exports with a custom report title. .EXAMPLE $cookie = Connect-ProxmoxServer -Server $server -Username "root@pam" -Password $pass $data = Get-ProxmoxDashboard -Server $server -Cookie $cookie Export-ProxmoxDashboardHtml -DashboardData $data -OutputPath "C:\Reports\proxmox.html" Start-Process "C:\Reports\proxmox.html" Full pipeline: authenticate, gather, export, and open the report in a browser. .OUTPUTS System.Void Writes an HTML file to the path specified by OutputPath. .NOTES Author : jason@wug.ninja Version : 1.0.0 Date : 2025-07-15 Requires: PowerShell 5.1+, Proxmox-Dashboard-Template.html in the script directory. .LINK https://github.com/jayyx2/WhatsUpGoldPS #> [CmdletBinding()] param( [Parameter(Mandatory)]$DashboardData, [Parameter(Mandatory)][string]$OutputPath, [string]$ReportTitle = "Proxmox Dashboard", [string]$TemplatePath ) if (-not $TemplatePath) { $TemplatePath = Join-Path $PSScriptRoot "Proxmox-Dashboard-Template.html" } if (-not (Test-Path $TemplatePath)) { throw "HTML template not found at $TemplatePath" } $titleMap = @{ 'Type' = 'Type' 'Name' = 'Name' 'Status' = 'Status' 'IPAddress' = 'IP Address' 'Node' = 'Node' 'CPU' = 'CPU' 'RAM' = 'RAM' 'Disk' = 'Disk' 'NetworkIn' = 'Network In' 'NetworkOut' = 'Network Out' 'Uptime' = 'Uptime' 'Tags' = 'Tags' 'HAState' = 'HA State' } $firstObj = $DashboardData | Select-Object -First 1 $columns = @() foreach ($prop in $firstObj.PSObject.Properties) { $title = if ($titleMap.ContainsKey($prop.Name)) { $titleMap[$prop.Name] } else { ($prop.Name -creplace '(?<=[a-z])([A-Z])', ' $1').Trim() } $col = @{ field = $prop.Name title = $title sortable = $true searchable = $true } if ($prop.Name -eq 'Status') { $col.formatter = 'formatStatus' } if ($prop.Name -eq 'Type') { $col.formatter = 'formatType' } $columns += $col } $columnsJson = $columns | ConvertTo-Json -Depth 5 -Compress $dataJson = $DashboardData | ConvertTo-Json -Depth 5 -Compress $tableConfig = @" columns: $columnsJson, data: $dataJson "@ $html = Get-Content -Path $TemplatePath -Raw $html = $html -replace 'replaceThisHere', $tableConfig $html = $html -replace 'ReplaceYourReportNameHere', $ReportTitle $html = $html -replace 'ReplaceUpdateTimeHere', (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") Set-Content -Path $OutputPath -Value $html -Encoding UTF8 Write-Verbose "Proxmox Dashboard HTML written to $OutputPath" } # SIG # Begin signature block # MIIrwgYJKoZIhvcNAQcCoIIrszCCK68CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDagSZ/CArUAWrc # 4zZ2EW9rY0RBEH76UnElE0Uj0izWIqCCJNcwggVvMIIEV6ADAgECAhBI/JO0YFWU # jTanyYqJ1pQWMA0GCSqGSIb3DQEBDAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQI # DBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoM # EUNvbW9kbyBDQSBMaW1pdGVkMSEwHwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2Vy # dmljZXMwHhcNMjEwNTI1MDAwMDAwWhcNMjgxMjMxMjM1OTU5WjBWMQswCQYDVQQG # EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0aWdv # IFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQCN55QSIgQkdC7/FiMCkoq2rjaFrEfUI5ErPtx94jGgUW+s # hJHjUoq14pbe0IdjJImK/+8Skzt9u7aKvb0Ffyeba2XTpQxpsbxJOZrxbW6q5KCD # J9qaDStQ6Utbs7hkNqR+Sj2pcaths3OzPAsM79szV+W+NDfjlxtd/R8SPYIDdub7 # P2bSlDFp+m2zNKzBenjcklDyZMeqLQSrw2rq4C+np9xu1+j/2iGrQL+57g2extme # me/G3h+pDHazJyCh1rr9gOcB0u/rgimVcI3/uxXP/tEPNqIuTzKQdEZrRzUTdwUz # T2MuuC3hv2WnBGsY2HH6zAjybYmZELGt2z4s5KoYsMYHAXVn3m3pY2MeNn9pib6q # RT5uWl+PoVvLnTCGMOgDs0DGDQ84zWeoU4j6uDBl+m/H5x2xg3RpPqzEaDux5mcz # mrYI4IAFSEDu9oJkRqj1c7AGlfJsZZ+/VVscnFcax3hGfHCqlBuCF6yH6bbJDoEc # QNYWFyn8XJwYK+pF9e+91WdPKF4F7pBMeufG9ND8+s0+MkYTIDaKBOq3qgdGnA2T # OglmmVhcKaO5DKYwODzQRjY1fJy67sPV+Qp2+n4FG0DKkjXp1XrRtX8ArqmQqsV/ # AZwQsRb8zG4Y3G9i/qZQp7h7uJ0VP/4gDHXIIloTlRmQAOka1cKG8eOO7F/05QID # AQABo4IBEjCCAQ4wHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD # VR0OBBYEFDLrkpr/NZZILyhAQnAgNpFcF4XmMA4GA1UdDwEB/wQEAwIBhjAPBgNV # HRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMDMBsGA1UdIAQUMBIwBgYE # VR0gADAIBgZngQwBBAEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21v # ZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEE # KDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZI # hvcNAQEMBQADggEBABK/oe+LdJqYRLhpRrWrJAoMpIpnuDqBv0WKfVIHqI0fTiGF # OaNrXi0ghr8QuK55O1PNtPvYRL4G2VxjZ9RAFodEhnIq1jIV9RKDwvnhXRFAZ/ZC # J3LFI+ICOBpMIOLbAffNRk8monxmwFE2tokCVMf8WPtsAO7+mKYulaEMUykfb9gZ # pk+e96wJ6l2CxouvgKe9gUhShDHaMuwV5KZMPWw5c9QLhTkg4IUaaOGnSDip0TYl # d8GNGRbFiExmfS9jzpjoad+sPKhdnckcW67Y8y90z7h+9teDnRGWYpquRRPaf9xH # +9/DUp/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggYUMIID/KADAgECAhB6I67a # U2mWD5HIPlz0x+M/MA0GCSqGSIb3DQEBDAUAMFcxCzAJBgNVBAYTAkdCMRgwFgYD # VQQKEw9TZWN0aWdvIExpbWl0ZWQxLjAsBgNVBAMTJVNlY3RpZ28gUHVibGljIFRp # bWUgU3RhbXBpbmcgUm9vdCBSNDYwHhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1 # OTU5WjBVMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSww # KgYDVQQDEyNTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5nIENBIFIzNjCCAaIw # DQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAM2Y2ENBq26CK+z2M34mNOSJjNPv # IhKAVD7vJq+MDoGD46IiM+b83+3ecLvBhStSVjeYXIjfa3ajoW3cS3ElcJzkyZlB # nwDEJuHlzpbN4kMH2qRBVrjrGJgSlzzUqcGQBaCxpectRGhhnOSwcjPMI3G0hedv # 2eNmGiUbD12OeORN0ADzdpsQ4dDi6M4YhoGE9cbY11XxM2AVZn0GiOUC9+XE0wI7 # CQKfOUfigLDn7i/WeyxZ43XLj5GVo7LDBExSLnh+va8WxTlA+uBvq1KO8RSHUQLg # zb1gbL9Ihgzxmkdp2ZWNuLc+XyEmJNbD2OIIq/fWlwBp6KNL19zpHsODLIsgZ+WZ # 1AzCs1HEK6VWrxmnKyJJg2Lv23DlEdZlQSGdF+z+Gyn9/CRezKe7WNyxRf4e4bwU # trYE2F5Q+05yDD68clwnweckKtxRaF0VzN/w76kOLIaFVhf5sMM/caEZLtOYqYad # tn034ykSFaZuIBU9uCSrKRKTPJhWvXk4CllgrwIDAQABo4IBXDCCAVgwHwYDVR0j # BBgwFoAU9ndq3T/9ARP/FqFsggIv0Ao9FCUwHQYDVR0OBBYEFF9Y7UwxeqJhQo1S # gLqzYZcZojKbMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMBMG # A1UdJQQMMAoGCCsGAQUFBwMIMBEGA1UdIAQKMAgwBgYEVR0gADBMBgNVHR8ERTBD # MEGgP6A9hjtodHRwOi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNUaW1l # U3RhbXBpbmdSb290UjQ2LmNybDB8BggrBgEFBQcBAQRwMG4wRwYIKwYBBQUHMAKG # O2h0dHA6Ly9jcnQuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY1RpbWVTdGFtcGlu # Z1Jvb3RSNDYucDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNv # bTANBgkqhkiG9w0BAQwFAAOCAgEAEtd7IK0ONVgMnoEdJVj9TC1ndK/HYiYh9lVU # acahRoZ2W2hfiEOyQExnHk1jkvpIJzAMxmEc6ZvIyHI5UkPCbXKspioYMdbOnBWQ # Un733qMooBfIghpR/klUqNxx6/fDXqY0hSU1OSkkSivt51UlmJElUICZYBodzD3M # /SFjeCP59anwxs6hwj1mfvzG+b1coYGnqsSz2wSKr+nDO+Db8qNcTbJZRAiSazr7 # KyUJGo1c+MScGfG5QHV+bps8BX5Oyv9Ct36Y4Il6ajTqV2ifikkVtB3RNBUgwu/m # SiSUice/Jp/q8BMk/gN8+0rNIE+QqU63JoVMCMPY2752LmESsRVVoypJVt8/N3qQ # 1c6FibbcRabo3azZkcIdWGVSAdoLgAIxEKBeNh9AQO1gQrnh1TA8ldXuJzPSuALO # z1Ujb0PCyNVkWk7hkhVHfcvBfI8NtgWQupiaAeNHe0pWSGH2opXZYKYG4Lbukg7H # pNi/KqJhue2Keak6qH9A8CeEOB7Eob0Zf+fU+CCQaL0cJqlmnx9HCDxF+3BLbUuf # rV64EbTI40zqegPZdA+sXCmbcZy6okx/SjwsusWRItFA3DE8MORZeFb6BmzBtqKJ # 7l939bbKBy2jvxcJI98Va95Q5JnlKor3m0E7xpMeYRriWklUPsetMSf2NvUQa/E5 # vVyefQIwggYaMIIEAqADAgECAhBiHW0MUgGeO5B5FSCJIRwKMA0GCSqGSIb3DQEB # DAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxLTAr # BgNVBAMTJFNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBSb290IFI0NjAeFw0y # MTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5NTlaMFQxCzAJBgNVBAYTAkdCMRgwFgYD # VQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3RpZ28gUHVibGljIENv # ZGUgU2lnbmluZyBDQSBSMzYwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIB # gQCbK51T+jU/jmAGQ2rAz/V/9shTUxjIztNsfvxYB5UXeWUzCxEeAEZGbEN4QMgC # sJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NVDgFigOMYzB2OKhdqfWGVoYW3haT29PST # ahYkwmMv0b/83nbeECbiMXhSOtbam+/36F09fy1tsB8je/RV0mIk8XL/tfCK6cPu # YHE215wzrK0h1SWHTxPbPuYkRdkP05ZwmRmTnAO5/arnY83jeNzhP06ShdnRqtZl # V59+8yv+KIhE5ILMqgOZYAENHNX9SJDm+qxp4VqpB3MV/h53yl41aHU5pledi9lC # BbH9JeIkNFICiVHNkRmq4TpxtwfvjsUedyz8rNyfQJy/aOs5b4s+ac7IH60B+Ja7 # TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz44MPZ1f9+YEQIQty/NQd/2yGgW+ufflcZ # /ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBMdlyh2n5HirY4jKnFH/9gRvd+QOfdRrJZ # b1sCAwEAAaOCAWQwggFgMB8GA1UdIwQYMBaAFDLrkpr/NZZILyhAQnAgNpFcF4Xm # MB0GA1UdDgQWBBQPKssghyi47G9IritUpimqF6TNDDAOBgNVHQ8BAf8EBAMCAYYw # EgYDVR0TAQH/BAgwBgEB/wIBADATBgNVHSUEDDAKBggrBgEFBQcDAzAbBgNVHSAE # FDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsGA1UdHwREMEIwQKA+oDyGOmh0dHA6Ly9j # cmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5nUm9vdFI0Ni5j # cmwwewYIKwYBBQUHAQEEbzBtMEYGCCsGAQUFBzAChjpodHRwOi8vY3J0LnNlY3Rp # Z28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RSNDYucDdjMCMGCCsG # AQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOC # AgEABv+C4XdjNm57oRUgmxP/BP6YdURhw1aVcdGRP4Wh60BAscjW4HL9hcpkOTz5 # jUug2oeunbYAowbFC2AKK+cMcXIBD0ZdOaWTsyNyBBsMLHqafvIhrCymlaS98+Qp # oBCyKppP0OcxYEdU0hpsaqBBIZOtBajjcw5+w/KeFvPYfLF/ldYpmlG+vd0xqlqd # 099iChnyIMvY5HexjO2AmtsbpVn0OhNcWbWDRF/3sBp6fWXhz7DcML4iTAWS+MVX # eNLj1lJziVKEoroGs9Mlizg0bUMbOalOhOfCipnx8CaLZeVme5yELg09Jlo8BMe8 # 0jO37PU8ejfkP9/uPak7VLwELKxAMcJszkyeiaerlphwoKx1uHRzNyE6bxuSKcut # isqmKL5OTunAvtONEoteSiabkPVSZ2z76mKnzAfZxCl/3dq3dUNw4rg3sTCggkHS # RqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5JKdGvspbOrTfOXyXvmPL6E52z1NZJ6ctu # MFBQZH3pwWvqURR8AgQdULUvrxjUYbHHj95Ejza63zdrEcxWLDX6xWls/GDnVNue # KjWUH3fTv1Y8Wdho698YADR7TNx8X8z2Bev6SivBBOHY+uqiirZtg0y9ShQoPzmC # cn63Syatatvx157YK9hlcPmVoa1oDE5/L9Uo2bC5a4CH2RwwggY+MIIEpqADAgEC # AhAHnODk0RR/hc05c892LTfrMA0GCSqGSIb3DQEBDAUAMFQxCzAJBgNVBAYTAkdC # MRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3RpZ28gUHVi # bGljIENvZGUgU2lnbmluZyBDQSBSMzYwHhcNMjYwMjA5MDAwMDAwWhcNMjkwNDIx # MjM1OTU5WjBVMQswCQYDVQQGEwJVUzEUMBIGA1UECAwLQ29ubmVjdGljdXQxFzAV # BgNVBAoMDkphc29uIEFsYmVyaW5vMRcwFQYDVQQDDA5KYXNvbiBBbGJlcmlubzCC # AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPN6aN4B1yYWkI5b5TBj3I0V # V/peETrHb6EY4BHGxt8Ap+eT+WpEpJyEtRYPxEmNJL3A38Bkg7mwzPE3/1NK570Z # BCuBjSAn4mSDIgIuXZnvyBO9W1OQs5d67MlJLUAEufl18tOr3ST1DeO9gSjQSAE5 # Nql0QDxPnm93OZBon+Fz3CmE+z3MwAe2h4KdtRAnCqwM+/V7iBdbw+JOxolpx+7R # VjGyProTENIG3pe/hKvPb501lf8uBAADLdjZr5ip8vIWbf857Yw1Bu10nVI7HW3e # E8Cl5//d1ribHlzTzQLfttW+k+DaFsKZBBL56l4YAlIVRsrOiE1kdHYYx6IGrEA8 # 09R7+TZA9DzGqyFiv9qmJAbL4fDwetDeyIq+Oztz1LvEdy8Rcd0JBY+J4S0eDEFI # A3X0N8VcLeAwabKb9AjulKXwUeqCJLvN79CJ90UTZb2+I+tamj0dn+IKMEsJ4v4G # gx72sxFr9+6XziodtTg5Luf2xd6+PhhamOxF2px9LObhBLLEMyRsCHZIzVZOFKu9 # BpHQH7ufGB+Sa80Tli0/6LEyn9+bMYWi2ttn6lLOPThXMiQaooRUq6q2u3+F4SaP # lxVFLI7OJVMhar6nW6joBvELTJPmANSMjDSRFDfHRCdGbZsL/keELJNy+jZctF6V # vxQEjFM8/bazu6qYhrA7AgMBAAGjggGJMIIBhTAfBgNVHSMEGDAWgBQPKssghyi4 # 7G9IritUpimqF6TNDDAdBgNVHQ4EFgQU6YF0o0D5AVhKHbVocr8GaSIBibAwDgYD # VR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMw # SgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcCARYXaHR0cHM6 # Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAwPqA8oDqGOGh0 # dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5nQ0FS # MzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0cDovL2NydC5z # ZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIzNi5jcnQwIwYI # KwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqGSIb3DQEBDAUA # A4IBgQAEIsm4xnOd/tZMVrKwi3doAXvCwOA/RYQnFJD7R/bSQRu3wXEK4o9SIefy # e18B/q4fhBkhNAJuEvTQAGfqbbpxow03J5PrDTp1WPCWbXKX8Oz9vGWJFyJxRGft # kdzZ57JE00synEMS8XCwLO9P32MyR9Z9URrpiLPJ9rQjfHMb1BUdvaNayomm7aWL # AnD+X7jm6o8sNT5An1cwEAob7obWDM6sX93wphwJNBJAstH9Ozs6LwISOX6sKS7C # Km9N3Kp8hOUue0ZHAtZdFl6o5u12wy+zzieGEI50fKnN77FfNKFOWKlS6OJwlArc # bFegB5K89LcE5iNSmaM3VMB2ADV1FEcjGSHw4lTg1Wx+WMAMdl/7nbvfFxJ9uu5t # NiT54B0s+lZO/HztwXYQUczdsFon3pjsNrsk9ZlalBi5SHkIu+F6g7tWiEv3rtVA # pmJRnLkUr2Xq2a4nbslUCt4jKs5UX4V1nSX8OM++AXoyVGO+iTj7z+pl6XE9Gw/T # d6WKKKswggZiMIIEyqADAgECAhEApCk7bh7d16c0CIetek63JDANBgkqhkiG9w0B # AQwFADBVMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSww # KgYDVQQDEyNTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5nIENBIFIzNjAeFw0y # NTAzMjcwMDAwMDBaFw0zNjAzMjEyMzU5NTlaMHIxCzAJBgNVBAYTAkdCMRcwFQYD # VQQIEw5XZXN0IFlvcmtzaGlyZTEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTAw # LgYDVQQDEydTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5nIFNpZ25lciBSMzYw # ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDThJX0bqRTePI9EEt4Egc8 # 3JSBU2dhrJ+wY7JgReuff5KQNhMuzVytzD+iXazATVPMHZpH/kkiMo1/vlAGFrYN # 2P7g0Q8oPEcR3h0SftFNYxxMh+bj3ZNbbYjwt8f4DsSHPT+xp9zoFuw0HOMdO3sW # eA1+F8mhg6uS6BJpPwXQjNSHpVTCgd1gOmKWf12HSfSbnjl3kDm0kP3aIUAhsodB # YZsJA1imWqkAVqwcGfvs6pbfs/0GE4BJ2aOnciKNiIV1wDRZAh7rS/O+uTQcb6JV # zBVmPP63k5xcZNzGo4DOTV+sM1nVrDycWEYS8bSS0lCSeclkTcPjQah9Xs7xbOBo # CdmahSfg8Km8ffq8PhdoAXYKOI+wlaJj+PbEuwm6rHcm24jhqQfQyYbOUFTKWFe9 # 01VdyMC4gRwRAq04FH2VTjBdCkhKts5Py7H73obMGrxN1uGgVyZho4FkqXA8/uk6 # nkzPH9QyHIED3c9CGIJ098hU4Ig2xRjhTbengoncXUeo/cfpKXDeUcAKcuKUYRNd # GDlf8WnwbyqUblj4zj1kQZSnZud5EtmjIdPLKce8UhKl5+EEJXQp1Fkc9y5Ivk4A # ZacGMCVG0e+wwGsjcAADRO7Wga89r/jJ56IDK773LdIsL3yANVvJKdeeS6OOEiH6 # hpq2yT+jJ/lHa9zEdqFqMwIDAQABo4IBjjCCAYowHwYDVR0jBBgwFoAUX1jtTDF6 # omFCjVKAurNhlxmiMpswHQYDVR0OBBYEFIhhjKEqN2SBKGChmzHQjP0sAs5PMA4G # A1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUF # BwMIMEoGA1UdIARDMEEwNQYMKwYBBAGyMQECAQMIMCUwIwYIKwYBBQUHAgEWF2h0 # dHBzOi8vc2VjdGlnby5jb20vQ1BTMAgGBmeBDAEEAjBKBgNVHR8EQzBBMD+gPaA7 # hjlodHRwOi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNUaW1lU3RhbXBp # bmdDQVIzNi5jcmwwegYIKwYBBQUHAQEEbjBsMEUGCCsGAQUFBzAChjlodHRwOi8v # Y3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNUaW1lU3RhbXBpbmdDQVIzNi5j # cnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqGSIb3 # DQEBDAUAA4IBgQACgT6khnJRIfllqS49Uorh5ZvMSxNEk4SNsi7qvu+bNdcuknHg # XIaZyqcVmhrV3PHcmtQKt0blv/8t8DE4bL0+H0m2tgKElpUeu6wOH02BjCIYM6HL # InbNHLf6R2qHC1SUsJ02MWNqRNIT6GQL0Xm3LW7E6hDZmR8jlYzhZcDdkdw0cHhX # jbOLsmTeS0SeRJ1WJXEzqt25dbSOaaK7vVmkEVkOHsp16ez49Bc+Ayq/Oh2BAkST # Fog43ldEKgHEDBbCIyba2E8O5lPNan+BQXOLuLMKYS3ikTcp/Qw63dxyDCfgqXYU # hxBpXnmeSO/WA4NwdwP35lWNhmjIpNVZvhWoxDL+PxDdpph3+M5DroWGTc1ZuDa1 # iXmOFAK4iwTnlWDg3QNRsRa9cnG3FBBpVHnHOEQj4GMkrOHdNDTbonEeGvZ+4nSZ # XrwCW4Wv2qyGDBLlKk3kUW1pIScDCpm/chL6aUbnSsrtbepdtbCLiGanKVR/KC1g # sR0tC6Q0RfWOI4owggaCMIIEaqADAgECAhA2wrC9fBs656Oz3TbLyXVoMA0GCSqG # SIb3DQEBDAUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKTmV3IEplcnNleTEU # MBIGA1UEBxMLSmVyc2V5IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0 # d29yazEuMCwGA1UEAxMlVVNFUlRydXN0IFJTQSBDZXJ0aWZpY2F0aW9uIEF1dGhv # cml0eTAeFw0yMTAzMjIwMDAwMDBaFw0zODAxMTgyMzU5NTlaMFcxCzAJBgNVBAYT # AkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxLjAsBgNVBAMTJVNlY3RpZ28g # UHVibGljIFRpbWUgU3RhbXBpbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQCIndi5RWedHd3ouSaBmlRUwHxJBZvMWhUP2ZQQRLRBQIF3 # FJmp1OR2LMgIU14g0JIlL6VXWKmdbmKGRDILRxEtZdQnOh2qmcxGzjqemIk8et8s # E6J+N+Gl1cnZocew8eCAawKLu4TRrCoqCAT8uRjDeypoGJrruH/drCio28aqIVEn # 45NZiZQI7YYBex48eL78lQ0BrHeSmqy1uXe9xN04aG0pKG9ki+PC6VEfzutu6Q3I # cZZfm00r9YAEp/4aeiLhyaKxLuhKKaAdQjRaf/h6U13jQEV1JnUTCm511n5avv4N # +jSVwd+Wb8UMOs4netapq5Q/yGyiQOgjsP/JRUj0MAT9YrcmXcLgsrAimfWY3MzK # m1HCxcquinTqbs1Q0d2VMMQyi9cAgMYC9jKc+3mW62/yVl4jnDcw6ULJsBkOkrcP # LUwqj7poS0T2+2JMzPP+jZ1h90/QpZnBkhdtixMiWDVgh60KmLmzXiqJc6lGwqoU # qpq/1HVHm+Pc2B6+wCy/GwCcjw5rmzajLbmqGygEgaj/OLoanEWP6Y52Hflef3XL # vYnhEY4kSirMQhtberRvaI+5YsD3XVxHGBjlIli5u+NrLedIxsE88WzKXqZjj9Zi # 5ybJL2WjeXuOTbswB7XjkZbErg7ebeAQUQiS/uRGZ58NHs57ZPUfECcgJC+v2wID # AQABo4IBFjCCARIwHwYDVR0jBBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYD # VR0OBBYEFPZ3at0//QET/xahbIICL9AKPRQlMA4GA1UdDwEB/wQEAwIBhjAPBgNV # HRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMIMBEGA1UdIAQKMAgwBgYE # VR0gADBQBgNVHR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20v # VVNFUlRydXN0UlNBQ2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmwwNQYIKwYBBQUH # AQEEKTAnMCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC51c2VydHJ1c3QuY29tMA0G # CSqGSIb3DQEBDAUAA4ICAQAOvmVB7WhEuOWhxdQRh+S3OyWM637ayBeR7djxQ8Si # hTnLf2sABFoB0DFR6JfWS0snf6WDG2gtCGflwVvcYXZJJlFfym1Doi+4PfDP8s0c # qlDmdfyGOwMtGGzJ4iImyaz3IBae91g50QyrVbrUoT0mUGQHbRcF57olpfHhQESt # z5i6hJvVLFV/ueQ21SM99zG4W2tB1ExGL98idX8ChsTwbD/zIExAopoe3l6JrzJt # Pxj8V9rocAnLP2C8Q5wXVVZcbw4x4ztXLsGzqZIiRh5i111TW7HV1AtsQa6vXy63 # 3vCAbAOIaKcLAo/IU7sClyZUk62XD0VUnHD+YvVNvIGezjM6CRpcWed/ODiptK+e # vDKPU2K6synimYBaNH49v9Ih24+eYXNtI38byt5kIvh+8aW88WThRpv8lUJKaPn3 # 7+YHYafob9Rg7LyTrSYpyZoBmwRWSE4W6iPjB7wJjJpH29308ZkpKKdpkiS9WNsf # /eeUtvRrtIEiSJHN899L1P4l6zKVsdrUu1FX1T/ubSrsxrYJD+3f3aKg6yxdbugo # t06YwGXXiy5UUGZvOu3lXlxA+fC13dQ5OlL2gIb5lmF6Ii8+CQOYDwXM+yd9dbmo # cQsHjcRPsccUd5E9FiswEqORvz8g3s+jR3SFCgXhN4wz7NgAnOgpCdUo4uDyllU9 # PzGCBkEwggY9AgEBMGgwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28g # TGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5nIENB # IFIzNgIQB5zg5NEUf4XNOXPPdi036zANBglghkgBZQMEAgEFAKCBhDAYBgorBgEE # AYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwG # CisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCCW+0e3 # 8USEFCHCO4ey86raytjTtJxSEG6BPEIbLPj7pDANBgkqhkiG9w0BAQEFAASCAgAR # B5Ugj5E6ZnhLSlcoyP63O6L2CjLeIie8NEodOz3KHUp7On6TBSZRGzyjP7x9QRpQ # 6DQ2tHoeESocT/RnUSZqLDPULC9VzBIMYqGY+3tyWyMwtSyscRDbdkAHhMFu9kn2 # IokCcbExQBsQs/2cHtGqK87mCeHOOOv+tr1Kyvkgoxqa1XWcIrezcTJYSDCVMv15 # f03tdTDKkXujHPyKjhytVlhfsVpD9KKKD1kloJI6QHrMcta0aeS5VmyqSvZqti4l # 1Rtb2K/1fsebyoHRPDntuM5sPEWn0i4q1pq6oswxyFXhobd6Unb3mTDNLSQ9uE6h # YxXaj/rIv1su4DiVUOxkcPwH/pAaPFlJPmZE5BKid6Z/wi4MfokidSeh0cZBvNPM # 6M3pNqMQfHVGLVE3Es2ZAmi3byxOE93eE/xqZUd2MJ1tbZqT7Qcg1bJ7H4UEoLQc # GP+jX0HA2XKiDc7rmHqYPsMdEz/JW3acHs1rH2IPJqVLYwMB1BN4K7o3jizsi+BR # qDaCbsvr04tVy2x7lxeP7DmmA+oMfrJWIDrZWpqmVtx4Jo+2H6a/iOWoQVNiqWDR # wM2hEXfQvCs8erXdZmNz7FiTvfMh9LcDBL2/Wlgzuqafmoij623aTP5zB9WBrP1H # vp+Zfr0ya7K9tb3U47ywqAR6GHcaPIkfzjgHegnzK6GCAyMwggMfBgkqhkiG9w0B # CQYxggMQMIIDDAIBATBqMFUxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdv # IExpbWl0ZWQxLDAqBgNVBAMTI1NlY3RpZ28gUHVibGljIFRpbWUgU3RhbXBpbmcg # Q0EgUjM2AhEApCk7bh7d16c0CIetek63JDANBglghkgBZQMEAgIFAKB5MBgGCSqG # SIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTI2MDQwODAxMzYy # OFowPwYJKoZIhvcNAQkEMTIEML1k4+AKhHPM1ZUY0dXDM66R7m/MuBWWxwqZ5psj # 4IUsoPeUNPlbquBNtmbPvGnZtTANBgkqhkiG9w0BAQEFAASCAgBHXfsoCM8OLjMb # cT9HuJ3vlUCr/mvBsOS8m5W2BgZaSNBpqwvZd/oc1VGUdEqodXfbigXvB94SGtns # kKSEyYH9oJgaUuLc9y1EnX4lFo8GNsbVQ14BykRMQO/CqN1CvRyrE9dc82CkaAGM # tM426k3nSY8QtKlLrukYqAW3+7wpFLgbI1FMooyDnly9Xpnm/xI/Twn5MVhVXJj3 # E3uz8rvhQhDZuyW9qJwMC0HNXSHyO4pAKice7Cbh3J4tkNUIiCVHHLBFhbqwKbDm # baPl1InyldRkZKKVINNwaRifKx1ZPxqDApZdJSCKK5Px2yzfJJB22HJKJitvipDV # w3Bvxm/sYuqb7d5n9TlUulPVLhsp7NVV2FdkmOr2Pu/USB3/HEoBVbgnkQ9i7ppe # /FMrwdgDRdITSvXA8VJuYcvKz9f1/aZt0LDEoYVwF8Gx7SgiNxqlopmaQPwvOyOg # hD5iASJpkMz7wTbYTSSKRazYf6A0Oe+z++Y4fKSp1/YX86oYXqnLR05qLlttOlDN # 8MwkurcfmV0A4D/6qdMd8Nwh8fnMiqy/Zxi/KJ7la+nfBYYhUAB1bOQxUKrBlgtG # LzEWIEsOSqy4AZSeMtJ8JxOcnpZGRu0BY+S9bycrNM4ILTWUGDvN8XA4sny8oQwp # D98MY7aTR45lr0YL+lu7PVNuY/jxHA== # SIG # End signature block |