Obs/bin/ObsDep/content/Powershell/Roles/Common/PhysicalMachineHelpers.psm1

# --------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All Rights Reserved.
# Microsoft Corporation (or based on where you live, one of its affiliates) licenses this sample code for your internal testing purposes only.
# Microsoft provides the following sample code AS IS without warranty of any kind. The sample code arenot supported under any Microsoft standard support program or services.
# Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose.
# The entire risk arising out of the use or performance of the sample code remains with you.
# In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the code be liable for any damages whatsoever
# (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss)
# arising out of the use of or inability to use the sample code, even if Microsoft has been advised of the possibility of such damages.
# ---------------------------------------------------------------

Import-Module -Name "$PSScriptRoot\..\Common\RoleHelpers.psm1"
Import-Module -Name "$PSScriptRoot\..\..\Common\NetworkHelpers.psm1"
Import-Module -Name "$PSScriptRoot\..\..\Common\StorageHelpers.psm1"
Import-Module -Name "$PSScriptRoot\..\..\Common\ClusterHelpers.psm1"

<#
.Synopsis
     Function to shut down all provisioning machines from any role context
.Parameter OOBManagementModulePath
     A relative path to the out-of-band management dll -- during deployment it is available locally but in ECE service it is located in an unpacked package.
.Parameter Parameters
    This object is based on the customer manifest. It contains the private information of the Key Vault role, as well as
    public information of all other roles. It is passed down by the deployment engine.
.Example
    Stop-DeployingMachines -Parameters $Parameters
 
    This will shut down all hosts that are in provisioning state
#>

function Stop-DeployingMachines {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)]
        [string]
        $OOBManagementModulePath = "$PSScriptRoot\..\..\OOBManagement\bin\Microsoft.AzureStack.OOBManagement.dll",

        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )
    # After deployment, the source of this module changes
    Import-Module -Name $OOBManagementModulePath
    Import-Module -Name "$PSScriptRoot\..\..\OOBManagement\bin\Newtonsoft.Json.dll"

    # Shut down all machines to avoid IP conflicts
    $physicalMachinesRole = $Parameters.Roles["BareMetal"].PublicConfiguration
    $bareMetalCredential = Get-BareMetalCredential -Parameters $Parameters

    $OEMRole = $Parameters.Roles["OEM"].PublicConfiguration
    $OEMModel = $OEMRole.PublicInfo.UpdatePackageManifest.UpdateInfo.Model

    if ([String]::IsNullOrEmpty($Parameters.Context.ExecutionContext.Roles.Role.Nodes.Node.Name))
    {
        Trace-Execution "Target to all physical nodes since node list not set in execution context"
        [Array]$provisioningNodes = $physicalMachinesRole.Nodes.Node
    }
    else
    {
        Trace-Execution "Get node list from execution context"

        # Determine whether the context of the operation is a cluster scale out
        if ($Parameters.Context.ExecutionContext.Roles.Role.RoleName -ieq "Cluster")
        {
            $nodes = $Parameters.Context.ExecutionContext.Roles.Role.Nodes.Node.PhysicalNodes.PhysicalNode.Name
        }
        else
        {
            $nodes = $Parameters.Context.ExecutionContext.Roles.Role.Nodes.Node.Name
        }

        [Array]$provisioningNodes = $physicalMachinesRole.Nodes.Node | Where-Object { $nodes -contains $_.Name }
    }

    # If any machines are not accessible during this call, the caller may fail. It is important to mark all provisioning nodes as failed when this happens
    foreach ($node in $provisioningNodes)
    {
        $bmcIP = $node.BmcIPAddress
        $nodeName = $node.Name
        $nodeInstance = $node.NodeInstance
        $oobProtocol = $node.OOBProtocol

        if ($OEMModel -match "Hyper-V")
        {
            $trustedHosts = (Get-Item -Path WSMan:\localhost\Client\TrustedHosts).Value
            if (($trustedHosts -ne '*') -and ($bmcIP -notin $trustedHosts.Split(',')))
            {
                Set-Item -Path WSMan:\localhost\Client\TrustedHosts -Value $bmcIP -Concatenate -Force
            }
            Invoke-Command -ComputerName $bmcIP -Credential $bareMetalCredential -ScriptBlock {Stop-VM -VMName $using:nodeName -TurnOff -Force -ErrorAction Stop } -ErrorAction Stop
            Set-Item -Path WSMan:\localhost\Client\TrustedHosts -Value $trustedHosts -Force
        }
        else
        {
            Trace-Execution "Shut down $nodeName ($bmcIP)."
            Stop-IpmiDevice -TargetAddress $bmcIP -Credential $bareMetalCredential -NodeInstance $nodeInstance -OOBProtocol $oobProtocol -Verbose -Wait
        }
    }
}

<#
.Synopsis
     Function to start-up all provisioning machines from any role context
.Parameter OOBManagementModulePath
     A relative path to the out-of-band management dll -- during deployment it is available locally but in ECE service it is located in an unpacked package.
.Parameter Parameters
    This object is based on the customer manifest. It contains the private information of the Key Vault role, as well as
    public information of all other roles. It is passed down by the deployment engine.
.Example
    Start-DeployingMachines -Parameters $Parameters
 
    This will startup all hosts that are in provisioning state
#>

function Start-DeployingMachines {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)]
        [string]
        $OOBManagementModulePath = "$PSScriptRoot\..\..\OOBManagement\bin\Microsoft.AzureStack.OOBManagement.dll",

        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )
    # After deployment, the source of this module changes
    Import-Module -Name $OOBManagementModulePath
    Import-Module -Name "$PSScriptRoot\..\..\OOBManagement\bin\Newtonsoft.Json.dll"

    $physicalMachinesRole = $Parameters.Roles["BareMetal"].PublicConfiguration
    $bareMetalCredential = Get-BareMetalCredential -Parameters $Parameters

    $OEMRole = $Parameters.Roles["OEM"].PublicConfiguration
    $OEMModel = $OEMRole.PublicInfo.UpdatePackageManifest.UpdateInfo.Model

    if ([String]::IsNullOrEmpty($Parameters.Context.ExecutionContext.Roles.Role.Nodes.Node.Name))
    {
        Trace-Execution "Target to all physical nodes since node list not set in execution context"
        [Array]$provisioningNodes = $physicalMachinesRole.Nodes.Node
    }
    else
    {
        Trace-Execution "Get node list from execution context"

        # Determine whether the context of the operation is a cluster scale out
        if ($Parameters.Context.ExecutionContext.Roles.Role.RoleName -ieq "Cluster")
        {
            $nodes = $Parameters.Context.ExecutionContext.Roles.Role.Nodes.Node.PhysicalNodes.PhysicalNode.Name
        }
        else
        {
            $nodes = $Parameters.Context.ExecutionContext.Roles.Role.Nodes.Node.Name
        }

        [Array]$provisioningNodes = $physicalMachinesRole.Nodes.Node | Where-Object { $nodes -contains $_.Name }
    }

    # If any machines are not accessible during this call, the caller may fail. It is important to mark all provisioning nodes as failed when this happens
    foreach ($node in $provisioningNodes)
    {
        $bmcIP = $node.BmcIPAddress
        $nodeName = $node.Name
        $nodeInstance = $node.NodeInstance
        $oobProtocol = $node.OOBProtocol

        # TODO: For now this function will not support virtual hosts.
        # Due to time constraints and TZL requirements to get
        # SED support and clean up in CI, we will comment this
        # part of handling Virtual environments. MUST FIX soon.
        if ($OEMModel -match "Hyper-V")
        {
            Trace-Warning "Virtual environments are not supported in this version."
            Trace-Warning "This function is FOR NOW only designed to be called for SED cleanup on physical machines"
            Trace-Warning "Virtual environments will be handled here with the coming version."

            <#
            $trustedHosts = (Get-Item -Path WSMan:\localhost\Client\TrustedHosts).Value
            if (($trustedHosts -ne '*') -and ($bmcIP -notin $trustedHosts.Split(',')))
            {
                Set-Item -Path WSMan:\localhost\Client\TrustedHosts -Value $bmcIP -Concatenate -Force
            }
 
            Invoke-Command -ComputerName $bmcIP -Credential $bareMetalCredential -ScriptBlock {Stop-VM -VMName $using:nodeName -TurnOff -Force -ErrorAction Stop } -ErrorAction Stop
 
            Set-Item -Path WSMan:\localhost\Client\TrustedHosts -Value $trustedHosts -Force
            #>

        }
        else
        {
            Trace-Execution "Starting physical node: $nodeName ($bmcIP)."
            Start-IpmiDevice -TargetAddress $bmcIP -Credential $bareMetalCredential -NodeInstance $NodeInstance -OOBProtocol $oobProtocol -Verbose -Wait
        }
    }
}

<#
.Synopsis
     Function to add a DHCP MAC-based reservation for a given machine.
.Parameter NetworkName
     The network name within the infrastructure network where the IP should be reserved.
.Parameter NodeName
     The node name to be associated with the reservation.
.Parameter IPv4Address
     An IPv4 Address from the speficied network scope that the node will have reserved.
.Parameter MacAddress
     The MAC address to associate with the reservation -- this can be in either dash or dashless format, but not comma format.
.Parameter RemoteDHCPServerName
     The remote server that will act as the DHCP server to configure.
.Parameter RemoteDHCPServerNameCredentials
     Credentials used to add the reservation only used on a remote computer when specified.
.Example
    Add-DeployingMachineNetworkReservation -NetworkName 's-cluster-HostNic' -NodeName 'foo' -IPv4Address '10.0.0.1' -MACAddress '7C-FE-90-AF-1A-00'
 
    This will add a reservation for node foo with MAC 7C-FE-90-AF-1A-00 using the local credentials on the local DHCP server
#>

function Add-DeployingMachineNetworkReservation {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String]
        $NetworkName,

        [Parameter(Mandatory=$true)]
        [String]
        $NodeName,

        [Parameter(Mandatory=$true)]
        [String]
        $IPv4Address,

        [Parameter(Mandatory=$true)]
        [String]
        $MacAddress,

        [Parameter(Mandatory=$false)]
        [String]
        $RemoteDHCPServerName,

        [Parameter(Mandatory=$false)]
        [PSCredential]
        $RemoteDHCPServerNameCredentials
    )
    $scriptBlock =
    {
        $scope = Get-DhcpServerv4Scope | Where-Object { $_.Name -eq $using:NetworkName }

        $reservation = $scope | Get-DhcpServerv4Reservation | Where-Object { $_.ClientId.Replace(':','').Replace('-','') -eq ($using:MacAddress).Replace(':','').Replace('-','') }
        foreach ($entry in $reservation)
        {
            # Always clear existing reservations that apply to this client ID
            Remove-DhcpServerv4Reservation -ClientId $entry.ClientId -ScopeId $entry.ScopeId
        }

        $scope | Add-DhcpServerv4Reservation -Name $using:NodeName -IPAddress $using:IPv4Address -ClientId $using:MacAddress -Description $using:NodeName
    }

    if ($remoteDHCPServerName)
    {
        $session = New-PSSession -ComputerName $RemoteDHCPServerName -Credential $RemoteDHCPServerNameCredentials
    }
    else
    {
        $session = New-PSSession
    }

    try
    {
        Trace-Execution "Adding DHCP reservation in '$NetworkName' scope: $NodeName - $IPv4Address - $MacAddress."
        Invoke-Command -Session $session -ScriptBlock $scriptBlock
    }
    catch
    {
        Trace-Error "Failed with error: $_"
        throw
    }
    finally
    {
        Remove-PSSession -Session $session -ErrorAction Ignore
    }
}

<#
.Synopsis
     Function to add a DHCP MAC-based reservation for a given machine.
.Parameter NetworkName
     The network name within the infrastructure network where the IP should be reserved.
.Parameter NodeName
     The node name to be associated with the reservation.
.Parameter IPv4Address
     An IPv4 Address from the speficied network scope that the node will have reserved.
.Parameter RemoteDHCPServerName
     The remote server that will act as the DHCP server to configure.
.Parameter RemoteDHCPServerNameCredentials
     Credentials used to add the reservation only used on a remote computer when specified.
.Example
    Remove-MachineNetworkReservation -NetworkName 's-cluster-HostNic' -NodeName 'foo' -IPv4Address '10.0.0.1' -RemoteDHCPServerName 'Machine' -RemoteDHCPServerNameCredentials $cred
 
    This will remove a reservation and its leases for node foo with IP address specified
#>

function Remove-MachineNetworkReservation {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String]
        $NetworkName,

        [Parameter(Mandatory=$true)]
        [String]
        $IPv4Address,

        [Parameter(Mandatory=$true)]
        [String]
        $RemoteDHCPServerName,

        [Parameter(Mandatory=$true)]
        [PSCredential]
        $RemoteDHCPServerNameCredentials
    )

    $scriptBlock =
    {
        $scope = Get-DhcpServerv4Scope | Where-Object { $_.Name -eq $using:NetworkName }
        $scope | Get-DhcpServerv4Reservation | Where-Object { $_.IPAddress -eq ($using:IPv4Address) } | Remove-DhcpServerv4Reservation

        # Remove the DHCP lease for the machine that is being removed as well
        Get-DhcpServerv4Lease -ScopeId $scope.ScopeId | Where-Object { $_.IPAddress -eq $using:IPv4Address } | Remove-DhcpServerv4Lease
    }

    try
    {
        $session = New-PSSession -ComputerName $RemoteDHCPServerName -Credential $RemoteDHCPServerNameCredentials
        Trace-Execution "Adding DHCP reservation in '$NetworkName' scope: $NodeName - $IPv4Address ."
        Invoke-Command -Session $session -ScriptBlock $scriptBlock
    }
    catch
    {
        Trace-Error "Failed with error: $_"
        throw
    }
    finally
    {
        $session | Remove-PSSession -ErrorAction Ignore
    }
}

<#
.Synopsis
     Function to force a machine to boot in to PXE using BMC controls only.
.Parameter OOBManagementModulePath
     A relative path to the out-of-band management dll -- during deployment it is available locally but in ECE service it is located in an unpacked package.
.Parameter PhysicalNode
     Information about the node to start in to PXE.
.Parameter BMCCredential
     Credentials used to interact with the BMC controller.
.Example
    Start-PXEBoot -NodeName $Name -BmcIPAddress $BmcIPAddress -BMCCredential $cred
 
    This will force the machine to boot in to PXE
#>

function Start-PXEBoot {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)]
        [string]
        $OOBManagementModulePath="$PSScriptRoot\..\..\OOBManagement\bin\Microsoft.AzureStack.OOBManagement.dll",

        [Parameter(Mandatory=$true)]
        [string]
        $NodeName,

        [Parameter(Mandatory=$true)]
        [string]
        $BmcIPAddress,

        [Parameter(Mandatory=$true)]
        [PSCredential]
        $BMCCredential,

        [Parameter(Mandatory=$true)]
        [string]
        $NodeInstance,

        [Parameter(Mandatory=$true)]
        [string]
        $OOBProtocol
    )
    # After deployment, the source of this module changes
    Import-Module -Name $OOBManagementModulePath
    Import-Module -Name "$PSScriptRoot\..\..\OOBManagement\bin\Newtonsoft.Json.dll"

    $logText = "Initiate PXE boot for $NodeName (BMC: $BmcIPAddress). `r`n"
    $logText += "Shutdown $NodeName (BMC: $BmcIPAddress). `r`n"
    # Normally the following line should do nothing, because the machine is expected to have been powered off in an earlier step, unless this is a deployment
    # restart, in which case the machine may be running.
    $logText += (Stop-IpmiDevice -TargetAddress $BmcIPAddress -Credential $BMCCredential -NodeInstance $NodeInstance -OOBProtocol $OOBProtocol -Wait -Verbose 4>&1)

    $logText += "`n Set PXE boot $NodeName (BMC: $BmcIPAddress). `r`n"
    # Some machines will set the next boot device correctly only if the machine is currently powered off.
    $logText += (Set-IpmiDeviceOneTimePxeBoot -TargetAddress $BmcIPAddress -Credential $BMCCredential -NodeInstance $NodeInstance -OOBProtocol $OOBProtocol -Verbose 4>&1)

    $logText += "`n Start $NodeName (BMC: $BmcIPAddress). `r`n"
    $logText += (Start-IpmiDevice -TargetAddress $BmcIPAddress -Credential $BMCCredential -NodeInstance $NodeInstance -OOBProtocol $OOBProtocol -Wait -Verbose 4>&1)

    $logText += "`n Set PXE boot $NodeName (BMC: $BmcIPAddress OEMWorkaround). `r`n"
    # Some machines (Dell R630) will set the next boot device correctly only if the machine is currently powered on and gets power cycled (not reset, but
    # specifically power cycled) after the request to set one-time PXE boot.
    $logText += (Set-IpmiDeviceOneTimePxeBoot -TargetAddress $BmcIPAddress -Credential $BMCCredential -NodeInstance $NodeInstance -OOBProtocol $OOBProtocol -Verbose 4>&1)

    $logText += "`n Restart $NodeName (BMC: $BmcIPAddress OEMWorkaround). `r`n"
    $logText += (Restart-IpmiDevice -TargetAddress $BmcIPAddress -Credential $BMCCredential -NodeInstance $NodeInstance -OOBProtocol $OOBProtocol -Verbose 4>&1)
    $retObj = @{LogText = $logText}
    return $retObj
}

<#
.Synopsis
     Function to force a machine to reboot out of band from any context that can access BMC.
.Parameter OOBManagementModulePath
     A relative path to the out-of-band management dll -- during deployment it is available locally but in ECE service it is located in an unpacked package.
.Parameter PhysicalNode
     Information about the node to restart.
.Parameter BMCCredential
     Credentials used to interact with the BMC controller.
.Example
    Restart-Node -PhysicalNode $node -BMCCredential $cred
 
    This will force the machine to reboot
#>

function Restart-Node {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)]
        [string]
        $OOBManagementModulePath="$PSScriptRoot\..\..\OOBManagement\bin\Microsoft.AzureStack.OOBManagement.dll",

        [Parameter(Mandatory=$true)]
        [System.Xml.XmlElement]
        $PhysicalNode,

        [Parameter(Mandatory=$true)]
        [PSCredential]
        $BMCCredential,

        [Parameter(Mandatory=$true)]
        [string]
        $NodeInstance,

        [Parameter(Mandatory=$true)]
        [string]
        $OOBProtocol
    )
    # After deployment, the source of this module changes
    Import-Module -Name $OOBManagementModulePath
    Import-Module -Name "$PSScriptRoot\..\..\OOBManagement\bin\Newtonsoft.Json.dll"

    $nodeName = $PhysicalNode.Name
    $bmcIP = $PhysicalNode.BmcIPAddress
    Trace-Execution "Initiate IPMI-based restart for $nodeName (BMC: $bmcIP)."

    Restart-IpmiDevice -TargetAddress $bmcIP -Credential $BMCCredential -NodeInstance $NodeInstance -OOBProtocol $OOBProtocol -Verbose -Wait
}
<#
.Synopsis
     Function to get product information of a physical node.
.Parameter OOBManagementModulePath
     A relative path to the out-of-band management dll -- during deployment it is available locally but in ECE service it is located in an unpacked package.
.Parameter PhysicalNode
     Information about the node.
.Parameter BMCCredential
     Credentials used to interact with the BMC controller.
.Example
    Restart-Node -PhysicalNode $node -BMCCredential $cred
#>

function Get-ProductInfo {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        $OOBManagementModulePath = "$PSScriptRoot\..\..\OOBManagement\bin\Microsoft.AzureStack.OOBManagement.dll",

        [Parameter(Mandatory=$true)]
        [System.Xml.XmlElement]
        $PhysicalMachinesRole,

        [Parameter(Mandatory=$true)]
        [PSCredential]
        $BMCCredential,

        [Parameter(Mandatory=$true)]
        [string]
        $OEMModel

    )
    # After deployment, the source of this module changes
    Import-Module -Name $OOBManagementModulePath
    Import-Module -Name "$PSScriptRoot\..\..\OOBManagement\bin\Newtonsoft.Json.dll"

    if ([String]::IsNullOrEmpty($Parameters.Context.ExecutionContext.Roles.Role.Nodes.Node.Name))
    {
        Trace-Execution "Target to all physical nodes since node list not set in execution context"
        [Array]$provisioningNodes = $PhysicalMachinesRole.Nodes.Node
    }
    else
    {
        Trace-Execution "Get node list from execution context"

        # Determine whether the context of the operation is a cluster scale out
        if ($Parameters.Context.ExecutionContext.Roles.Role.RoleName -ieq "Cluster")
        {
            $nodes = $Parameters.Context.ExecutionContext.Roles.Role.Nodes.Node.PhysicalNodes.PhysicalNode.Name
        }
        else
        {
            $nodes = $Parameters.Context.ExecutionContext.Roles.Role.Nodes.Node.Name
        }

        [Array]$provisioningNodes = $PhysicalMachinesRole.Nodes.Node | Where-Object { $nodes -contains $_.Name }
    }
    foreach ($node in $provisioningNodes)
    {
        $bmcIP = $node.BmcIPAddress
        $nodeName = $node.Name
        $nodeInstance = $node.NodeInstance
        $oobProtocol = $node.OOBProtocol

        try
        {
            Trace-Execution "Initiate IPMI-based cmd to get Fru log for $nodeName (BMC: $bmcIP)."
            $fru = Get-IpmiDeviceFruLogs -TargetAddress $bmcIP -Credential $BMCCredential -NodeInstance $nodeInstance -OOBProtocol $oobProtocol
            if ($fru)
            {
                return @{"Model"  = $fru.ProductInfo.ProductName
                        "Vendor" = $fru.ProductInfo.ManufacturerName
                        "Serial" = $fru.ProductInfo.SerialNumber
                       }
            }
        }
        catch
        {
            Trace-Execution "Fail to Get-IpmiDeviceFruLogs for $nodeName (BMC: $bmcIP): $_"
        }
    }

    return $null

}

<#
.Synopsis
     Function to wait for ping, CIM, recent OS installation (with a deployment artifact) and WinRM to be available on a machine
.Parameter StartTime
     The start time of the operation, used to check that OS boot time was strictly after the wait period.
.Parameter StopTime
     The stop time for the wait operation after which the operation is considered failed.
.Parameter PhysicalNodeArray
     A list of physical machine nodes to wait.
.Parameter RemoteCIMCredential
     The credential to use to connect to CIM and WinRM.
.Parameter DeploymentID
     A unique identifier meant to signify the deployment that the machine is associated with.
.Parameter IgnoreOldOSCheckResult
     Whether to consider it a failure if the OS discovered on the machine is from before the waiting period began.
#>

function Wait-RemoteCimInitializedOneNodeBareMetal {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [DateTime]
        $StartTime,

        [Parameter(Mandatory=$true)]
        [DateTime]
        $StopTime,

        [Parameter(Mandatory=$true)]
        [System.Xml.XmlElement[]]
        $PhysicalNodeArray,

        [Parameter(Mandatory=$true)]
        [PSCredential]
        $RemoteCIMCredential,

        [Parameter(Mandatory=$true)]
        [Guid]
        $DeploymentID,

        [Parameter(Mandatory=$true)]
        [bool]
        $IgnoreOldOSCheckResults
    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    $remainingNodes = $PhysicalNodeArray

    $failedNodeNames = @()
    do {
        $nodes = $remainingNodes
        foreach ($node in $nodes)
        {
            $nodeName = $node.Name
            $nodeIP = $node.IPv4Address

            # Default to HostNIC if we cant find any IPv4Addresses
            if (-not $nodeIP)
            {
                $hostNICNetworkName = Get-NetworkNameForCluster -ClusterName "s-cluster" -NetworkName "HostNIC"
                $nodeIP = ($node.NICs.NIC | Where-Object NetworkId -eq $hostNICNetworkName | ForEach-Object IPv4Address)[0].Split('/')[0]
            }

            if (Test-IPConnection $nodeIP)
            {
                $cimSession = $null
                $session = $null

                try
                {
                    $errorCountBefore = $global:error.Count
                    #try the node name first because in some scenario IP is not trusted
                    $cimSession = New-CimSession -ComputerName $nodeName -Credential $RemoteCIMCredential -ErrorAction SilentlyContinue
                    if ($null -eq $cimSession)
                    {
                        $cimSession = New-CimSession -ComputerName $nodeIP -Credential $RemoteCIMCredential -ErrorAction SilentlyContinue
                    }
                    $os = $null

                    if ($cimSession)
                    {
                        $os = Get-CimInstance win32_operatingsystem -CimSession $cimSession -ErrorAction SilentlyContinue
                    }

                    $errorCountAfter = $global:error.Count
                    $numberOfNewErrors = $errorCountAfter - $errorCountBefore

                    if ($numberOfNewErrors -gt 0)
                    {
                        $global:error.RemoveRange(0, $numberOfNewErrors)
                    }

                    if ($os)
                    {
                        $osLastBootUpTime = $os.LastBootUpTime
                        $osInstallTime = $os.InstallDate
                        #try the node name first because in some scenario IP is not trusted
                        $session = New-PSSession -ComputerName $nodeName -Credential $RemoteCIMCredential -ErrorAction SilentlyContinue
                        if ($null -eq $session)
                        {
                            $session = New-PSSession -ComputerName $nodeIP -Credential $RemoteCIMCredential -ErrorAction SilentlyContinue
                        }
                    }
                    else
                    {
                        Trace-Execution "$nodeName could not query an OS."
                    }
                }
                catch
                {
                    if ($session)
                    {
                        Trace-Execution "Caught exception after session to new OS was created: $_"
                        Remove-PSSession $session -ErrorAction SilentlyContinue
                        $session = $null
                    }

                    $global:error.RemoveAt(0)
                }

                if ($session)
                {
                    $isNewDeployment = Invoke-Command -Session $session -ScriptBlock { Test-Path "$env:SystemDrive\CloudBuilderTemp\$($using:DeploymentID).txt" }

                    if ($isNewDeployment)
                    {
                        Trace-Execution "$nodeName has finished OS deployment. Boot time reported by the node - $($osLastBootUpTime.ToString())."
                        $failedNodeNames = $failedNodeNames | Where-Object Name -ne $nodeName
                    }
                    else
                    {
                        Trace-Warning "$nodeName has booted up to an old OS installed on $($osInstallTime.ToString())."

                        if (-not $IgnoreOldOSCheckResult)
                        {
                            $failedNodeNames += $nodeName
                        }
                    }

                    $remainingNodes = $remainingNodes | Where-Object Name -ne $node.Name
                }
                elseif ($cimSession)
                {
                    Remove-CimSession $cimSession
                }
                else
                {
                    Trace-Execution "$nodeName could not establish CIM session using IP $nodeIP or name."
                }
            }
            else
            {
                Trace-Execution "$nodeName did not respond to ping with IP $nodeIP ."
            }
        }

        if (-not $remainingNodes) { break }

        Start-Sleep -Seconds 15

    } until ([DateTime]::Now -gt $StopTime)

    $remainingNodeNames = @($remainingNodes | ForEach-Object Name)
    $totalBmdWaitTimeMinutes = [int]($StopTime - $StartTime).TotalMinutes

    if ($failedNodeNames -or $remainingNodeNames)
    {
        Trace-Error "Bare metal deployment failed to complete in $totalBmdWaitTimeMinutes minutes - $(($failedNodeNames + $remainingNodeNames) -join ',')."
    }
    else
    {
        Trace-Execution "Bare metal deployment has completed on the target node."
    }
}

function Wait-ForISOImageBareMetal {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [DateTime]
        $StartTime,

        [Parameter(Mandatory=$true)]
        [DateTime]
        $StopTime,

        [Parameter(Mandatory=$true)]
        [Array]
        $PhysicalNodeArray,

        [Parameter(Mandatory=$true)]
        [PSCredential]
        $RemoteCIMCredential,

        [Parameter(Mandatory=$true)]
        [Guid]
        $DeploymentID,

        [Parameter(Mandatory=$true)]
        [bool]
        $IgnoreOldOSCheckResults
    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    [Array]$remainingNodes = $PhysicalNodeArray | Sort-Object Name
    $failedNodeNames = @()
    $wait = $true

    while ($wait)
    {
        if ([DateTime]::Now -lt $StopTime)
        {
            foreach ($node in $remainingNodes)
            {
                $nodeName = $node.Name
                $nodeIP = $node.IPv4Address
                if (Test-IPConnection $nodeIP)
                {
                    Trace-Execution "$nodeName is responding to ping"
                    $cimSession = $null
                    $session = $null
                    try
                    {
                        $errorCountBefore = $global:error.Count
                        # Try the node name first because in some scenario IP is not trusted
                        $cimSession = New-CimSession -ComputerName $nodeName -Credential $RemoteCIMCredential -ErrorAction SilentlyContinue
                        if ($null -eq $cimSession)
                        {
                            $cimSession = New-CimSession -ComputerName $nodeIP -Credential $RemoteCIMCredential -ErrorAction SilentlyContinue
                        }
    
                        $os = $null
                        if ($null -ne $cimSession)
                        {
                            Trace-Execution "$nodeName CIM was session created - getting Win32_OperatingSystem data"
                            $os = Get-CimInstance Win32_OperatingSystem -CimSession $cimSession -ErrorAction SilentlyContinue
                        }
    
                        $errorCountAfter = $global:error.Count
                        $numberOfNewErrors = $errorCountAfter - $errorCountBefore
    
                        if ($numberOfNewErrors -gt 0)
                        {
                            $global:error.RemoveRange(0, $numberOfNewErrors)
                        }
    
                        if ($null -ne $os)
                        {
                            $osLastBootUpTime = $os.LastBootUpTime
                            $osInstallTime = $os.InstallDate
    
                            Trace-Execution "$nodeName attempting to create PS session as '$($RemoteCIMCredential.UserName)'"
                            # Try the node name first because in some scenario IP is not trusted
                            try
                            {
                                $session = Microsoft.PowerShell.Core\New-PSSession -ComputerName $nodeName -Credential $RemoteCIMCredential
                                if ($null -eq $session)
                                {
                                    $session = Microsoft.PowerShell.Core\New-PSSession -ComputerName $nodeIP -Credential $RemoteCIMCredential
                                }
                            }
                            catch
                            {
                                Trace-Execution "$nodeName failed to created PS session with error: $($PSItem.Exception.Message)"
                            }
                        }
                        else
                        {
                            Trace-Execution "$nodeName could not query Win32_OperatingSystem"
                        }
                    }
                    catch
                    {
                        if ($null -ne $session)
                        {
                            Trace-Execution "Caught exception after session to new OS was created: $($PSItem.Exception.Message)"
                            Remove-PSSession $session -ErrorAction SilentlyContinue
                            $session = $null
                        }
                        $global:error.RemoveAt(0)
                    }
    
                    if ($null -ne $session)
                    {
                        Trace-Execution "$nodeName PS session was created - looking for $DeploymentID.txt file on this node"
                        Trace-Execution "Waiting for 5 min after successful network connection so that setupcomplete can converge on the physical machine."
                        Start-Sleep -Seconds 300
                        $isNewDeployment = Invoke-Command -Session $session -ScriptBlock { Test-Path -Path "$env:SystemDrive\CloudBuilderTemp\$($using:DeploymentID).txt" }
                        if ($true -eq $isNewDeployment)
                        {
                            Trace-Execution "$nodeName has finished OS deployment. Boot time reported by the node - $($osLastBootUpTime.ToString())"
                            $failedNodeNames = $failedNodeNames | Where-Object Name -ne $nodeName
                        }
                        else
                        {
                            Trace-Warning "$nodeName has booted up to an old OS installed on $($osInstallTime.ToString())"
                            if (-not $IgnoreOldOSCheckResult)
                            {
                                $failedNodeNames += $nodeName
                            }
                        }
                        Trace-Execution "$nodeName has completed bare metal deployment"
                        $remainingNodes = $remainingNodes | Where-Object Name -ne $nodeName
                    }
                    else
                    {
                        Trace-Execution "$nodeName could not establish Powershell session using IP $nodeIP or name"
                        Trace-Execution "$nodeName attempt to map network drive to C`$"
                        try
                        {
                            $fileFound = $false
                            $netShare = New-PSDrive -Name $nodeName -PSProvider FileSystem -Root "\\$nodeName\C`$" -Credential $RemoteCIMCredential
                            if ($null -ne $netShare)
                            {
                                $fileFound = Test-Path -Path "$($nodeName):\CloudBuilderTemp\$($DeploymentID).txt"
                            }
                        }
                        catch
                        {
                            Trace-Execution "$nodeName failed to map network drive with error: $($PSItem.Exception.Message)"
                        }
                        if ($true -eq $fileFound)
                        {
                            Trace-Execution "$nodeName has completed bare metal deployment"
                            $remainingNodes = $remainingNodes | Where-Object Name -ne $nodeName
                        }
                    }
    
                    if ($null -ne $cimSession)
                    {
                        Remove-CimSession $cimSession
                    }
                    else
                    {
                        Trace-Execution "$nodeName could not establish CIM session using IP $nodeIP or name"
                    }

                    if ($null -ne $netShare)
                    {
                        Remove-PSDrive -Name $nodeName -ErrorAction SilentlyContinue
                        $netShare = $null
                    }
                }
                else
                {
                    Trace-Execution "$nodeName did not respond to ping with IP $nodeIP"
                }
            }
    
            if ($remainingNodes.Count -eq 0)
            {
                Trace-Execution "All nodes have completed bare metal deployment"
                $wait = $false
            }
            else
            {
                Trace-Execution "Still waiting for $($remainingNodes.Count) node(s) to complete"
                Start-Sleep -Seconds 60
            }
        }
        else
        {
            Trace-Execution "Timed out waiting for all nodes to complete bare metal deployment"
            Trace-Execution "$($remainingNodes.Count) node(s) did not respond in time"
            $wait = $false
        }
    }

    if ($remainingNodes.Count -eq 0)
    {
        $remainingNodeNames = @()
    }
    else
    {
        $remainingNodeNames = @($remainingNodes | ForEach-Object Name)
    }

    $totalBmdWaitTimeMinutes = [int]($StopTime - $StartTime).TotalMinutes

    if (($failedNodeNames.Count + $remainingNodeNames.Count) -gt 0)
    {
        [string]$badNodes = ($failedNodeNames + $remainingNodeNames) -join ','
        Trace-Error "Bare metal deployment failed to complete in $totalBmdWaitTimeMinutes minutes - $badNodes."
    }
    else
    {
        Trace-Execution "Bare metal deployment has completed on the target nodes."
    }
}

<#
.SYNOPSIS
    While a host is being restarted and brought back online, this function is used to select a different host on the same cluster
    to perform remote PowerShell operations on.
#>

function Get-HostNameForRemoting {
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters,

        [Parameter(Mandatory=$true)]
        [string]
        $HostBeingRebooted
    )

    $ErrorActionPreference = "Stop"

    $clusterName = Get-NodeRefClusterId -Parameters $Parameters -RoleName "BareMetal" -NodeName $HostBeingRebooted
    $availableHosts = Get-ActiveClusterNodes -Parameters $Parameters -ClusterName $clusterName
    $availableHosts = $availableHosts | Where-Object { $_ -ne $HostBeingRebooted }
    if ($availableHosts.Count -eq 0)
    {
        Trace-Error "There are no available hosts on which to perform remote operations."
    }
    else
    {
        Trace-Execution "Selected host $($availableHosts[0])."
        return $availableHosts[0]
    }
}

<#
.SYNOPSIS
    This function is used to do cluster aware update based on different Cau Plugins.
#>

function Invoke-CauRunHelper {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [System.Collections.Hashtable]
        $cauParams
    )

    $ErrorActionPreference = "Stop"

    Trace-Execution "Start invoke caurun"

    Invoke-CauRun @cauParams
}

Export-ModuleMember -Function Start-PXEBoot
Export-ModuleMember -Function Restart-Node
Export-ModuleMember -Function Stop-DeployingMachines
Export-ModuleMember -Function Start-DeployingMachines
Export-ModuleMember -Function Add-DeployingMachineNetworkReservation
Export-ModuleMember -Function Remove-MachineNetworkReservation
Export-ModuleMember -Function Wait-RemoteCimInitializedOneNodeBareMetal
Export-ModuleMember -Function Wait-ForISOImageBareMetal
Export-ModuleMember -Function Get-HostNameForRemoting
Export-ModuleMember -Function Get-ProductInfo
Export-ModuleMember -Function Invoke-CauRunHelper

# SIG # Begin signature block
# MIIoLQYJKoZIhvcNAQcCoIIoHjCCKBoCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD0NXoJNEboxNGW
# ZU2YnPu9GkeV561/p/S/GGxHF9S7X6CCDXYwggX0MIID3KADAgECAhMzAAADrzBA
# DkyjTQVBAAAAAAOvMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwOTAwWhcNMjQxMTE0MTkwOTAwWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDOS8s1ra6f0YGtg0OhEaQa/t3Q+q1MEHhWJhqQVuO5amYXQpy8MDPNoJYk+FWA
# hePP5LxwcSge5aen+f5Q6WNPd6EDxGzotvVpNi5ve0H97S3F7C/axDfKxyNh21MG
# 0W8Sb0vxi/vorcLHOL9i+t2D6yvvDzLlEefUCbQV/zGCBjXGlYJcUj6RAzXyeNAN
# xSpKXAGd7Fh+ocGHPPphcD9LQTOJgG7Y7aYztHqBLJiQQ4eAgZNU4ac6+8LnEGAL
# go1ydC5BJEuJQjYKbNTy959HrKSu7LO3Ws0w8jw6pYdC1IMpdTkk2puTgY2PDNzB
# tLM4evG7FYer3WX+8t1UMYNTAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQURxxxNPIEPGSO8kqz+bgCAQWGXsEw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMTgyNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAISxFt/zR2frTFPB45Yd
# mhZpB2nNJoOoi+qlgcTlnO4QwlYN1w/vYwbDy/oFJolD5r6FMJd0RGcgEM8q9TgQ
# 2OC7gQEmhweVJ7yuKJlQBH7P7Pg5RiqgV3cSonJ+OM4kFHbP3gPLiyzssSQdRuPY
# 1mIWoGg9i7Y4ZC8ST7WhpSyc0pns2XsUe1XsIjaUcGu7zd7gg97eCUiLRdVklPmp
# XobH9CEAWakRUGNICYN2AgjhRTC4j3KJfqMkU04R6Toyh4/Toswm1uoDcGr5laYn
# TfcX3u5WnJqJLhuPe8Uj9kGAOcyo0O1mNwDa+LhFEzB6CB32+wfJMumfr6degvLT
# e8x55urQLeTjimBQgS49BSUkhFN7ois3cZyNpnrMca5AZaC7pLI72vuqSsSlLalG
# OcZmPHZGYJqZ0BacN274OZ80Q8B11iNokns9Od348bMb5Z4fihxaBWebl8kWEi2O
# PvQImOAeq3nt7UWJBzJYLAGEpfasaA3ZQgIcEXdD+uwo6ymMzDY6UamFOfYqYWXk
# ntxDGu7ngD2ugKUuccYKJJRiiz+LAUcj90BVcSHRLQop9N8zoALr/1sJuwPrVAtx
# HNEgSW+AKBqIxYWM4Ev32l6agSUAezLMbq5f3d8x9qzT031jMDT+sUAoCw0M5wVt
# CUQcqINPuYjbS1WgJyZIiEkBMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGg0wghoJAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIDyDubBbNjKcac35c9njzJLp
# b+TlCMDwT8eiEKZZxaAfMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAL4Q/EI37VVze5XAlmCPbW9a+rk4jnEtEAgGKV422SiZiU7+5eWPoX8UJ
# KXYm3DAcmYV8mZxeP1d1UV3lf1vvvMaFA75bCNFVNSJaSpSf5UO+f8AOu8wmUJmV
# fnx8+U8ua+7qTSj2dJ7xEU1CgFr5j7fzic6PDEu7RCWwzB7vLA3E8TL/dBR+Foqe
# 11XYUUmCHIhSmw+UT7ZSh0HbI25WhCO0SSlpYLsJpR+04dObMxz8AArb1Kchse3R
# eqHfSNc6kP5lLHPC062/YEtxnugfyqWmK9vDEAfKVgDDFJdXQrt3m1K6+JtJ85YO
# GO3xEjx7QnnSEH388xvzJ0JAvFcWaaGCF5cwgheTBgorBgEEAYI3AwMBMYIXgzCC
# F38GCSqGSIb3DQEHAqCCF3AwghdsAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq
# hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCBxO3IjOswz9C+0CFqg3gELpqj27VtoQ3ZeHJp5KaRkSwIGZbwS80Vj
# GBMyMDI0MDIxMjE0MDUyNy45NDZaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l
# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTIwMC0w
# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg
# ghHtMIIHIDCCBQigAwIBAgITMwAAAecujy+TC08b6QABAAAB5zANBgkqhkiG9w0B
# AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzEyMDYxODQ1
# MTlaFw0yNTAzMDUxODQ1MTlaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z
# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTIwMC0wNUUwLUQ5NDcxJTAjBgNV
# BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQDCV58v4IuQ659XPM1DtaWMv9/HRUC5kdiEF89YBP6/
# Rn7kjqMkZ5ESemf5Eli4CLtQVSefRpF1j7S5LLKisMWOGRaLcaVbGTfcmI1vMRJ1
# tzMwCNIoCq/vy8WH8QdV1B/Ab5sK+Q9yIvzGw47TfXPE8RlrauwK/e+nWnwMt060
# akEZiJJz1Vh1LhSYKaiP9Z23EZmGETCWigkKbcuAnhvh3yrMa89uBfaeHQZEHGQq
# dskM48EBcWSWdpiSSBiAxyhHUkbknl9PPztB/SUxzRZjUzWHg9bf1mqZ0cIiAWC0
# EjK7ONhlQfKSRHVLKLNPpl3/+UL4Xjc0Yvdqc88gOLUr/84T9/xK5r82ulvRp2A8
# /ar9cG4W7650uKaAxRAmgL4hKgIX5/0aIAsbyqJOa6OIGSF9a+DfXl1LpQPNKR79
# 2scF7tjD5WqwIuifS9YUiHMvRLjjKk0SSCV/mpXC0BoPkk5asfxrrJbCsJePHSOE
# blpJzRmzaP6OMXwRcrb7TXFQOsTkKuqkWvvYIPvVzC68UM+MskLPld1eqdOOMK7S
# bbf2tGSZf3+iOwWQMcWXB9gw5gK3AIYK08WkJJuyzPqfitgubdRCmYr9CVsNOuW+
# wHDYGhciJDF2LkrjkFUjUcXSIJd9f2ssYitZ9CurGV74BQcfrxjvk1L8jvtN7mul
# IwIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFM/+4JiAnzY4dpEf/Zlrh1K73o9YMB8G
# A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG
# Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy
# MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w
# XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy
# dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG
# A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD
# AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQB0ofDbk+llWi1cC6nsfie5Jtp09o6b6ARC
# pvtDPq2KFP+hi+UNNP7LGciKuckqXCmBTFIhfBeGSxvk6ycokdQr3815pEOaYWTn
# HvQ0+8hKy86r1F4rfBu4oHB5cTy08T4ohrG/OYG/B/gNnz0Ol6v7u/qEjz48zXZ6
# ZlxKGyZwKmKZWaBd2DYEwzKpdLkBxs6A6enWZR0jY+q5FdbV45ghGTKgSr5ECAOn
# LD4njJwfjIq0mRZWwDZQoXtJSaVHSu2lHQL3YHEFikunbUTJfNfBDLL7Gv+sTmRi
# DZky5OAxoLG2gaTfuiFbfpmSfPcgl5COUzfMQnzpKfX6+FkI0QQNvuPpWsDU8sR+
# uni2VmDo7rmqJrom4ihgVNdLaMfNUqvBL5ZiSK1zmaELBJ9a+YOjE5pmSarW5sGb
# n7iVkF2W9JQIOH6tGWLFJS5Hs36zahkoHh8iD963LeGjZqkFusKaUW72yMj/yxTe
# GEDOoIr35kwXxr1Uu+zkur2y+FuNY0oZjppzp95AW1lehP0xaO+oBV1XfvaCur/B
# 5PVAp2xzrosMEUcAwpJpio+VYfIufGj7meXcGQYWA8Umr8K6Auo+Jlj8IeFS6lSv
# KhqQpmdBzAMGqPOQKt1Ow3ZXxehK7vAiim3ZiALlM0K546k0sZrxdZPgpmz7O8w9
# gHLuyZAQezCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI
# hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy
# MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
# AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg
# M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF
# dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6
# GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp
# Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu
# yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E
# XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0
# lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q
# GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ
# +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA
# PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw
# EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG
# NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV
# MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj
# cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK
# BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG
# 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x
# M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC
# VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449
# xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM
# nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS
# PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d
# Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn
# GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs
# QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL
# jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL
# 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNQ
# MIICOAIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjkyMDAtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQCz
# cgTnGasSwe/dru+cPe1NF/vwQ6CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6XRomjAiGA8yMDI0MDIxMjA5NTEy
# MloYDzIwMjQwMjEzMDk1MTIyWjB3MD0GCisGAQQBhFkKBAExLzAtMAoCBQDpdGia
# AgEAMAoCAQACAiQnAgH/MAcCAQACAhNpMAoCBQDpdboaAgEAMDYGCisGAQQBhFkK
# BAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJ
# KoZIhvcNAQELBQADggEBAHhDZT8ty0+HTaXE1QfKEhJH/rvJzWEO4nHtcHJIH8yq
# k0objav8hh+sDUiNDqhoN+F+mA+2mpGuR3E0YvquPCG+T/EdDAOiXEjZ0cQhSNYJ
# w2MnVzd0PXYYSpICmGfYW2uHiVfG8juh7Mbk2dNQtq1PXM9ELZihLw0vis6sxXLy
# CebMHHwx/2j/s27g2kEN9BLs+Ih8v5HTno3kZMsGmqB+HX3G78m7y/FCJ8Z0+fbr
# tWnsIF71r8O3t/M7eX6HlekXNNAu2MdxOlGHKirfwsJ80Atfl9wcHkVbpfi4dffm
# bZ381cZNMRaMTKymjgs7CJhj2Y3lVr/2Tw8LKfMJZCExggQNMIIECQIBATCBkzB8
# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk
# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N
# aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAecujy+TC08b6QABAAAB
# 5zANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEE
# MC8GCSqGSIb3DQEJBDEiBCDRgpNWMZsJSxP+H/Z6VMgvKWujJKEo9RPoOwrg76OR
# fTCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIOU2XQ12aob9DeDFXM9UFHeE
# X74Fv0ABvQMG7qC51nOtMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB
# IDIwMTACEzMAAAHnLo8vkwtPG+kAAQAAAecwIgQg3D/byHCyRT6F6OS8ZCWcS+7X
# bbnGEcjZ7MC8fdRvYZYwDQYJKoZIhvcNAQELBQAEggIAqaEuHZdXm8uHL1hKu/vA
# Q0necChJAP9/6ZkVQCLtxGraT8ncFSaOkrYNT3QIsB07LGiSmAePhGh6S8Ms4T55
# E7E7v3XQoK/poDIbP2oUZsemkT5iuVky0kIf6ZPLhiezMN5gaWaVXJkVeHlW88rl
# QjX+5Fk4MQuyYzvGlUfes4rr9U/BDs4dFA7gpF6pf8U+PbkBcRrjPeJoS5NS76Qo
# 0oQ/MpaJg8AT8JKlx9si2FGTgLirivgWp1/bWuzUoewAxGvVxdLbaTdVrECGgRSM
# RKNLymbC+lpYk9GTV3HNd5ELBTgKgYeLQ8bNHFd02nuI4XE4qlTpqdoFCGYEyYtn
# Wa3vdMeE6yhDoYcf+ct7rzlNvD9WziZEgnkmYz+COfCMAXPkjL3GCEHqLZbm3l6I
# STk9NzIyP7VY3pwifcQ9QiHWaWOF3u1i94RmanxqACgwJvO3lugB41c1WVZF8qpV
# MINscEK7XPEJc8mCM6xp5DhFO9l1G8HqKUYzKrtSBfsii53t0enk0ekw3Kin9Zvu
# VGOVfNnVvuxSCBg/Yaj+Al3NijuoE3nsIgQ4psYJGrn6h4Z7MOhVq9+LjM/vLIX5
# gWIR+WBu+mDr94sDjGeeOqd/s6wbpasnw2/5WkR2vmEd1DXkxwD+ELfxbV+u78+E
# J5BD8ycBuFM/NwcDNKye4E0=
# SIG # End signature block