helpers/nutanix/NutanixHelpers.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-NutanixCluster {
    <#
    .SYNOPSIS
        Builds an authorization header for the Nutanix Prism REST API.
    .DESCRIPTION
        Takes a server URI and credential, returns a hashtable of headers
        used by all subsequent Nutanix API calls (Basic auth).
    .PARAMETER Server
        The base URI of Prism Element or Prism Central (e.g. https://192.168.1.50:9440).
    .PARAMETER Credential
        PSCredential for Prism authentication.
    .EXAMPLE
        $cred = Get-Credential
        $headers = Connect-NutanixCluster -Server "https://192.168.1.50:9440" -Credential $cred
        Authenticates to the Nutanix Prism API and returns authorization headers.
    .EXAMPLE
        $headers = Connect-NutanixCluster -Server "https://prism.lab.local:9440" -Credential (Get-Credential)
        Connects to Prism Central and stores the auth headers for subsequent API calls.
    #>

    param(
        [Parameter(Mandatory)][string]$Server,
        [Parameter(Mandatory)][PSCredential]$Credential
    )

    $pair = "$($Credential.UserName):$($Credential.GetNetworkCredential().Password)"
    $bytes = [System.Text.Encoding]::ASCII.GetBytes($pair)
    $base64 = [System.Convert]::ToBase64String($bytes)

    $headers = @{
        Authorization  = "Basic $base64"
        "Content-Type" = "application/json"
        Accept         = "application/json"
    }

    # Validate connectivity
    try {
        Invoke-RestMethod -Uri "$Server/api/nutanix/v2.0/cluster" -Headers $headers -Method Get | Out-Null
        Write-Verbose "Connected to Nutanix cluster at $Server"
    }
    catch {
        throw "Failed to connect to Nutanix at $Server : $($_.Exception.Message)"
    }

    return $headers
}

function Get-NutanixCluster {
    <#
    .SYNOPSIS
        Returns cluster-level information from Prism.
    .PARAMETER Server
        The Prism base URI.
    .PARAMETER Headers
        Auth headers from Connect-NutanixCluster.
    .EXAMPLE
        $headers = Connect-NutanixCluster -Server "https://192.168.1.50:9440" -Credential $cred
        Get-NutanixCluster -Server "https://192.168.1.50:9440" -Headers $headers
        Returns cluster name, version, node count, and other cluster-level details.
    #>

    param(
        [Parameter(Mandatory)][string]$Server,
        [Parameter(Mandatory)][hashtable]$Headers
    )

    $cluster = Invoke-RestMethod -Uri "$Server/api/nutanix/v2.0/cluster" -Headers $Headers -Method Get

    [PSCustomObject]@{
        ClusterName      = "$($cluster.name)"
        ClusterUuid      = "$($cluster.uuid)"
        ClusterVersion   = "$($cluster.version)"
        HypervisorTypes  = ($cluster.hypervisor_types -join ", ")
        NumNodes         = "$($cluster.num_nodes)"
        ClusterExternalIP = if ($cluster.cluster_external_ipaddress) { "$($cluster.cluster_external_ipaddress)" } else { "N/A" }
        Timezone         = "$($cluster.timezone)"
        SupportVerbosity = "$($cluster.support_verbosity_type)"
    }
}

function Get-NutanixHosts {
    <#
    .SYNOPSIS
        Returns all hosts in the Nutanix cluster.
    .PARAMETER Server
        The Prism base URI.
    .PARAMETER Headers
        Auth headers from Connect-NutanixCluster.
    .EXAMPLE
        $hosts = Get-NutanixHosts -Server "https://192.168.1.50:9440" -Headers $headers
        Returns all hypervisor hosts in the Nutanix cluster.
    .EXAMPLE
        Get-NutanixHosts -Server $server -Headers $headers | ForEach-Object { $_.name }
        Lists the names of all hosts in the cluster.
    #>

    param(
        [Parameter(Mandatory)][string]$Server,
        [Parameter(Mandatory)][hashtable]$Headers
    )

    (Invoke-RestMethod -Uri "$Server/api/nutanix/v2.0/hosts" -Headers $Headers -Method Get).entities
}

function Get-NutanixVMs {
    <#
    .SYNOPSIS
        Returns all VMs in the Nutanix cluster.
    .PARAMETER Server
        The Prism base URI.
    .PARAMETER Headers
        Auth headers from Connect-NutanixCluster.
    .EXAMPLE
        $vms = Get-NutanixVMs -Server "https://192.168.1.50:9440" -Headers $headers
        Returns all VMs in the Nutanix cluster including NIC and disk configurations.
    .EXAMPLE
        Get-NutanixVMs -Server $server -Headers $headers | Where-Object { $_.power_state -eq "on" }
        Returns only powered-on VMs.
    #>

    param(
        [Parameter(Mandatory)][string]$Server,
        [Parameter(Mandatory)][hashtable]$Headers
    )

    (Invoke-RestMethod -Uri "$Server/api/nutanix/v2.0/vms/?include_vm_nic_config=true&include_vm_disk_config=true" -Headers $Headers -Method Get).entities
}

function Get-NutanixHostDetail {
    <#
    .SYNOPSIS
        Formats detailed information about a Nutanix AHV/ESXi host.
    .PARAMETER HostEntity
        A host object from Get-NutanixHosts.
    .EXAMPLE
        $hosts = Get-NutanixHosts -Server $server -Headers $headers
        Get-NutanixHostDetail -HostEntity $hosts[0]
        Returns CPU, memory, storage, and network details for the first host.
    .EXAMPLE
        Get-NutanixHosts -Server $server -Headers $headers | ForEach-Object { Get-NutanixHostDetail -HostEntity $_ }
        Returns detailed information for every host in the cluster.
    #>

    param(
        [Parameter(Mandatory)]$HostEntity
    )

    $h = $HostEntity

    # Controller VM IP (CVM)
    $cvmIP = if ($h.controller_vm_backplane_ip) { "$($h.controller_vm_backplane_ip)" } else { "N/A" }
    # Hypervisor IP
    $hypervisorIP = if ($h.hypervisor_address) { "$($h.hypervisor_address)" } else { "N/A" }
    # Management IP (IPMI)
    $ipmiIP = if ($h.ipmi_address) { "$($h.ipmi_address)" } else { "N/A" }

    # Stats
    $stats = $h.stats
    $cpuPct = if ($stats.'hypervisor_cpu_usage_ppm') { "{0:N1}%" -f ($stats.'hypervisor_cpu_usage_ppm' / 10000) } else { "N/A" }
    $memPct = if ($stats.'hypervisor_memory_usage_ppm') { "{0:N1}%" -f ($stats.'hypervisor_memory_usage_ppm' / 10000) } else { "N/A" }

    [PSCustomObject]@{
        Type             = "Nutanix Host"
        HostName         = if ($h.name) { "$($h.name)" } else { "N/A" }
        HostUuid         = "$($h.uuid)"
        HypervisorIP     = $hypervisorIP
        CvmIP            = $cvmIP
        IpmiIP           = $ipmiIP
        IPAddress        = $hypervisorIP
        HypervisorType   = if ($h.hypervisor_type) { "$($h.hypervisor_type)" } else { "N/A" }
        HypervisorVersion = if ($h.hypervisor_full_name) { "$($h.hypervisor_full_name)" } else { "N/A" }
        AcropolisVersion = if ($h.service_vmexternal_ip) { "$($h.service_vmexternal_ip)" } else { "N/A" }
        Serial           = if ($h.serial) { "$($h.serial)" } else { "N/A" }
        BlockModel       = if ($h.block_model_name) { "$($h.block_model_name)" } else { "N/A" }
        BlockSerial      = if ($h.block_serial) { "$($h.block_serial)" } else { "N/A" }
        CPUSockets       = "$($h.num_cpu_sockets)"
        CPUCores         = "$($h.num_cpu_cores)"
        CPUThreads       = "$($h.num_cpu_threads)"
        CPUModel         = if ($h.cpu_model) { "$($h.cpu_model)" } else { "N/A" }
        CPUFreqGHz       = if ($h.cpu_frequency_in_hz) { "{0:N2}" -f ($h.cpu_frequency_in_hz / 1000000000) } else { "N/A" }
        CPUUsagePct      = $cpuPct
        RAM_TotalGB      = "$([math]::Round($h.memory_capacity_in_bytes / 1GB, 2))"
        MemUsagePct      = $memPct
        NumVMs           = "$($h.num_vms)"
        NumDisks         = "$($h.num_disks)"
        StorageCapacityGB = if ($h.usage_stats.'storage.capacity_bytes') { "$([math]::Round([long]$h.usage_stats.'storage.capacity_bytes' / 1GB, 2))" } else { "N/A" }
        StorageUsedGB    = if ($h.usage_stats.'storage.usage_bytes') { "$([math]::Round([long]$h.usage_stats.'storage.usage_bytes' / 1GB, 2))" } else { "N/A" }
        BootTimeUsecs    = if ($h.boot_time_in_usecs) { "$($h.boot_time_in_usecs)" } else { "N/A" }
    }
}

function Get-NutanixVMDetail {
    <#
    .SYNOPSIS
        Formats detailed information about a Nutanix VM.
    .PARAMETER VMEntity
        A VM object from Get-NutanixVMs.
    .PARAMETER HostLookup
        A hashtable mapping host UUIDs to host names for display.
    .EXAMPLE
        $vms = Get-NutanixVMs -Server $server -Headers $headers
        Get-NutanixVMDetail -VMEntity $vms[0]
        Returns detailed information (IP, CPU, memory, disks) for the first VM.
    .EXAMPLE
        $hosts = Get-NutanixHosts -Server $server -Headers $headers
        $lookup = @{}; $hosts | ForEach-Object { $lookup[$_.uuid] = $_.name }
        $vms | ForEach-Object { Get-NutanixVMDetail -VMEntity $_ -HostLookup $lookup }
        Returns VM details with host names resolved via lookup table.
    #>

    param(
        [Parameter(Mandatory)]$VMEntity,
        [hashtable]$HostLookup = @{}
    )

    $vm = $VMEntity

    # IP addresses from NIC list
    $ipAddresses = @()
    if ($vm.vm_nics) {
        foreach ($nic in $vm.vm_nics) {
            if ($nic.ip_address) { $ipAddresses += $nic.ip_address }
            if ($nic.requested_ip_address) { $ipAddresses += $nic.requested_ip_address }
        }
    }
    $ip = $ipAddresses |
        Where-Object { $_ -match '^\d{1,3}(\.\d{1,3}){3}$' -and $_ -notlike '169.254.*' } |
        Select-Object -First 1
    if (-not $ip) { $ip = "N/A" }

    # Disk info
    $diskCount = 0
    $totalDiskGB = 0
    if ($vm.vm_disk_info) {
        foreach ($disk in $vm.vm_disk_info) {
            if ($disk.disk_address.device_bus -ne "ide" -or !$disk.is_cdrom) {
                $diskCount++
                if ($disk.size) { $totalDiskGB += $disk.size / 1GB }
            }
        }
    }

    # NIC info
    $nicCount = if ($vm.vm_nics) { @($vm.vm_nics).Count } else { 0 }
    $vlanIds = ($vm.vm_nics | ForEach-Object { $_.vlan_id } | Where-Object { $_ } | Select-Object -Unique) -join ", "
    $networkNames = ($vm.vm_nics | ForEach-Object { $_.network_name } | Where-Object { $_ } | Select-Object -Unique) -join ", "

    # Host mapping
    $hostName = if ($HostLookup.ContainsKey($vm.host_uuid)) { $HostLookup[$vm.host_uuid] } else { "$($vm.host_uuid)" }

    # Stats
    $stats = $vm.stats
    $cpuPct = if ($stats.'hypervisor.cpu_ready_time_ppm') { "{0:N1}%" -f ($stats.'hypervisor.cpu_ready_time_ppm' / 10000) } else { "N/A" }

    [PSCustomObject]@{
        Name             = if ($vm.name) { "$($vm.name)" } else { "N/A" }
        VMId             = "$($vm.uuid)"
        Host             = "$hostName"
        HostUuid         = "$($vm.host_uuid)"
        IPAddress        = $ip
        PowerState       = if ($vm.power_state) { "$($vm.power_state)" } else { "N/A" }
        NumCPU           = "$($vm.num_vcpus)"
        NumCoresPerVcpu  = "$($vm.num_cores_per_vcpu)"
        MemoryGB         = "$([math]::Round($vm.memory_mb / 1024, 2))"
        DiskCount        = "$diskCount"
        DiskTotalGB      = "$([math]::Round($totalDiskGB, 2))"
        NicCount         = "$nicCount"
        VLanIds          = if ($vlanIds) { $vlanIds } else { "N/A" }
        NetworkNames     = if ($networkNames) { $networkNames } else { "N/A" }
        GuestOS          = if ($vm.guest_os) { "$($vm.guest_os)" } else { "N/A" }
        Description      = if ($vm.description) { "$($vm.description.Substring(0, [math]::Min(200, $vm.description.Length)))" } else { "" }
        ProtectionDomain = if ($vm.protection_domain_name) { "$($vm.protection_domain_name)" } else { "N/A" }
        Timezone         = if ($vm.timezone) { "$($vm.timezone)" } else { "N/A" }
        NgtEnabled       = if ($null -ne $vm.vm_features.AGENT_VM) { "$($vm.vm_features.AGENT_VM)" } else { "N/A" }
        MachineType      = if ($vm.machine_type) { "$($vm.machine_type)" } else { "N/A" }
    }
}

function Get-NutanixDashboard {
    <#
    .SYNOPSIS
        Builds a flat dashboard view combining Nutanix cluster, hosts, and VMs.
    .DESCRIPTION
        Queries the Nutanix Prism API for cluster info, hosts, and VMs,
        then returns a unified collection of objects suitable for an
        interactive Bootstrap Table dashboard. Each row represents a VM
        enriched with host CPU/memory usage and cluster context.
    .PARAMETER Server
        The Prism base URI (e.g. https://192.168.1.50:9440).
    .PARAMETER Headers
        Auth headers hashtable obtained from Connect-NutanixCluster.
    .EXAMPLE
        $headers = Connect-NutanixCluster -Server "https://192.168.1.50:9440" -Credential $cred
        Get-NutanixDashboard -Server "https://192.168.1.50:9440" -Headers $headers
 
        Returns a flat dashboard view of all VMs across the specified cluster.
    .EXAMPLE
        $dashboard = Get-NutanixDashboard -Server $server -Headers $headers
        $dashboard | Where-Object { $_.PowerState -eq "on" }
 
        Retrieves the dashboard and filters for powered-on VMs.
    .EXAMPLE
        $cred = Get-Credential
        $headers = Connect-NutanixCluster -Server "https://prism01:9440" -Credential $cred
        $data = Get-NutanixDashboard -Server "https://prism01:9440" -Headers $headers
        Export-NutanixDashboardHtml -DashboardData $data -OutputPath "C:\Reports\nutanix.html"
        Start-Process "C:\Reports\nutanix.html"
 
        End-to-end: authenticate, gather data, export HTML, and open in browser.
    .OUTPUTS
        PSCustomObject[]
        Each object contains VM details enriched with host and cluster context: VMName,
        PowerState, IPAddress, Host, HostIP, HostCPUUsage, HostMemUsage, ClusterName,
        ClusterVersion, NumCPU, CoresPerVcpu, MemoryGB, DiskCount, DiskTotalGB, NicCount,
        VLanIds, NetworkNames, GuestOS, ProtectionDomain, NgtEnabled, MachineType, Description.
    .NOTES
        Author : jason@wug.ninja
        Version : 1.0.0
        Date : 2025-07-15
        Requires: PowerShell 5.1+, network access to Nutanix Prism API (port 9440).
    .LINK
        https://github.com/jayyx2/WhatsUpGoldPS
    #>

    param(
        [Parameter(Mandatory)][string]$Server,
        [Parameter(Mandatory)][hashtable]$Headers
    )

    $cluster = Get-NutanixCluster -Server $Server -Headers $Headers
    $hosts = Get-NutanixHosts -Server $Server -Headers $Headers
    $vms = Get-NutanixVMs -Server $Server -Headers $Headers

    # Build host lookup
    $hostLookup = @{}
    $hostDetails = @{}
    foreach ($h in $hosts) {
        $hostLookup[$h.uuid] = $h.name
        $hostDetails[$h.uuid] = Get-NutanixHostDetail -HostEntity $h
    }

    $results = @()
    foreach ($vm in $vms) {
        $vmDetail = Get-NutanixVMDetail -VMEntity $vm -HostLookup $hostLookup
        $hd = if ($hostDetails.ContainsKey($vm.host_uuid)) { $hostDetails[$vm.host_uuid] } else { $null }

        $results += [PSCustomObject]@{
            VMName           = $vmDetail.Name
            PowerState       = $vmDetail.PowerState
            IPAddress        = $vmDetail.IPAddress
            Host             = $vmDetail.Host
            HostIP           = if ($hd) { $hd.HypervisorIP } else { "N/A" }
            HostCPUUsage     = if ($hd) { $hd.CPUUsagePct } else { "N/A" }
            HostMemUsage     = if ($hd) { $hd.MemUsagePct } else { "N/A" }
            ClusterName      = $cluster.ClusterName
            ClusterVersion   = $cluster.ClusterVersion
            NumCPU           = $vmDetail.NumCPU
            CoresPerVcpu     = $vmDetail.NumCoresPerVcpu
            MemoryGB         = $vmDetail.MemoryGB
            DiskCount        = $vmDetail.DiskCount
            DiskTotalGB      = $vmDetail.DiskTotalGB
            NicCount         = $vmDetail.NicCount
            VLanIds          = $vmDetail.VLanIds
            NetworkNames     = $vmDetail.NetworkNames
            GuestOS          = $vmDetail.GuestOS
            ProtectionDomain = $vmDetail.ProtectionDomain
            NgtEnabled       = $vmDetail.NgtEnabled
            MachineType      = $vmDetail.MachineType
            Description      = $vmDetail.Description
        }
    }

    return $results
}

function Export-NutanixDashboardHtml {
    <#
    .SYNOPSIS
        Renders Nutanix dashboard data into a self-contained HTML file.
    .DESCRIPTION
        Takes the output of Get-NutanixDashboard 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-NutanixDashboard containing VM and cluster details.
    .PARAMETER OutputPath
        File path for the output HTML file. Parent directory must exist.
    .PARAMETER ReportTitle
        Title shown in the report header. Defaults to "Nutanix Dashboard".
    .PARAMETER TemplatePath
        Optional path to a custom HTML template. If omitted, uses the
        Nutanix-Dashboard-Template.html in the same directory as this script.
    .EXAMPLE
        $data = Get-NutanixDashboard -Server $server -Headers $headers
        Export-NutanixDashboardHtml -DashboardData $data -OutputPath "C:\Reports\nutanix.html"
 
        Exports the dashboard data to an HTML file using the default template.
    .EXAMPLE
        Export-NutanixDashboardHtml -DashboardData $data -OutputPath "$env:TEMP\nutanix.html" -ReportTitle "Production Nutanix"
 
        Exports with a custom report title.
    .EXAMPLE
        $headers = Connect-NutanixCluster -Server $server -Credential $cred
        $data = Get-NutanixDashboard -Server $server -Headers $headers
        Export-NutanixDashboardHtml -DashboardData $data -OutputPath "C:\Reports\nutanix.html"
        Start-Process "C:\Reports\nutanix.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+, Nutanix-Dashboard-Template.html in the script directory.
    .LINK
        https://github.com/jayyx2/WhatsUpGoldPS
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]$DashboardData,
        [Parameter(Mandatory)][string]$OutputPath,
        [string]$ReportTitle = "Nutanix Dashboard",
        [string]$TemplatePath
    )

    if (-not $TemplatePath) {
        $TemplatePath = Join-Path $PSScriptRoot "Nutanix-Dashboard-Template.html"
    }

    if (-not (Test-Path $TemplatePath)) {
        throw "HTML template not found at $TemplatePath"
    }

    $firstObj = $DashboardData | Select-Object -First 1
    $columns = @()
    foreach ($prop in $firstObj.PSObject.Properties) {
        $col = @{
            field      = $prop.Name
            title      = ($prop.Name -creplace '([A-Z])', ' $1').Trim()
            sortable   = $true
            searchable = $true
        }
        if ($prop.Name -eq 'PowerState') {
            $col.formatter = 'formatPowerState'
        }
        $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 "Nutanix Dashboard HTML written to $OutputPath"
}

# SIG # Begin signature block
# MIIVlwYJKoZIhvcNAQcCoIIViDCCFYQCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB/UBvI/WLWx7Ha
# UytLAO8oCPfM3+mJqWD3XDB2xXOSTaCCEdMwggVvMIIEV6ADAgECAhBI/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
# BAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgY16Idi77vZXtWONoajuF4x3bA8TnTJo0
# RciBR5U829AwDQYJKoZIhvcNAQEBBQAEggIA2hD0A2TRFi/gCpjsD60z+vcrWP/k
# Iwdv1leshDQl5g/Wy1Du0kuLr95w0bm9DbsHwDK8MLhqG5cVKQ5X2Y6eYh1VBL69
# paV4cTABo+oLs/aM/Xi5yThxQvAfn9BVnR1/tlzqtsc3ivjcfkwESDhooclRiYi6
# C53vuEOVvUC6eglEwW4mRCscYfHKAwttIKO3M3utthDs4IedFlkDQ3Ea3WaKTGbx
# CoQCaOeebEYt0HhjuuEaz5tfMttvN7LId5uyUwXa2vDIsj1H9TjrpDtY6FVfAmXH
# EwpYk+BmUTqd8VuvbdLVEcxeTMjYHqicwxslQa1ZrHf9gBwWNZwccRNPmL2sHJv9
# LG1/I87OWlLeGnO276rYTV1PqfWAxOAfo7ioLyiKFjJXGF2ye+okco7yEvpr1TIl
# wRMZoOWY7dbpxAzrT0/Uq3jHTtaMgcw9HMQLc+L74s6Sttac5uPFfnhnniZppQhK
# IM7p84AoRWNbfg9S1LhtN+KhJEgtZANMXPJcxnZX0fko37yTuT+uP/9kQzFPoCkK
# 7eDlDzaaLiK/Gy48m+UDgLOHOfZtgZw1G1E5zSCEuQ5tRIahQCjNrBcH7WBDLfHz
# T8NELmuD1/BpFOqvv1V1I7rTyJlIOXV3B94N3ZpgzSbtSFN0qlh/s6lWoH1sh7eg
# YFvuB9KXv2CxwYI=
# SIG # End signature block