helpers/vmware/VMwareHelpers.ps1
|
# ============================================================================= # VMware vSphere Helpers for WhatsUpGoldPS # # Two collection methods supported: # [1] VMware.PowerCLI module (Connect-VIServer, Get-VM, etc.) # [2] vCenter REST API direct (zero external dependencies -- uses Invoke-RestMethod) # # Functions suffixed with "REST" use the vCenter REST API directly. # ============================================================================= function Connect-VMware { <# .SYNOPSIS Connects to a VMware vCenter Server or ESXi host. .DESCRIPTION Wraps Connect-VIServer to establish a session with a vCenter Server or standalone ESXi host. The session is stored in PowerCLI's default connection state and used by all subsequent helper functions. .PARAMETER Server The hostname or IP address of the vCenter Server or ESXi host. .PARAMETER Credential PSCredential for authentication. .PARAMETER Port The port number. Defaults to 443. .PARAMETER IgnoreSSLErrors Skip SSL certificate validation. .EXAMPLE $cred = Get-Credential -UserName "administrator@vsphere.local" Connect-VMware -Server "vcenter01.lab.local" -Credential $cred Connects to vCenter Server using the specified credentials. .EXAMPLE Connect-VMware -Server "192.168.1.100" -Credential $cred -IgnoreSSLErrors Connects to a vCenter Server with self-signed certificates. #> param( [Parameter(Mandatory)][string]$Server, [Parameter(Mandatory)][PSCredential]$Credential, [int]$Port = 443, [switch]$IgnoreSSLErrors ) if ($IgnoreSSLErrors) { Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false -Scope Session -ErrorAction SilentlyContinue | Out-Null } try { $connection = Connect-VIServer -Server $Server -Credential $Credential -Port $Port -ErrorAction Stop Write-Verbose "Connected to VMware: $Server ($($connection.ProductLine) $($connection.Version))" return $connection } catch { throw "Failed to connect to VMware server $Server : $($_.Exception.Message)" } } function Get-VMwareClusters { <# .SYNOPSIS Returns all vSphere clusters. .DESCRIPTION Wraps Get-Cluster and returns a collection of cluster objects with HA, DRS, and EVC configuration details. .EXAMPLE Get-VMwareClusters Returns all clusters in the connected vCenter. .EXAMPLE Get-VMwareClusters | Where-Object { $_.HAEnabled -eq $true } Returns only HA-enabled clusters. #> foreach ($cluster in (Get-Cluster -ErrorAction Stop)) { [PSCustomObject]@{ Name = "$($cluster.Name)" HAEnabled = "$($cluster.HAEnabled)" HAFailoverLevel = "$($cluster.HAFailoverLevel)" DrsEnabled = "$($cluster.DrsEnabled)" DrsAutomationLevel = "$($cluster.DrsAutomationLevel)" EVCMode = "$($cluster.EVCMode)" } } } function Get-VMwareDatastores { <# .SYNOPSIS Returns all datastores in the connected vSphere environment. .DESCRIPTION Wraps Get-Datastore and returns capacity, free space, and usage information. .EXAMPLE Get-VMwareDatastores Returns all datastores with capacity and usage details. .EXAMPLE Get-VMwareDatastores | Where-Object { [double]$_.PercentFree -lt 20 } Returns datastores with less than 20% free space. #> foreach ($ds in (Get-Datastore -ErrorAction Stop)) { [PSCustomObject]@{ Name = "$($ds.Name)" CapacityGB = "$([math]::Round($ds.CapacityGB, 2))" FreeSpaceGB = "$([math]::Round($ds.FreeSpaceGB, 2))" PercentFree = "$([math]::Round(($ds.FreeSpaceGB / $ds.CapacityGB) * 100, 1))" Type = "$($ds.Type)" State = "$($ds.State)" } } } function Get-VMwareHosts { <# .SYNOPSIS Returns all ESXi hosts in the connected vSphere environment. .DESCRIPTION Wraps Get-VMHost and returns a simplified collection of ESXi host objects. .EXAMPLE Get-VMwareHosts Returns all ESXi hosts in the connected vCenter. .EXAMPLE Get-VMwareHosts | Where-Object { $_.ConnectionState -eq 'Connected' } Returns only connected ESXi hosts. #> Get-VMHost -ErrorAction Stop } function Get-VMwareHostDetail { <# .SYNOPSIS Gathers detailed information about a single ESXi host. .DESCRIPTION Takes an ESXi host object and returns comprehensive details including hardware, CPU, memory, networking, and real-time performance metrics. .PARAMETER VMHost An ESXi host object returned by Get-VMwareHosts or Get-VMHost. .EXAMPLE $hosts = Get-VMwareHosts Get-VMwareHostDetail -VMHost $hosts[0] Returns detailed information for the first ESXi host. .EXAMPLE Get-VMwareHosts | ForEach-Object { Get-VMwareHostDetail -VMHost $_ } Returns detailed information for every ESXi host. #> param( [Parameter(Mandatory)]$VMHost ) $mgmtIP = (Get-VMHostNetworkAdapter -VMHost $VMHost -VMKernel -ErrorAction SilentlyContinue | Where-Object { $_.ManagementTrafficEnabled } | Select-Object -First 1).IP if (-not $mgmtIP) { $mgmtIP = "N/A" } $clusterName = if ($VMHost.Parent -and $VMHost.Parent.GetType().Name -eq 'ClusterImpl') { "$($VMHost.Parent.Name)" } else { "N/A" } $cpuAvg = (Get-Stat -Entity $VMHost -Stat "cpu.usage.average" -Realtime -MaxSamples 5 -ErrorAction SilentlyContinue | Measure-Object -Property Value -Average).Average $memAvg = (Get-Stat -Entity $VMHost -Stat "mem.usage.average" -Realtime -MaxSamples 5 -ErrorAction SilentlyContinue | Measure-Object -Property Value -Average).Average $netAvg = (Get-Stat -Entity $VMHost -Stat "net.usage.average" -Realtime -MaxSamples 5 -ErrorAction SilentlyContinue | Measure-Object -Property Value -Average).Average $diskAvg = (Get-Stat -Entity $VMHost -Stat "disk.usage.average" -Realtime -MaxSamples 5 -ErrorAction SilentlyContinue | Measure-Object -Property Value -Average).Average $hostDatastores = ($VMHost | Get-Datastore -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name) -join ", " [PSCustomObject]@{ Type = "ESXi Host" Name = "$($VMHost.Name)" IPAddress = $mgmtIP Cluster = $clusterName ConnectionState = "$($VMHost.ConnectionState)" PowerState = "$($VMHost.PowerState)" Version = "$($VMHost.Version)" Build = "$($VMHost.Build)" Manufacturer = "$($VMHost.Manufacturer)" Model = "$($VMHost.Model)" CpuSockets = "$($VMHost.ExtensionData.Hardware.CpuInfo.NumCpuPackages)" CpuCores = "$($VMHost.ExtensionData.Hardware.CpuInfo.NumCpuCores)" CpuThreads = "$($VMHost.ExtensionData.Hardware.CpuInfo.NumCpuThreads)" CpuTotalMHz = "$($VMHost.CpuTotalMhz)" CpuUsageMHz = "$($VMHost.CpuUsageMhz)" MemoryTotalGB = "$([math]::Round($VMHost.MemoryTotalGB, 2))" MemoryUsageGB = "$([math]::Round($VMHost.MemoryUsageGB, 2))" CpuUsagePct = "$(if ($null -ne $cpuAvg) { [math]::Round($cpuAvg, 2) } else { 'N/A' })" MemUsagePct = "$(if ($null -ne $memAvg) { [math]::Round($memAvg, 2) } else { 'N/A' })" NetUsageKBps = "$(if ($null -ne $netAvg) { [math]::Round($netAvg, 2) } else { 'N/A' })" DiskUsageKBps = "$(if ($null -ne $diskAvg) { [math]::Round($diskAvg, 2) } else { 'N/A' })" Datastores = $hostDatastores } } function Get-VMwareVMs { <# .SYNOPSIS Returns all virtual machines in the connected vSphere environment. .DESCRIPTION Wraps Get-VM and returns a simplified collection of VM objects. .EXAMPLE Get-VMwareVMs Returns all VMs in the connected vCenter. .EXAMPLE Get-VMwareVMs | Where-Object { $_.PowerState -eq 'PoweredOn' } Returns only powered-on VMs. #> Get-VM -ErrorAction Stop } function Get-VMwareVMDetail { <# .SYNOPSIS Gathers detailed information about a single virtual machine. .DESCRIPTION Takes a VM object and returns comprehensive details including guest OS, CPU, memory, disk, network, and real-time performance metrics. .PARAMETER VM A VM object returned by Get-VMwareVMs or Get-VM. .EXAMPLE $vms = Get-VMwareVMs Get-VMwareVMDetail -VM $vms[0] Returns detailed information for the first VM. .EXAMPLE Get-VMwareVMs | ForEach-Object { Get-VMwareVMDetail -VM $_ } Returns detailed information for every VM. #> param( [Parameter(Mandatory)]$VM ) $guest = $VM | Get-VMGuest -ErrorAction SilentlyContinue $ipAddr = $guest.IPAddress | Where-Object { $_ -match '^\d{1,3}(\.\d{1,3}){3}$' } | Select-Object -First 1 if (-not $ipAddr) { $ipAddr = "N/A" } $nics = $VM | Get-NetworkAdapter -ErrorAction SilentlyContinue $disks = $VM | Get-HardDisk -ErrorAction SilentlyContinue # Parse datastore names from hard disk file paths ([DatastoreName] path/to.vmdk) $vmDatastores = @() if ($disks) { $vmDatastores = @($disks | ForEach-Object { if ($_.Filename -match '^\[([^\]]+)\]') { $Matches[1] } } | Select-Object -Unique) } $datastoreStr = if ($vmDatastores.Count -gt 0) { $vmDatastores -join ', ' } else { 'N/A' } $cpuAvg = $null; $memAvg = $null; $netAvg = $null; $diskAvg = $null; $diskLat = $null if ($VM.PowerState -eq 'PoweredOn') { $cpuAvg = (Get-Stat -Entity $VM -Stat "cpu.usage.average" -Realtime -MaxSamples 5 -ErrorAction SilentlyContinue | Measure-Object -Property Value -Average).Average $memAvg = (Get-Stat -Entity $VM -Stat "mem.usage.average" -Realtime -MaxSamples 5 -ErrorAction SilentlyContinue | Measure-Object -Property Value -Average).Average $netAvg = (Get-Stat -Entity $VM -Stat "net.usage.average" -Realtime -MaxSamples 5 -ErrorAction SilentlyContinue | Measure-Object -Property Value -Average).Average $diskAvg = (Get-Stat -Entity $VM -Stat "disk.usage.average" -Realtime -MaxSamples 5 -ErrorAction SilentlyContinue | Measure-Object -Property Value -Average).Average $diskLat = (Get-Stat -Entity $VM -Stat "disk.totalLatency.average" -Realtime -MaxSamples 5 -ErrorAction SilentlyContinue | Measure-Object -Property Value -Average).Average # Fallback to 5-minute interval stats if realtime returned nothing if ($null -eq $diskAvg) { $diskAvg = (Get-Stat -Entity $VM -Stat "disk.usage.average" -IntervalSecs 300 -MaxSamples 1 -ErrorAction SilentlyContinue | Measure-Object -Property Value -Average).Average } if ($null -eq $diskLat) { $diskLat = (Get-Stat -Entity $VM -Stat "disk.totalLatency.average" -IntervalSecs 300 -MaxSamples 1 -ErrorAction SilentlyContinue | Measure-Object -Property Value -Average).Average } } $hostName = "$($VM.VMHost.Name)" $clusterName = if ($VM.VMHost.Parent -and $VM.VMHost.Parent.GetType().Name -eq 'ClusterImpl') { "$($VM.VMHost.Parent.Name)" } else { "N/A" } [PSCustomObject]@{ Name = "$($VM.Name)" IPAddress = $ipAddr PowerState = "$($VM.PowerState)" ESXiHost = $hostName Cluster = $clusterName GuestOS = "$($VM.ExtensionData.Config.GuestFullName)" GuestFamily = "$($guest.GuestFamily)" ToolsStatus = "$($VM.ExtensionData.Guest.ToolsStatus)" NumCPU = "$($VM.NumCpu)" MemoryGB = "$($VM.MemoryGB)" ProvisionedSpaceGB = "$([math]::Round($VM.ProvisionedSpaceGB, 2))" UsedSpaceGB = "$([math]::Round($VM.UsedSpaceGB, 2))" NicCount = "$(@($nics).Count)" NicTypes = "$(($nics.Type | Select-Object -Unique) -join ', ')" NetworkNames = "$(($nics.NetworkName | Select-Object -Unique) -join ', ')" DiskCount = "$(@($disks).Count)" DiskTotalGB = "$([math]::Round(($disks | Measure-Object -Property CapacityGB -Sum).Sum, 2))" Datastores = $datastoreStr CpuUsagePct = "$(if ($null -ne $cpuAvg) { [math]::Round($cpuAvg, 2) } else { 'N/A' })" MemUsagePct = "$(if ($null -ne $memAvg) { [math]::Round($memAvg, 2) } else { 'N/A' })" NetUsageKBps = "$(if ($null -ne $netAvg) { [math]::Round($netAvg, 2) } else { 'N/A' })" DiskUsageKBps = "$(if ($null -ne $diskAvg) { [math]::Round($diskAvg, 2) } else { 'N/A' })" DiskLatencyMs = "$(if ($null -ne $diskLat) { [math]::Round($diskLat, 2) } else { 'N/A' })" } } function Get-VMwareDashboard { <# .SYNOPSIS Builds a flat dashboard view combining ESXi hosts and their VMs. .DESCRIPTION Gathers host and VM details from the connected vCenter, then returns a unified collection of objects suitable for rendering in an interactive Bootstrap Table dashboard. Each row is either an ESXi host or a VM, distinguished by a Type column. Columns that apply to only one type show "N/A" for the other, following the Proxmox dashboard pattern. .EXAMPLE Connect-VMware -Server "vcenter01" -Credential $cred Get-VMwareDashboard Returns a flat dashboard view of all hosts and VMs. .EXAMPLE $data = Get-VMwareDashboard Export-VMwareDashboardHtml -DashboardData $data -OutputPath "C:\Reports\vmware.html" Start-Process "C:\Reports\vmware.html" End-to-end: connect, gather, export, and open in browser. #> $results = @() $esxiHosts = Get-VMwareHosts foreach ($esxi in $esxiHosts) { $hostDetail = Get-VMwareHostDetail -VMHost $esxi # -- Host row -- $results += [PSCustomObject]@{ Type = "Host" Name = $hostDetail.Name PowerState = $hostDetail.PowerState IPAddress = $hostDetail.IPAddress Cluster = $hostDetail.Cluster ESXiHost = $hostDetail.Name GuestOS = "VMware ESXi $($hostDetail.Version)" ToolsStatus = "N/A" CPU = "$($hostDetail.CpuUsagePct)% ($($hostDetail.CpuSockets)s/$($hostDetail.CpuCores)c/$($hostDetail.CpuThreads)t)" Memory = "$($hostDetail.MemUsagePct)% ($($hostDetail.MemoryUsageGB) / $($hostDetail.MemoryTotalGB) GB)" CpuUsagePct = $hostDetail.CpuUsagePct MemUsagePct = $hostDetail.MemUsagePct NetUsageKBps = $hostDetail.NetUsageKBps DiskUsageKBps = $hostDetail.DiskUsageKBps Hardware = "$($hostDetail.Manufacturer) $($hostDetail.Model)".Trim() VersionBuild = "ESXi $($hostDetail.Version) Build $($hostDetail.Build)" Datastores = $hostDetail.Datastores ProvisionedSpaceGB = "N/A" UsedSpaceGB = "N/A" NicCount = "N/A" DiskCount = "N/A" DiskLatencyMs = "N/A" } # -- VM rows -- $vms = Get-VM -Location $esxi -ErrorAction SilentlyContinue foreach ($vm in $vms) { $vmDetail = Get-VMwareVMDetail -VM $vm $results += [PSCustomObject]@{ Type = "VM" Name = $vmDetail.Name PowerState = $vmDetail.PowerState IPAddress = $vmDetail.IPAddress Cluster = $vmDetail.Cluster ESXiHost = $vmDetail.ESXiHost GuestOS = $vmDetail.GuestOS ToolsStatus = $vmDetail.ToolsStatus CPU = "$($vmDetail.CpuUsagePct)% ($($vmDetail.NumCPU) vCPU)" Memory = "$($vmDetail.MemUsagePct)% ($($vmDetail.MemoryGB) GB)" CpuUsagePct = $vmDetail.CpuUsagePct MemUsagePct = $vmDetail.MemUsagePct NetUsageKBps = $vmDetail.NetUsageKBps DiskUsageKBps = $vmDetail.DiskUsageKBps Hardware = "N/A" VersionBuild = "N/A" Datastores = $vmDetail.Datastores ProvisionedSpaceGB = $vmDetail.ProvisionedSpaceGB UsedSpaceGB = $vmDetail.UsedSpaceGB NicCount = $vmDetail.NicCount DiskCount = $vmDetail.DiskCount DiskLatencyMs = $vmDetail.DiskLatencyMs } } } return $results } function Export-VMwareDashboardHtml { <# .SYNOPSIS Renders VMware dashboard data into a self-contained HTML file. .DESCRIPTION Takes the output of Get-VMwareDashboard and generates a Bootstrap-based HTML report with sortable, searchable, and exportable tables. .PARAMETER DashboardData Array of PSCustomObject from Get-VMwareDashboard. .PARAMETER OutputPath File path for the output HTML file. .PARAMETER ReportTitle Title shown in the report header. Defaults to "VMware vSphere Dashboard". .PARAMETER TemplatePath Optional path to a custom HTML template. If omitted, uses the VMware-Dashboard-Template.html in the same directory as this script. .EXAMPLE $data = Get-VMwareDashboard Export-VMwareDashboardHtml -DashboardData $data -OutputPath "C:\Reports\vmware.html" Exports the dashboard data to an HTML file using the default template. .EXAMPLE Export-VMwareDashboardHtml -DashboardData $data -OutputPath "$env:TEMP\vmware.html" -ReportTitle "Production vSphere" Exports with a custom report title. #> [CmdletBinding()] param( [Parameter(Mandatory)]$DashboardData, [Parameter(Mandatory)][string]$OutputPath, [string]$ReportTitle = "VMware vSphere Dashboard", [string]$TemplatePath ) if (-not $TemplatePath) { $TemplatePath = Join-Path $PSScriptRoot "VMware-Dashboard-Template.html" } if (-not (Test-Path $TemplatePath)) { throw "HTML template not found at $TemplatePath" } $firstObj = $DashboardData | Select-Object -First 1 $titleMap = @{ 'Type' = 'Type' 'Name' = 'Name' 'PowerState' = 'Power State' 'IPAddress' = 'IP Address' 'Cluster' = 'Cluster' 'ESXiHost' = 'ESXi Host' 'GuestOS' = 'Guest OS' 'ToolsStatus' = 'Tools Status' 'CPU' = 'CPU' 'Memory' = 'Memory' 'CpuUsagePct' = 'CPU %' 'MemUsagePct' = 'Mem %' 'NetUsageKBps' = 'Net KBps' 'DiskUsageKBps' = 'Disk KBps' 'Hardware' = 'Hardware' 'VersionBuild' = 'Version / Build' 'Datastores' = 'Datastores' 'ProvisionedSpaceGB' = 'Provisioned GB' 'UsedSpaceGB' = 'Used GB' 'NicCount' = 'NICs' 'DiskCount' = 'Disks' 'DiskLatencyMs' = 'Disk Latency ms' } $columns = @() foreach ($prop in $firstObj.PSObject.Properties) { $title = if ($titleMap.ContainsKey($prop.Name)) { $titleMap[$prop.Name] } else { ($prop.Name -creplace '([A-Z])', ' $1').Trim() } $col = @{ field = $prop.Name title = $title sortable = $true searchable = $true } if ($prop.Name -eq 'PowerState') { $col.formatter = 'formatPowerState' } if ($prop.Name -eq 'ToolsStatus') { $col.formatter = 'formatToolsStatus' } if ($prop.Name -eq 'Type') { $col.formatter = 'formatType' } $columns += $col } $columnsJson = $columns | ConvertTo-Json -Depth 5 -Compress $dataJson = ConvertTo-Json -InputObject @($DashboardData) -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 "VMware Dashboard HTML written to $OutputPath" } function Disconnect-VMware { <# .SYNOPSIS Disconnects from the VMware vCenter Server or ESXi host. .DESCRIPTION Wraps Disconnect-VIServer to cleanly close the vSphere session. .PARAMETER Server Optional server name to disconnect from. If omitted, disconnects all. .EXAMPLE Disconnect-VMware Disconnects from all connected vSphere servers. .EXAMPLE Disconnect-VMware -Server "vcenter01" Disconnects from a specific vCenter server. #> param( [string]$Server ) try { if ($Server) { Disconnect-VIServer -Server $Server -Confirm:$false -ErrorAction Stop } else { Disconnect-VIServer -Confirm:$false -Force -ErrorAction Stop } Write-Verbose "Disconnected from VMware" } catch { Write-Warning "VMware disconnect: $($_.Exception.Message)" } } # ============================================================================= # REST API Collection Method (zero external dependencies) # Uses vCenter REST API via Invoke-RestMethod (PS 5.1 built-in) # vSphere 6.5+ required for REST API support # ============================================================================= # Script-scoped session cache if (-not (Get-Variable -Name '_VMwareRESTSession' -Scope Script -ErrorAction SilentlyContinue)) { $script:_VMwareRESTSession = $null $script:_VMwareRESTServer = $null $script:_VMwareRESTPort = 443 } function Invoke-VMwareREST { <# .SYNOPSIS Internal helper -- calls a vCenter REST API endpoint with the cached session token. .PARAMETER Path API path (e.g., /api/vcenter/vm). .PARAMETER Method HTTP method. Defaults to GET. .PARAMETER Body Request body for POST/PUT. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Path, [string]$Method = 'GET', [string]$Body ) if (-not $script:_VMwareRESTSession) { throw "vCenter REST session not established. Call Connect-VMwareREST first." } $uri = "https://$($script:_VMwareRESTServer):$($script:_VMwareRESTPort)$Path" $headers = @{ 'vmware-api-session-id' = $script:_VMwareRESTSession } $params = @{ Uri = $uri Method = $Method Headers = $headers ErrorAction = 'Stop' } if ($Body) { $params.Body = $Body $params.ContentType = 'application/json' } return Invoke-RestMethod @params } function Connect-VMwareREST { <# .SYNOPSIS Authenticates to vCenter REST API and caches the session token. .PARAMETER Server vCenter server hostname or IP. .PARAMETER Credential PSCredential with vCenter username and password. .PARAMETER Port vCenter HTTPS port. Defaults to 443. .PARAMETER IgnoreSSLErrors Skip SSL certificate validation. .EXAMPLE $cred = Get-Credential Connect-VMwareREST -Server 'vcenter.lab.local' -Credential $cred -IgnoreSSLErrors #> param( [Parameter(Mandatory)][string]$Server, [Parameter(Mandatory)][PSCredential]$Credential, [int]$Port = 443, [switch]$IgnoreSSLErrors ) if ($IgnoreSSLErrors) { # WARNING: On PowerShell 5.1, SSL bypass is process-wide and cannot be scoped per-request. # This affects all .NET HTTP connections in this process until the callback is reset. # On PowerShell 7+, SkipCertificateCheck is per-request via PSDefaultParameterValues. if ($PSVersionTable.PSEdition -eq 'Core') { $Script:PSDefaultParameterValues["Invoke-RestMethod:SkipCertificateCheck"] = $true $Script:PSDefaultParameterValues["Invoke-WebRequest:SkipCertificateCheck"] = $true } else { # PowerShell 5.1 SSL bypass — process-wide try { Add-Type @" using System.Net; using System.Security.Cryptography.X509Certificates; public class TrustAllCertsPolicy : ICertificatePolicy { public bool CheckValidationResult(ServicePoint sp, X509Certificate cert, WebRequest req, int problem) { return true; } } "@ [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy } catch { # Type may already be added [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } } } Write-Warning "Ignoring SSL certificate validation errors. This affects all connections in this process." } [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 $script:_VMwareRESTServer = $Server $script:_VMwareRESTPort = $Port $uri = "https://${Server}:${Port}/api/session" $authBytes = [System.Text.Encoding]::UTF8.GetBytes("$($Credential.UserName):$($Credential.GetNetworkCredential().Password)") $authHeader = [System.Convert]::ToBase64String($authBytes) $authBytes = $null $headers = @{ Authorization = "Basic $authHeader" } try { $session = Invoke-RestMethod -Uri $uri -Method POST -Headers $headers -ErrorAction Stop # vCenter 7+ returns a plain string token; 6.x returns JSON with value if ($session -is [string]) { $script:_VMwareRESTSession = $session.Trim('"') } elseif ($session.value) { $script:_VMwareRESTSession = $session.value } else { $script:_VMwareRESTSession = "$session" } Write-Verbose "Connected to vCenter REST API at ${Server}:${Port}" return @{ Server = $Server; Port = $Port } } catch { throw "Failed to authenticate to vCenter REST API: $($_.Exception.Message)" } } function Get-VMwareClustersREST { <# .SYNOPSIS Returns all clusters from vCenter via REST API. .EXAMPLE Get-VMwareClustersREST #> $clusters = Invoke-VMwareREST -Path '/api/vcenter/cluster' foreach ($c in $clusters) { $detail = $null try { $detail = Invoke-VMwareREST -Path "/api/vcenter/cluster/$($c.cluster)" } catch { } [PSCustomObject]@{ Name = "$($c.name)" HAEnabled = if ($detail -and $detail.ha_enabled -ne $null) { $detail.ha_enabled } else { 'N/A' } DrsEnabled = if ($detail -and $detail.drs_enabled -ne $null) { $detail.drs_enabled } else { 'N/A' } ClusterId = "$($c.cluster)" } } } function Get-VMwareHostsREST { <# .SYNOPSIS Returns all ESXi hosts from vCenter via REST API. .EXAMPLE Get-VMwareHostsREST #> $hosts = Invoke-VMwareREST -Path '/api/vcenter/host' foreach ($h in $hosts) { [PSCustomObject]@{ Name = "$($h.name)" HostId = "$($h.host)" ConnectionState = "$($h.connection_state)" PowerState = "$($h.power_state)" } } } function Get-VMwareHostDetailREST { <# .SYNOPSIS Returns detailed ESXi host information via REST API. .PARAMETER HostId The vCenter host MoRef ID (e.g., host-10). .PARAMETER HostName The ESXi host name (for output). .EXAMPLE Get-VMwareHostDetailREST -HostId 'host-10' -HostName 'esxi01.lab.local' #> param( [Parameter(Mandatory)][string]$HostId, [string]$HostName ) $ip = $null # Get management IP from network interfaces try { $nics = Invoke-VMwareREST -Path "/api/vcenter/host/$HostId" # Host detail endpoint may not have IP; try to resolve hostname if ($HostName) { try { $resolved = [System.Net.Dns]::GetHostAddresses($HostName) | Where-Object { $_.AddressFamily -eq 'InterNetwork' } | Select-Object -First 1 if ($resolved) { $ip = $resolved.IPAddressToString } } catch { } } } catch { } [PSCustomObject]@{ Type = 'ESXi Host' Name = if ($HostName) { $HostName } else { $HostId } IPAddress = if ($ip) { $ip } else { '' } HostId = $HostId } } function Get-VMwareVMsREST { <# .SYNOPSIS Returns all VMs from vCenter via REST API. .EXAMPLE Get-VMwareVMsREST #> $vms = Invoke-VMwareREST -Path '/api/vcenter/vm' foreach ($vm in $vms) { [PSCustomObject]@{ Name = "$($vm.name)" VMId = "$($vm.vm)" PowerState = "$($vm.power_state)" CpuCount = if ($vm.cpu_count) { $vm.cpu_count } else { 0 } MemorySizeMB = if ($vm.memory_size_MiB) { $vm.memory_size_MiB } else { 0 } } } } function Get-VMwareVMDetailREST { <# .SYNOPSIS Returns detailed VM information via REST API. .PARAMETER VMId The vCenter VM MoRef ID (e.g., vm-10). .PARAMETER VMName The VM name (for output). .EXAMPLE Get-VMwareVMDetailREST -VMId 'vm-10' -VMName 'MyServer' #> param( [Parameter(Mandatory)][string]$VMId, [string]$VMName ) $detail = $null try { $detail = Invoke-VMwareREST -Path "/api/vcenter/vm/$VMId" } catch { } $ip = $null $guestOS = 'N/A' $guestFamily = 'N/A' $toolsStatus = 'N/A' if ($detail) { if ($detail.guest_OS) { $guestOS = "$($detail.guest_OS)" } # Try to get guest identity for IP try { $identity = Invoke-VMwareREST -Path "/api/vcenter/vm/$VMId/guest/identity" if ($identity.ip_address) { $ip = "$($identity.ip_address)" } if ($identity.family) { $guestFamily = "$($identity.family)" } if ($identity.name) { $guestOS = "$($identity.name)" } } catch { } # Try tools info try { $tools = Invoke-VMwareREST -Path "/api/vcenter/vm/$VMId/tools" if ($tools.run_state) { $toolsStatus = "$($tools.run_state)" } } catch { } } $numCpu = 0; $memGB = 0; $diskCount = 0; $nicCount = 0 if ($detail) { if ($detail.cpu -and $detail.cpu.count) { $numCpu = $detail.cpu.count } if ($detail.memory -and $detail.memory.size_MiB) { $memGB = [math]::Round($detail.memory.size_MiB / 1024, 2) } if ($detail.disks) { $diskCount = @($detail.disks.PSObject.Properties).Count } if ($detail.nics) { $nicCount = @($detail.nics.PSObject.Properties).Count } } $hostName = 'N/A' # VM detail doesn't always include host; we pass it from the caller if needed [PSCustomObject]@{ Name = if ($VMName) { $VMName } else { $VMId } IPAddress = if ($ip) { $ip } else { '' } PowerState = if ($detail -and $detail.power_state) { "$($detail.power_state)" } else { 'N/A' } GuestOS = $guestOS GuestFamily = $guestFamily ToolsStatus = $toolsStatus NumCPU = $numCpu MemoryGB = $memGB DiskCount = $diskCount NicCount = $nicCount VMId = $VMId } } function Disconnect-VMwareREST { <# .SYNOPSIS Disconnects from vCenter REST API session. .EXAMPLE Disconnect-VMwareREST #> if ($script:_VMwareRESTSession) { try { Invoke-VMwareREST -Path '/api/session' -Method DELETE Write-Verbose "Disconnected from vCenter REST API" } catch { Write-Verbose "vCenter REST disconnect: $($_.Exception.Message)" } $script:_VMwareRESTSession = $null } } # SIG # Begin signature block # MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAN/DN7AUEAzD91 # glqFJapZt+S48qxWjMARDaPeIr3Sp6CCJQ0wggVvMIIEV6ADAgECAhBI/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/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggWNMIIEdaADAgECAhAOmxiO # +dAt5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYD # VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAi # BgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAw # MDBaFw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp # Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERp # Z2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC # AgoCggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsb # hA3EMB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iT # cMKyunWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGb # NOsFxl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclP # XuU15zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCr # VYJBMtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFP # ObURWBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTv # kpI6nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWM # cCxBYKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls # 5Q5SUUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBR # a2+xq4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6 # MIIBNjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qY # rhwPTzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8E # BAMCAYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k # aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDig # NoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v # dENBLmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCg # v0NcVec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQT # SnovLbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh # 65ZyoUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSw # uKFWjuyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAO # QGPFmCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjD # TZ9ztwGpn1eqXijiuZQwggYaMIIEAqADAgECAhBiHW0MUgGeO5B5FSCJIRwKMA0G # CSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExp # bWl0ZWQxLTArBgNVBAMTJFNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBSb290 # IFI0NjAeFw0yMTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5NTlaMFQxCzAJBgNVBAYT # AkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3RpZ28g # UHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwggGiMA0GCSqGSIb3DQEBAQUAA4IB # jwAwggGKAoIBgQCbK51T+jU/jmAGQ2rAz/V/9shTUxjIztNsfvxYB5UXeWUzCxEe # AEZGbEN4QMgCsJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NVDgFigOMYzB2OKhdqfWGV # oYW3haT29PSTahYkwmMv0b/83nbeECbiMXhSOtbam+/36F09fy1tsB8je/RV0mIk # 8XL/tfCK6cPuYHE215wzrK0h1SWHTxPbPuYkRdkP05ZwmRmTnAO5/arnY83jeNzh # P06ShdnRqtZlV59+8yv+KIhE5ILMqgOZYAENHNX9SJDm+qxp4VqpB3MV/h53yl41 # aHU5pledi9lCBbH9JeIkNFICiVHNkRmq4TpxtwfvjsUedyz8rNyfQJy/aOs5b4s+ # ac7IH60B+Ja7TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz44MPZ1f9+YEQIQty/NQd/ # 2yGgW+ufflcZ/ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBMdlyh2n5HirY4jKnFH/9g # Rvd+QOfdRrJZb1sCAwEAAaOCAWQwggFgMB8GA1UdIwQYMBaAFDLrkpr/NZZILyhA # QnAgNpFcF4XmMB0GA1UdDgQWBBQPKssghyi47G9IritUpimqF6TNDDAOBgNVHQ8B # Af8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNVHSUEDDAKBggrBgEFBQcD # AzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsGA1UdHwREMEIwQKA+oDyG # Omh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5n # Um9vdFI0Ni5jcmwwewYIKwYBBQUHAQEEbzBtMEYGCCsGAQUFBzAChjpodHRwOi8v # Y3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RSNDYu # cDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG # 9w0BAQwFAAOCAgEABv+C4XdjNm57oRUgmxP/BP6YdURhw1aVcdGRP4Wh60BAscjW # 4HL9hcpkOTz5jUug2oeunbYAowbFC2AKK+cMcXIBD0ZdOaWTsyNyBBsMLHqafvIh # rCymlaS98+QpoBCyKppP0OcxYEdU0hpsaqBBIZOtBajjcw5+w/KeFvPYfLF/ldYp # mlG+vd0xqlqd099iChnyIMvY5HexjO2AmtsbpVn0OhNcWbWDRF/3sBp6fWXhz7Dc # ML4iTAWS+MVXeNLj1lJziVKEoroGs9Mlizg0bUMbOalOhOfCipnx8CaLZeVme5yE # Lg09Jlo8BMe80jO37PU8ejfkP9/uPak7VLwELKxAMcJszkyeiaerlphwoKx1uHRz # NyE6bxuSKcutisqmKL5OTunAvtONEoteSiabkPVSZ2z76mKnzAfZxCl/3dq3dUNw # 4rg3sTCggkHSRqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5JKdGvspbOrTfOXyXvmPL6 # E52z1NZJ6ctuMFBQZH3pwWvqURR8AgQdULUvrxjUYbHHj95Ejza63zdrEcxWLDX6 # xWls/GDnVNueKjWUH3fTv1Y8Wdho698YADR7TNx8X8z2Bev6SivBBOHY+uqiirZt # g0y9ShQoPzmCcn63Syatatvx157YK9hlcPmVoa1oDE5/L9Uo2bC5a4CH2RwwggY+ # MIIEpqADAgECAhAHnODk0RR/hc05c892LTfrMA0GCSqGSIb3DQEBDAUAMFQxCzAJ # BgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNl # Y3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwHhcNMjYwMjA5MDAwMDAw # WhcNMjkwNDIxMjM1OTU5WjBVMQswCQYDVQQGEwJVUzEUMBIGA1UECAwLQ29ubmVj # dGljdXQxFzAVBgNVBAoMDkphc29uIEFsYmVyaW5vMRcwFQYDVQQDDA5KYXNvbiBB # bGJlcmlubzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPN6aN4B1yYW # kI5b5TBj3I0VV/peETrHb6EY4BHGxt8Ap+eT+WpEpJyEtRYPxEmNJL3A38Bkg7mw # zPE3/1NK570ZBCuBjSAn4mSDIgIuXZnvyBO9W1OQs5d67MlJLUAEufl18tOr3ST1 # DeO9gSjQSAE5Nql0QDxPnm93OZBon+Fz3CmE+z3MwAe2h4KdtRAnCqwM+/V7iBdb # w+JOxolpx+7RVjGyProTENIG3pe/hKvPb501lf8uBAADLdjZr5ip8vIWbf857Yw1 # Bu10nVI7HW3eE8Cl5//d1ribHlzTzQLfttW+k+DaFsKZBBL56l4YAlIVRsrOiE1k # dHYYx6IGrEA809R7+TZA9DzGqyFiv9qmJAbL4fDwetDeyIq+Oztz1LvEdy8Rcd0J # BY+J4S0eDEFIA3X0N8VcLeAwabKb9AjulKXwUeqCJLvN79CJ90UTZb2+I+tamj0d # n+IKMEsJ4v4Ggx72sxFr9+6XziodtTg5Luf2xd6+PhhamOxF2px9LObhBLLEMyRs # CHZIzVZOFKu9BpHQH7ufGB+Sa80Tli0/6LEyn9+bMYWi2ttn6lLOPThXMiQaooRU # q6q2u3+F4SaPlxVFLI7OJVMhar6nW6joBvELTJPmANSMjDSRFDfHRCdGbZsL/keE # LJNy+jZctF6VvxQEjFM8/bazu6qYhrA7AgMBAAGjggGJMIIBhTAfBgNVHSMEGDAW # gBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU6YF0o0D5AVhKHbVocr8G # aSIBibAwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYI # KwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcC # ARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAw # PqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVT # aWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0 # cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIz # Ni5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqG # SIb3DQEBDAUAA4IBgQAEIsm4xnOd/tZMVrKwi3doAXvCwOA/RYQnFJD7R/bSQRu3 # wXEK4o9SIefye18B/q4fhBkhNAJuEvTQAGfqbbpxow03J5PrDTp1WPCWbXKX8Oz9 # vGWJFyJxRGftkdzZ57JE00synEMS8XCwLO9P32MyR9Z9URrpiLPJ9rQjfHMb1BUd # vaNayomm7aWLAnD+X7jm6o8sNT5An1cwEAob7obWDM6sX93wphwJNBJAstH9Ozs6 # LwISOX6sKS7CKm9N3Kp8hOUue0ZHAtZdFl6o5u12wy+zzieGEI50fKnN77FfNKFO # WKlS6OJwlArcbFegB5K89LcE5iNSmaM3VMB2ADV1FEcjGSHw4lTg1Wx+WMAMdl/7 # nbvfFxJ9uu5tNiT54B0s+lZO/HztwXYQUczdsFon3pjsNrsk9ZlalBi5SHkIu+F6 # g7tWiEv3rtVApmJRnLkUr2Xq2a4nbslUCt4jKs5UX4V1nSX8OM++AXoyVGO+iTj7 # z+pl6XE9Gw/Td6WKKKswgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0G # CSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTla # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEy # NTYgMjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHT # CphBcr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPh # of6pvF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mA # xAHeHYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBv # MgEdgkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps # 0wjUjsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF # 83bRVFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXi # UOeSLsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOM # CZIVNSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydP # pOjL6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrU # G2ZdSoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+ # sdFUeEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0T # AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYD # VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG # A1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV # HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU # cnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s # BwEwDQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WI # GjB/T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+ # IQhQE7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8M # yb9rEVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2 # th9y1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjaj # V/gxdEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2 # Lr3ty9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFze # GxcytL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG # 7uEBYTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+N # Jpud/v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckT # etiSuEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszW # kPZPubdcMIIG7TCCBNWgAwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0B # AQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/ # BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg # U0hBMjU2IDIwMjUgQ0ExMB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVow # YzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQD # EzJEaWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIw # MjUgMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5g # VrMrV7pvUf+GcAoB38o3zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN # +vo8dE2/pPvOx/Vj8TchTySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qo # me7MrxVyfQO9sMx6ZAWjFDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ/ # /nBZZREr4h/GI6Dxb2UoyrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouT # MYFOnHoRh6+86Ltc5zjPKHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8 # DD+nigNJFmt6LAHvH3KSuNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnp # JeItK/DhKbPxTTuGoX7wJNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP5 # 1ho1zBp+xUIZkpSFA8vWdoUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49 # kPmk8iyyizNDIXj//cOgrY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5P # WPsWeupWs7NpChUk555K096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7Y # ufAkprxMiXAJQ1XCmnCfgPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAA # MB0GA1UdDgQWBBTkO/zyMe39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK # 6eQGfHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYI # KwYBBQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8v # b2NzcC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZT # SEEyNTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5k # aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2 # U0hBMjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9 # bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP # 2zWLpQq1b4URGnwWBdEZD9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O # 6Lgjg8K8elC4+oWCqnU/ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskg # iC3QYIUP2S3HQvHG1FDu+WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMU # BaB5bdrPbF6MRYs03h4obEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDF # kxUGtMTaiLR9wjxUxu2hECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+ # zJNEsuEB7O7/cuvTQasnM9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/l # wd6UArb+BOVAkg2oOvol/DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxl # RcGG0LIhp6GvReQGgMgYxQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2 # zRWVFjF7mcr4C34Mj3ocCVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJg # baP5t2nGj/ULLi49xTcBZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOC # IUjsarfNZzGCBkQwggZAAgEBMGgwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1Nl # Y3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWdu # aW5nIENBIFIzNgIQB5zg5NEUf4XNOXPPdi036zANBglghkgBZQMEAgEFAKCBhDAY # BgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3 # AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEi # BCDt6suBTiabCBepsfhnGTbNBpAliE0Zt8pROWuHuwYWKDANBgkqhkiG9w0BAQEF # AASCAgBMf3zICmVTwpJVISfSMw9qA3DtacySOcl6Wxd0dybqnSdW58BKpG6rze6N # xt5WshefSI+8amka9r+tfpAhdN+CTZkStA5w46ZXA6ytO2iLdl+Ty9BuQ90dLkax # n1j2cZlKGoEH61pzvOz9lSRdfS+D9p4Muk5T+ldDQt4K7VnC5I3toXuylisBOAuV # P6RsKVOQdE7Yhu7Pcsw5C5E48sEhtO3aeeG0uKP1TXCfBMi9wCBX0WreRnjhazrv # +3+PtENHiR/gVWLAHTLB/LxYcBue3TRGGLvpy4OWs/RwAE5D0mYS0tJVUhQgmaNL # Qd43Zi5oZ8/zCgaM/KpFF9zm1yuzRpDrWlHputHXQRkbSRMoTZjUQgrrb6VyQzEl # AYfORyhhzvw9yc1jPOK3xGtl5deXNT1ewu5UGIaChyFK4m3exG6zz1k7RDiq2GaI # LA2cVYxYyKtxkbj5fAa/tBZ3ESp0femZQR/gRaCyxDgSbFX+cLA2Bt2ZedowPVzw # eydtEGEhuctLt5cYQLLzFYJjOn0PRlg0jXrNnGnDqzQWTTei3yCqQtw2q0VMEzDB # 4/VnhfKTEQV7o8TKIBTflrUHIQC5qBK/Ac3xHFZgxoA9TeN3txCJeQ/qvD8uohiZ # KgOPPEtm93IWay/GcFz6I+zYVBIhJpayVN0W41igCSGAMAAv+qGCAyYwggMiBgkq # hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E # aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l # U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeV # dGgwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG # CSqGSIb3DQEJBTEPFw0yNjA0MjAwMDE2NDFaMC8GCSqGSIb3DQEJBDEiBCDC5I3J # kL95frjlaCZEGky7oZC1NuLns4HiqTlGdryb4TANBgkqhkiG9w0BAQEFAASCAgCw # BJ76yUv6YIxx0EqPloOPoE6pPdZubLsIyoocRFgR8l9s5yXPeN3xGnlsRdVyFIZj # MIShMpTt5RrYXpe1eZJXrJb9g3GsBknJ9W1GqwujD9gZOCn04JMbLkWeoVNvLkoU # oCcRdQOonrIh0N2kylpMLVfnwHaa55IUmNG5lbZCcsVuNwLFB9vh57Fdn/wICZfe # GlXlXKhpmLCjXUA3hPCJC3SM11RdzFQIvBdM98Rs/X6zwLv9ZUkFwUdcNUBtB1xK # l0EaDCow/X1MvjxMQ5CD1c/EOwaZbYBvsl8bgl/YjRpUfa+A80hpoBaz0eCuDiOJ # 32OchgALAOM0rytKBHTsNYtzQiHXrkbvGfSGlaQ4V9KWNXobkL9o1G1vGKnoz1lc # zUgQtaJIZ/g8xDybw49ueTS3b0DToRTF5vV/QYVxNJrDgsbE57HhE5esLhk77Sv1 # FKC6MpXS/FXnEY9oOWdUf15f/vB4gKLLAdVJ4K7QM8rgc8a21sNOxZsTeTWurCb/ # eGszElXSGfEitSHi5KtUwmc8toZsVaT/cSTeDyMcd36cQHq5qosx5Fm0yFRjbaq7 # j2rnIcmW/zOhuea6n65pMf5Zn1c+gryeeHw7iWm6kJ0Gk1o3GUB5emU0wl1F/TS0 # CZzAHHywpRzkVO05ODLymlpSbNhOceVrwotx0t1O6A== # SIG # End signature block |