helpers/proxmox/ProxmoxHelpers.ps1
|
function Initialize-SSLBypass { if ($PSVersionTable.PSEdition -eq 'Core') { $PSDefaultParameterValues["Invoke-RestMethod:SkipCertificateCheck"] = $true $PSDefaultParameterValues["Invoke-WebRequest:SkipCertificateCheck"] = $true } else { [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } } Write-Warning "Ignoring SSL certificate validation errors. Use this option with caution." } function Connect-ProxmoxServer { <# .SYNOPSIS Authenticates to a Proxmox VE server and returns an auth cookie. .DESCRIPTION Posts credentials to the Proxmox API ticket endpoint and returns a PVEAuthCookie string for use in subsequent API calls. .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). .PARAMETER Password The password for authentication. .EXAMPLE $cookie = Connect-ProxmoxServer -Server "https://192.168.1.100:8006" -Username "root@pam" -Password "MyPassword" Authenticates and returns the PVEAuthCookie for subsequent API calls. .EXAMPLE $cookie = Connect-ProxmoxServer -Server "https://pve.lab.local:8006" -Username "admin@pve" -Password $pass Connects to Proxmox using a stored password variable. #> param( [string]$Server, [string]$Username, [string]$Password ) $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. .EXAMPLE $nodes = Get-ProxmoxNodes -Server "https://192.168.1.100:8006" -Cookie $cookie Returns all nodes in the Proxmox cluster. .EXAMPLE Get-ProxmoxNodes -Server $server -Cookie $cookie | ForEach-Object { $_.node } Lists all node names in the cluster. #> param( [string]$Server, [string]$Cookie ) (Invoke-RestMethod -Uri "$Server/api2/json/nodes" -Headers @{ Cookie = $Cookie }).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 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 Get-ProxmoxVMs -Server $server -Cookie $cookie -Node "pve1" | Where-Object { $_.status -eq "running" } Returns only running VMs on the node. #> param( [string]$Server, [string]$Cookie, [string]$Node ) (Invoke-RestMethod -Uri "$Server/api2/json/nodes/$Node/qemu" -Headers @{ Cookie = $Cookie }).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 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-ProxmoxNodes -Server $server -Cookie $cookie | ForEach-Object { Get-ProxmoxNodeDetail -Server $server -Cookie $cookie -Node $_.node } Returns detailed information for every node in the cluster. #> param( [string]$Server, [string]$Cookie, [string]$Node ) $status = (Invoke-RestMethod -Uri "$Server/api2/json/nodes/$Node/status" -Headers @{ Cookie = $Cookie }).data # Get the node's IP from its network interfaces try { $network = (Invoke-RestMethod -Uri "$Server/api2/json/nodes/$Node/network" -Headers @{ Cookie = $Cookie }).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-RestMethod -Uri "$Server/api2/json/nodes/$Node/version" -Headers @{ Cookie = $Cookie }).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 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 $vms = Get-ProxmoxVMs -Server $server -Cookie $cookie -Node "pve1" $vms | ForEach-Object { Get-ProxmoxVMDetail -Server $server -Cookie $cookie -Node "pve1" -VMID $_.vmid } Returns detailed information for every VM on the node. #> param( [string]$Server, [string]$Cookie, [string]$Node, [int]$VMID ) $config = Invoke-RestMethod -Uri "$Server/api2/json/nodes/$Node/qemu/$VMID/config" -Headers @{ Cookie = $Cookie } $status = Invoke-RestMethod -Uri "$Server/api2/json/nodes/$Node/qemu/$VMID/status/current" -Headers @{ Cookie = $Cookie } # Try to get IP via guest agent try { $netInfo = Invoke-RestMethod -Uri "$Server/api2/json/nodes/$Node/qemu/$VMID/agent/network-get-interfaces" ` -Headers @{ Cookie = $Cookie } $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. .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, [Parameter(Mandatory)][string]$Cookie ) $nodes = Get-ProxmoxNodes -Server $Server -Cookie $Cookie $results = @() foreach ($node in $nodes) { $nodeName = $node.node $nodeDetail = Get-ProxmoxNodeDetail -Server $Server -Cookie $Cookie -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 -Node $nodeName foreach ($vm in $vms) { $vmDetail = Get-ProxmoxVMDetail -Server $Server -Cookie $Cookie -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 # MIIVlwYJKoZIhvcNAQcCoIIViDCCFYQCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDHPB8yl5ZHYnSi # yndKtooHocn4Y0iAKU5LPKlYRuS18qCCEdMwggVvMIIEV6ADAgECAhBI/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/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggYaMIIEAqADAgECAhBiHW0M # UgGeO5B5FSCJIRwKMA0GCSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYD # VQQKEw9TZWN0aWdvIExpbWl0ZWQxLTArBgNVBAMTJFNlY3RpZ28gUHVibGljIENv # ZGUgU2lnbmluZyBSb290IFI0NjAeFw0yMTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5 # NTlaMFQxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzAp # BgNVBAMTIlNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwggGiMA0G # CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCbK51T+jU/jmAGQ2rAz/V/9shTUxjI # ztNsfvxYB5UXeWUzCxEeAEZGbEN4QMgCsJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NV # DgFigOMYzB2OKhdqfWGVoYW3haT29PSTahYkwmMv0b/83nbeECbiMXhSOtbam+/3 # 6F09fy1tsB8je/RV0mIk8XL/tfCK6cPuYHE215wzrK0h1SWHTxPbPuYkRdkP05Zw # mRmTnAO5/arnY83jeNzhP06ShdnRqtZlV59+8yv+KIhE5ILMqgOZYAENHNX9SJDm # +qxp4VqpB3MV/h53yl41aHU5pledi9lCBbH9JeIkNFICiVHNkRmq4TpxtwfvjsUe # dyz8rNyfQJy/aOs5b4s+ac7IH60B+Ja7TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz4 # 4MPZ1f9+YEQIQty/NQd/2yGgW+ufflcZ/ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBM # dlyh2n5HirY4jKnFH/9gRvd+QOfdRrJZb1sCAwEAAaOCAWQwggFgMB8GA1UdIwQY # MBaAFDLrkpr/NZZILyhAQnAgNpFcF4XmMB0GA1UdDgQWBBQPKssghyi47G9IritU # pimqF6TNDDAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNV # HSUEDDAKBggrBgEFBQcDAzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsG # A1UdHwREMEIwQKA+oDyGOmh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1 # YmxpY0NvZGVTaWduaW5nUm9vdFI0Ni5jcmwwewYIKwYBBQUHAQEEbzBtMEYGCCsG # AQUFBzAChjpodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2Rl # U2lnbmluZ1Jvb3RSNDYucDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0 # aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEABv+C4XdjNm57oRUgmxP/BP6YdURh # w1aVcdGRP4Wh60BAscjW4HL9hcpkOTz5jUug2oeunbYAowbFC2AKK+cMcXIBD0Zd # OaWTsyNyBBsMLHqafvIhrCymlaS98+QpoBCyKppP0OcxYEdU0hpsaqBBIZOtBajj # cw5+w/KeFvPYfLF/ldYpmlG+vd0xqlqd099iChnyIMvY5HexjO2AmtsbpVn0OhNc # WbWDRF/3sBp6fWXhz7DcML4iTAWS+MVXeNLj1lJziVKEoroGs9Mlizg0bUMbOalO # hOfCipnx8CaLZeVme5yELg09Jlo8BMe80jO37PU8ejfkP9/uPak7VLwELKxAMcJs # zkyeiaerlphwoKx1uHRzNyE6bxuSKcutisqmKL5OTunAvtONEoteSiabkPVSZ2z7 # 6mKnzAfZxCl/3dq3dUNw4rg3sTCggkHSRqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5J # KdGvspbOrTfOXyXvmPL6E52z1NZJ6ctuMFBQZH3pwWvqURR8AgQdULUvrxjUYbHH # j95Ejza63zdrEcxWLDX6xWls/GDnVNueKjWUH3fTv1Y8Wdho698YADR7TNx8X8z2 # Bev6SivBBOHY+uqiirZtg0y9ShQoPzmCcn63Syatatvx157YK9hlcPmVoa1oDE5/ # L9Uo2bC5a4CH2RwwggY+MIIEpqADAgECAhAHnODk0RR/hc05c892LTfrMA0GCSqG # SIb3DQEBDAUAMFQxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0 # ZWQxKzApBgNVBAMTIlNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYw # HhcNMjYwMjA5MDAwMDAwWhcNMjkwNDIxMjM1OTU5WjBVMQswCQYDVQQGEwJVUzEU # MBIGA1UECAwLQ29ubmVjdGljdXQxFzAVBgNVBAoMDkphc29uIEFsYmVyaW5vMRcw # FQYDVQQDDA5KYXNvbiBBbGJlcmlubzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC # AgoCggIBAPN6aN4B1yYWkI5b5TBj3I0VV/peETrHb6EY4BHGxt8Ap+eT+WpEpJyE # tRYPxEmNJL3A38Bkg7mwzPE3/1NK570ZBCuBjSAn4mSDIgIuXZnvyBO9W1OQs5d6 # 7MlJLUAEufl18tOr3ST1DeO9gSjQSAE5Nql0QDxPnm93OZBon+Fz3CmE+z3MwAe2 # h4KdtRAnCqwM+/V7iBdbw+JOxolpx+7RVjGyProTENIG3pe/hKvPb501lf8uBAAD # LdjZr5ip8vIWbf857Yw1Bu10nVI7HW3eE8Cl5//d1ribHlzTzQLfttW+k+DaFsKZ # BBL56l4YAlIVRsrOiE1kdHYYx6IGrEA809R7+TZA9DzGqyFiv9qmJAbL4fDwetDe # yIq+Oztz1LvEdy8Rcd0JBY+J4S0eDEFIA3X0N8VcLeAwabKb9AjulKXwUeqCJLvN # 79CJ90UTZb2+I+tamj0dn+IKMEsJ4v4Ggx72sxFr9+6XziodtTg5Luf2xd6+Phha # mOxF2px9LObhBLLEMyRsCHZIzVZOFKu9BpHQH7ufGB+Sa80Tli0/6LEyn9+bMYWi # 2ttn6lLOPThXMiQaooRUq6q2u3+F4SaPlxVFLI7OJVMhar6nW6joBvELTJPmANSM # jDSRFDfHRCdGbZsL/keELJNy+jZctF6VvxQEjFM8/bazu6qYhrA7AgMBAAGjggGJ # MIIBhTAfBgNVHSMEGDAWgBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU # 6YF0o0D5AVhKHbVocr8GaSIBibAwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQC # MAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIB # AwIwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EM # AQQBMEkGA1UdHwRCMEAwPqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2Vj # dGlnb1B1YmxpY0NvZGVTaWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBE # BggrBgEFBQcwAoY4aHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGlj # Q29kZVNpZ25pbmdDQVIzNi5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNl # Y3RpZ28uY29tMA0GCSqGSIb3DQEBDAUAA4IBgQAEIsm4xnOd/tZMVrKwi3doAXvC # wOA/RYQnFJD7R/bSQRu3wXEK4o9SIefye18B/q4fhBkhNAJuEvTQAGfqbbpxow03 # J5PrDTp1WPCWbXKX8Oz9vGWJFyJxRGftkdzZ57JE00synEMS8XCwLO9P32MyR9Z9 # URrpiLPJ9rQjfHMb1BUdvaNayomm7aWLAnD+X7jm6o8sNT5An1cwEAob7obWDM6s # X93wphwJNBJAstH9Ozs6LwISOX6sKS7CKm9N3Kp8hOUue0ZHAtZdFl6o5u12wy+z # zieGEI50fKnN77FfNKFOWKlS6OJwlArcbFegB5K89LcE5iNSmaM3VMB2ADV1FEcj # GSHw4lTg1Wx+WMAMdl/7nbvfFxJ9uu5tNiT54B0s+lZO/HztwXYQUczdsFon3pjs # Nrsk9ZlalBi5SHkIu+F6g7tWiEv3rtVApmJRnLkUr2Xq2a4nbslUCt4jKs5UX4V1 # nSX8OM++AXoyVGO+iTj7z+pl6XE9Gw/Td6WKKKsxggMaMIIDFgIBATBoMFQxCzAJ # BgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNl # Y3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYCEAec4OTRFH+FzTlzz3Yt # N+swDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZ # BgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYB # BAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgrniNRq3unsMHIfjz5JLsQPymjvAAMjG7 # 72jpgwewFbAwDQYJKoZIhvcNAQEBBQAEggIAph8t9qy2IlLiZwTqpCfd0DPpIdPQ # Z5eDr1APOCybLenHrXecYyYkplVv0KyS2GWIMDBatnsRD79xP9pPlT1RTC7oWwHm # 0H2RRA6PqoukQVPFioycbuWnvjSZEbwtpXsWikTD/rmCOQ10drAIdwJqizQRIQuJ # 4ksfl7dEl910XvK8jqXsMEBohL/AJ8mHR9wqbmLqrn4jYd5ryNXdPVq7P0k2z6ph # W36nJOyKTIxVRoUGts8QoZOLsTHFWhZfxck94rZMWHufYsJU3BN5X24qXpJaTjHV # Y3Ih2uqjrj6eqCVmRQVcTX5DS0k5/fAoVVvblLCW8ZhZvdcz6WL0UiODP6xmC8Nv # 27hFpt06lfB7x8L8WX2GINI4AEGKZ8d6DcgOLXMr1CQuUbWM6O7apnd/8G6uKCnm # VzaQtAbySplCkz5+htiNzAYr+5ptVxD+yJzsyFoKaW7mHUbDs2TLHTHcc7Yr3qQ8 # OG0FOUEpkZZgB+isteOzwgW8qsBgVq9TxYegxnQ64w4iiWlT5sIrZ+6blBQt9Q8C # ZdGc332Db0RjoNM9ZduN2jP4ctNVXvWEUH2gsCdKsRa/wVJX3Ec87lLvLIgovA8I # bA1tyLgVsi/Q6aYyvgRo1MwS89/7T+noU2JFivjzqkXu16yW6pBxNof51JVQtASg # GSHQkRSWOzLZ+mc= # SIG # End signature block |