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

<###################################################
 # #
 # Copyright (c) Microsoft. All rights reserved. #
 # #
 ##################################################>


Import-Module $PSScriptRoot\JustEnoughAdministrationHelpers.psm1 -DisableNameChecking -Verbose:$false | Out-Null
Import-Module NetworkControllerRESTWrappers -DisableNameChecking -Verbose:$false | Out-Null
Import-Module $PSScriptRoot\NetworkControllerWorkloadHelpers.psm1 -DisableNameChecking -Verbose:$false | Out-Null

# Below 4 modules will ues -ErrorAction SilentlyContinue as the import might happen at the dependency repo code and those might not package the CloudCommon modules.
# In such case, the functionnality is not impacted as CloudCommon is always copied into Windows PowerShell module at the beginning of action plan exectuion
Import-Module $PSScriptRoot\..\..\Common\Helpers.psm1 -DisableNameChecking -Verbose:$false -ErrorAction SilentlyContinue | Out-Null
Import-Module $PSScriptRoot\..\..\Common\NetworkHelpers.psm1 -DisableNameChecking -Verbose:$false -ErrorAction SilentlyContinue | Out-Null
Import-Module $PSScriptRoot\..\..\Common\StorageHelpers.psm1 -DisableNameChecking -Verbose:$false -ErrorAction SilentlyContinue | Out-Null
Import-Module $PSScriptRoot\..\..\Common\ClusterHelpers.psm1 -DisableNameChecking -Verbose:$false -ErrorAction SilentlyContinue | Out-Null

Import-LocalizedData LocalizedStrings -FileName Roles.Strings.psd1 -ErrorAction SilentlyContinue
Import-LocalizedData LocalizedNetworkData -FileName Network.Strings.psd1 -ErrorAction SilentlyContinue

function Test-PSSession {
    Param(
        [Parameter(Mandatory=$true)]
        $RemoteSession,

        [Parameter(Mandatory=$true)]
        $SessionCredential,

        [Parameter(Mandatory=$true)]
        $SessionComputer
    )

    if (($RemoteSession -eq $null) -or ($RemoteSession.State -ne "Opened"))
    {
        Trace-Execution "Session not ready, state: $($RemoteSession.State)"
        Trace-Execution "Removing the session"
        $RemoteSession | Remove-PSSession

        Trace-Execution "Regenerating the session"
        $RemoteSession = New-PSSession -ComputerName $SessionComputer -Credential $SessionCredential

        Initialize-ECESession $RemoteSession
        Trace-Execution "State after regenerate = $($RemoteSession.State)"
    } else {
        Trace-Execution "Session Opened"
    }

    return $RemoteSession
}

# Make sure the MAC addresses and IPv4Address are loaded into Parameters object from the manifest.
function Set-MacAndIPAddressSingleNode {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters,

        [Parameter(Mandatory=$true)]
        [PSObject]
        $Node,

        [ValidateSet('PXE','Management')]
        [string]
        $IPv4AddressSource = 'PXE'
    )

    if (-not $node.MacAddress) {
        Trace-Execution "Failed to find MAC address for node '$($node.Name)' in the manifest. Skip this"
    }
    else
    {
        $macAddress = $node.MacAddress
        Trace-Execution "Normalizing MAC address '$macAddress'."
        # Normalize MAC address to .NET canonical form, e.g., E4-1D-2D-1D-25-30.
        $node.MacAddress = $node.MacAddress.ToUpper().Replace(':', '-')
    }

    $allHostNodes = Get-NetworkMgmtIPv4FromECEForAllHosts -Parameters $Parameters

    $ipv4Address = $allHostNodes[$node.Name]
    Trace-Execution "Setting IPv4 address $ipv4Address for node $($node.Name) ."

    if(-not $node.IPv4Address) {
        $node | Add-Member -MemberType NoteProperty -Name "IPv4Address" -value $Ipv4Address
    } else {
        $node.IPv4Address = $ipv4Address
    }
}

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

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

        [ValidateSet('PXE','Management')]
        [string]
        $IPv4AddressSource = 'PXE',

        [Parameter(Mandatory=$false)]
        [System.Xml.XmlElement]
        $Node = $null
    )

    Trace-ECEScript "Setting IP addresses." {

        if ($Node)
        {
            Set-MacAndIPAddressSingleNode -Parameters $Parameters -Node $node -IPv4AddressSource $IPv4AddressSource
        }
        else
        {
            foreach ($node in $PhysicalMachinesRole.Nodes.Node)
            {
                Set-MacAndIPAddressSingleNode -Parameters $Parameters -Node $node -IPv4AddressSource $IPv4AddressSource
            }
        }
    }
}

function Get-BareMetalCredential
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

    $cloudRole = $Parameters.Roles['Cloud'].PublicConfiguration

    # Account info
    $securityInfo = $cloudRole.PublicInfo.SecurityInfo
    $bareMetalUser = $securityInfo.HardwareUsers.User | Where-Object -Property Role -EQ 'BareMetalAdmin'
    $bareMetalCredential = $Parameters.GetCredential($bareMetalUser.Credential)

    return $bareMetalCredential
}

# Gets the current active Domain Controller computer name.
function Get-AvailableADComputerName
{
 [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true)]
        [String[]]
        $AllADComputerName,

        [Parameter(Mandatory = $false)]
        [PSCredential]
        $RemoteServiceCredentials = $null
    )
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    Trace-ECEScript "Getting the current active Domain Controller computer name." {

        # Proactively look for AD running on local machine (in case of multi-node, this will return the DVM).
        $service = Get-Service -Name "NTDS" -ErrorAction SilentlyContinue
        if($service -and $service.Status -eq "Running")
        {
            $activeADComputer = $env:COMPUTERNAME
        }
        else
        {
            # In case of one Node or post deployment, this will return DC01.
            foreach($adComputer in $AllADComputerName)
            {
                if ($RemoteServiceCredentials)
                {
                    try
                    {
                        $psSession = New-PsSession -Credential $RemoteServiceCredentials -ComputerName $adComputer -ErrorAction SilentlyContinue
                        if($psSession)
                        {
                            $service = Invoke-Command -Session $psSession -ScriptBlock { Get-Service -Name "NTDS" -ErrorAction SilentlyContinue }
                        }
                    }
                    finally
                    {
                        if ($psSession)
                        {
                            Remove-PsSession $psSession
                        }
                    }
                }
                else
                {
                    $service = Invoke-Command -ComputerName $adComputer -ScriptBlock { Get-Service -Name "NTDS" -ErrorAction SilentlyContinue } -ErrorAction SilentlyContinue
                }

                if ($service -and $service.Status -eq "Running")
                {
                    $activeADComputer = $adComputer
                    break
                }
            }
        }
    }

    if($activeADComputer)
    {
        Trace-Execution "Found AD running on $activeADComputer"
        return $activeADComputer
    }
    else
    {
        Trace-Error "No Available AD computer found."
    }
}

# Virtual Network Common
function Test-NetworkMap {
    param (
        [Parameter(Mandatory=$true)]
        $NetworkMap,

        [Parameter(Mandatory=$false)]
        [UInt32] $RetryCount = 0,

        [Parameter(Mandatory=$false)]
        [UInt32] $RetryIntervalInSeconds = 60,

        [Parameter(Mandatory=$false)]
        [string[]] $NodeNames
    )

    Trace-ECEScript $LocalizedNetworkData.CheckNICConnectivity {

        $nodesToCheck = $NetworkMap.Keys
        if ($NodeNames) {
            $nodesToCheck = $nodesToCheck | Where-Object {$_ -in $NodeNames}
        }

        $retry = 0
        do
        {
            try
            {
                # Test nic connectivity for each machine
                foreach ($nodeName in ($nodesToCheck | Sort-Object))
                {
                    $node = $NetworkMap.$nodeName
                    $hasPingableConnections = $false

                    foreach ($nicId in ($node.Keys | Sort-Object))
                    {
                        $nic = $NetworkMap.$nodeName.$nicId
                        try
                        {
                            # This cast will fail if the IP address isn't well-formed
                            $ipAddress = [ipaddress]$nic.IPv4Address.Split('/')[0]
                            $canPing = Test-IPConnection $ipAddress.IPAddressToString
                        }
                        catch
                        {
                            $canPing = $false
                        }

                        if ($canPing) {
                            $hasPingableConnections = $true
                        }

                        $nic | Add-Member -MemberType NoteProperty -Name CanPing -Value $canPing

                        Trace-Execution "$(if ($canPing) {'+'} else {'-'}) $nodeName | $($nic.Name)"
                    }

                    if (-not $hasPingableConnections) {
                        throw ($LocalizedNetworkData.NoPingableConnections -f $nodeName)
                    }
                }
                break
            }
            catch
            {
                if($retry -eq $RetryCount) {
                    throw
                }
                Trace-Execution "Test-NetworkMap failed. Will retry in $RetryIntervalInSeconds seconds. Retry attempted: $retry."
                Start-Sleep -Seconds $RetryIntervalInSeconds
            }
            $retry++
        } while ($retry -le $RetryCount)
    }
}

function Get-IsVirtualNetworkAlreadyConfigured {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        $NetworkMap,

        [Parameter(Mandatory=$true)]
        $IsOneNode
    )

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

    # Check that all non-Deployment vNICs can be pinged.
    $notPingableAdapters = $NetworkMap.Values.Values | ? CanPing -ne $true

    if (-not $notPingableAdapters)
    {
        Trace-Execution $LocalizedNetworkData.NetworkingAlreadyConfigured
        return $true
    }

    return $false
}

# BUGBUG: This function may not needed any more in ASZ. Leave it here to not break existing code. Need refactoring.
function Get-NetworkMap {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters,

        $PhysicalMachinesRole
    )

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

    $nodeNames = $PhysicalMachinesRole.Nodes.Node | % Name

    Trace-ECESCript "Get Network Map for nodes $nodeNames" {

        # If multiple nodes are being added at the same time, only test those that are being deployed by the current action plan instance
        $executionRoleName = $Parameters.Context.ExecutionContext.Roles.Role.RoleName
        $hostsList = $Parameters.Context.ExecutionContext.Roles.Role.Nodes.Node.Name

        if ($executionRoleName -ieq "Cluster")
        {
            $hostsList = $null
            $hostsList = [array]($Parameters.Context.ExecutionContext.Roles.Role.Nodes.Node.PhysicalNodes.PhysicalNode.Name)
        }

        $networkmap = @{}
        $allHostNodes = Get-NetworkMgmtIPv4FromECEForAllHosts -Parameters $Parameters

        foreach ($node in $PhysicalMachinesRole.Nodes.Node)
        {
            if (-not($node.OperationStatus -eq "RequiresRemediation") -and (-not($node.OperationStatus -eq "Adding") -or ($hostsList -contains $node.Name)))
            {
                # there would be only 1 IP on each host that may need checking.
                $nodeNics = @{}

                $nicIPv4 = $allHostNodes[$node.Name]
                $nicName = $node.Name

                if (-not (Test-NetworkIPv4Address -IPv4Address $nicIPv4))
                {
                    Trace-Execution "[Get-NetworkMap]: IPv4 Address for node $($node.Name) $nicIPv4 is not with expected format, skip."
                    continue
                }

                $nicData = New-Object PSObject -Property @{
                    Name = $nicName
                    IPv4Address = $nicIPv4
                }

                $nodeNics.$nicName = $nicData
                $networkMap.($node.Name) = $nodeNics
            }
        }
    }

    return $networkMap
}

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

        $PhysicalMachinesRole,

        [Boolean]
        $IsIdempotentRun = $true,

        [Parameter(Mandatory=$false)]
        [string] $UpdateNodeName
    )

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

    Trace-ECEScript "Waiting for Virtual Network to be configured on the host." {
        $cloudRole = $Parameters.Roles["Cloud"].PublicConfiguration
        $securityInfo = $cloudRole.PublicInfo.SecurityInfo
        $localAdminUser = $securityInfo.LocalUsers.User | ? Role -EQ $Parameters.Configuration.Role.PrivateInfo.Accounts.BuiltInAdminAccountID
        $credential = $Parameters.GetCredential($localAdminUser.Credential)

        # in the case of Bare Metal the hosts, it is possible that the DSC hasn't completed the configuration for the hosts at this point,
        # so add some retry logic to test the network connectivity
        $networkMap = Get-NetworkMap -Parameters $Parameters -PhysicalMachinesRole $PhysicalMachinesRole
        if ($UpdateNodeName) {
            Test-NetworkMap -NetworkMap $networkMap -RetryCount 10 -NodeNames @($UpdateNodeName)
        }
        else {
            Test-NetworkMap -NetworkMap $networkMap -RetryCount 10
        }

        $nodes = $PhysicalMachinesRole.Nodes.Node
        $nodeNames = $nodes | ForEach-Object Name
        $jobs = @()

        $isOneNode = $false
        if ($UpdateNodeName) {
            # For update, only one host is service at a time and this should not get confused with One Node MAS environment
            $nodes = $nodes | Where-Object Name -eq $UpdateNodeName
        }
        elseif ($nodeNames.Count -eq 1 ) {
            $isOneNode = $true
            # MASD One Node will always be run locally
            $nodeNames = 'LocalHost'
        }

        if ($IsIdempotentRun) {
            if (Get-IsVirtualNetworkAlreadyConfigured -NetworkMap $networkMap -IsOneNode $isOneNode) {
                return
            }
        }

        $allHostNodes = Get-NetworkMgmtIPv4FromECEForAllHosts -Parameters $Parameters
        if ($env:OSImageType -ne "ISO") {
            Trace-ECEScript "Start PowerShell jobs to retrieve and set the DSC state on all nodes $nodeNames." {
                foreach ($node in $nodes) {
                    # Collect information about the state of the recently deployed physical host.
                    $managementIpAddress = $allHostNodes[$node.Name]
                    $nodeName = $node.Name
                    Trace-Execution "Sending DSC query to $managementIpAddress for $nodeName"
                    $jobs += Start-Job -ScriptBlock {
                        $retVal = $false
                        $logText = "DSC report for node $(($using:Node).Name)`r`n"

                        # Wait as much as ten minutes for the exiting DSC configuration to complete. This is just a best effort.
                        for ($i = 0; $i -lt 40; $i++) {
                            $cimSession = $null
                            $result = $null
                            try {
                                # This call may fail if it can't connect to the remote host when remote host is configuring vmswitch and host vnic.
                                $cimSession = New-CimSession -ComputerName $Using:nodeName -Credential $using:credential -ErrorAction SilentlyContinue
                                if ($null -eq $cimSession) {
                                    $cimSession = New-CimSession -ComputerName $using:managementIpAddress -Credential $using:credential -ErrorAction Stop
                                }
                                # This call will throw exception if there is currently a DSC config running on the host.
                                # we use it as a way as an indication.
                                $logText += "Calling Get-DscConfigurationStatus at attempt #$i. `r`n"
                                $result = Get-DscConfigurationStatus -CimSession $cimSession -ErrorAction Stop
                                break
                            }
                            catch {
                                Start-Sleep -Seconds 15
                                $logText += "Get-DscConfigurationStatus failed with exception ($($_.Exception.Message)) at attempt #$i `r`n"
                            }
                            finally {
                                if ($cimSession) {
                                    Remove-CimSession $cimSession
                                }
                            }
                        }

                        $logText += "Trying to make sure host DSC configuration is completed. `r`n"
                        for ($i = 0; $i -lt 5; $i++) {
                            $cimSession = $null
                            try {
                                $cimSession = New-CimSession -ComputerName $Using:nodeName -Credential $using:credential -ErrorAction SilentlyContinue
                                if ($null -eq $cimSession) {
                                    $cimSession = New-CimSession -ComputerName $using:managementIpAddress -Credential $using:credential -ErrorAction Stop
                                }
                                $logText += "Calling Start-DscConfiguration at attempt #$i `r`n"
                                Start-DscConfiguration -UseExisting -Wait -CimSession $cimSession -Verbose -Force -ErrorAction Stop
                                $logText += "Calling Get-DscConfigurationStatus at attempt #$i. `r`n"
                                $result = Get-DscConfigurationStatus -CimSession $cimSession -ErrorAction Stop
                                $logText += "Calling Get-DscConfiguration at attempt #$i. `r`n"
                                $logText += (Get-DscConfiguration -CimSession $cimSession -Verbose -ErrorAction Stop | out-string)
                            }
                            catch {
                                # While DSC configuration on the host is running, it may cause the network connectivity to be disrupted.
                                # If that disruption last too long, say more than 3 or 4 minutes, the above calls may fail with exception.
                                # So catch the exception here but do nothing.
                                $logText += "Attempt #$i : failed with exception ($($_.Exception.Message)). `r`n"
                            }
                            finally {
                                if ($result.status -eq 'Success') {
                                    $retVal = $true
                                }

                                if ($cimSession) {
                                    Remove-CimSession $cimSession
                                }
                            }
                            if ($retVal) {
                                break
                            }
                        }

                        $retObj = @{RetVal = $retVal; LogText = $logText; HostIp = $using:managementIpAddress}
                        $retObj
                    }
                }
            }

            # Wait for all the nodes to either converge or fail to converge. If they failed to
            # converge, or if we failed to send the WSManAction to them, then use the more stateful
            # networking methods to figure out why, later.
            Trace-ECEScript "Waiting for DSC query to complete" {
                $results = Receive-Job -Wait -Job $jobs
                $results | ForEach-Object {
                    Trace-Execution $_.LogText
                    if ($_.RetVal -ne $true) {
                        Trace-Error "DSC failed to converge on a physical host $($_.HostIp)."
                    }
                }
            }

            $jobs | Remove-Job -ErrorAction SilentlyContinue
        }
        else {
            Trace-Execution "Skipping wait for DSC config as this is an ISO-based deployment."
        }
    }
}

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

    $domainIPMapping = @{}
    $deploymentMachineRoleNode = $Parameters.Roles["DeploymentMachine"].PublicConfiguration.Nodes.Node

    if($deploymentMachineRoleNode -ne $null)
    {
        $domainIPMapping.Add($deploymentMachineRoleNode.Name, $deploymentMachineRoleNode.IPv4Address)
    }

    return $domainIPMapping
}

# Get the node names from the ExecutionContext in the $Parameters variable
# if available; otherwise, return $null
function Get-ExecutionContextNodeName
{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters,

        [Parameter(Mandatory = $false)]
        [switch]
        $EnsureSingle
    )

    $nodeName = $null
    $executionRoleName = $Parameters.Context.ExecutionContext.Roles.Role.RoleName
    $nodeName = $Parameters.Context.ExecutionContext.Roles.Role.Nodes.Node.Name

    if ($null -ne $executionRoleName -and $null -eq $nodeName)
    {
        # If node names are not provided, get all nodes associated to the $executionRoleName
        $nodeName = ($Parameters.Roles[$executionRoleName].PublicConfiguration.Nodes.Node.Name) | Select-Object -Unique
    }

    Trace-Execution "Received ExecutionContext with role name $executionRoleName and node(s): $($nodeName -join ', ')"

    if ($EnsureSingle -and (!$nodeName -or ($nodeName.Count -ne 1)))
    {
        throw "Expected single node in ExecutionContext but found node(s): $($nodeName -join ', ')"
    }

    return $nodeName
}

# Get the cluster name from the ExecutionContext in the $Parameters variable
# if available; otherwise, return $null
function Get-ExecutionContextClusterName
{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

    $executionRoleName = $Parameters.Context.ExecutionContext.Roles.Role.RoleName
    $clusterName = $Parameters.Context.ExecutionContext.Roles.Role.Nodes.Node.ClusterName
    Trace-Execution "Received ExecutionContext with role name $executionRoleName and clusterName : $clusterName"

    return $clusterName
}

# Gets the names of the roles specified in the execution context.
function Get-ExecutionContextRoleName
{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

    # ExecutionContext can specify RolePath and/or RoleName attributes. Since we want just the names
    # we truncate the role paths if they are the only attribute provided, and return a list of the unique role names.
    $executionRoleName = @( $Parameters.Context.ExecutionContext.Roles.Role.RoleName )
    $executionRolePath = @( $Parameters.Context.ExecutionContext.Roles.Role.RolePath )

    $executionRoleName += $executionRolePath | Split-Path -Leaf
    return ($executionRoleName | Select-Object -Unique)
}

# Determines whether the environment is one-node from the interface parameters.
function IsOneNode
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

    return @($Parameters.Roles["Storage"].PublicConfiguration.Nodes.Node).Count -eq 1
}

# Determines whether the environment is virtual from the interface parameters.
function IsVirtualAzureStack
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

    $oemModel = $Parameters.Roles["OEM"].PublicConfiguration.PublicInfo.UpdatePackageManifest.UpdateInfo.Model
    $supportedModelList = $Parameters.Roles["OEM"].PublicConfiguration.PublicInfo.UpdatePackageManifest.UpdateInfo.SupportedModels.SupportedModel
    if ($null -eq $supportedModelList)
    {
        # The model string reported by BIOS and recorded by ExtractOEMContent.ps1 will still have spaces and special characters
        return (@("Virtual Machine", "Hyper-V") -contains $oemModel)
    }
    else
    {
        # The supported model list will have had spaces and special characters removed
        return ($supportedModelList -contains "VirtualMachine")
    }
}

# This function checks whether the deployment is running on Virtual Machine by system prams
function IsOnVirtualMachine
{
    try
    {
        # is on VM
        $computerSystem = Get-WmiObject win32_computersystem
        $isOnVM = (('Microsoft Corporation' -eq $computerSystem.Manufacturer) -and ('Virtual Machine' -eq  $computerSystem.Model))
    }
    catch
    {
        Trace-Error "Error while checking system info for Virtual Machine deployment"
        return $false
    }

    return $isOnVM
}

# Checks the RunInformation for UpdateVersion. Returns the value if found else returns null
function Get-InProgressUpdateVersion
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

    $version = $null
    Trace-Execution "Getting in progress Update Version."
    # In case of update, run time information will be populated
    if($Parameters.RunInformation -ne $null)
    {
        $runtimeParameters = $Parameters.RunInformation['RuntimeParameter']

        if( $runtimeParameters -ne $null -and $runtimeParameters.ContainsKey('UpdateVersion') )
        {
            $version = $runtimeParameters["UpdateVersion"]
            Trace-Execution "Version of the update being applied : $version"
            return $version
        }
    }

    # If it is a one node or bootstrap VM return null as an update will not run on it.
    $buildLocally = $Parameters.Context.ExecutionContext.BuildVhdLocally -and [bool]::Parse($Parameters.Context.ExecutionContext.BuildVhdLocally)
    $RoleName = $Parameters.Configuration.Role.Id
    $isOneNode = @($Parameters.Roles["BareMetal"].PublicConfiguration.Nodes.Node).Count -eq 1
    if(($isOneNode -eq $true) -or ($buildLocally -and ($RoleName -eq "Domain")))
    {
        return $version
    }

    # If the version is set to null, check if share has update version info file.
    # This file only exists while a host is being updated.
    if($version -eq $null)
    {
        $bareMetalRole = $Parameters.Roles["BareMetal"].PublicConfiguration
        $storageClusterName = Get-ManagementClusterName $Parameters
        if($bareMetalRole.PublicInfo.UpdateInformation -ne $null)
        {
            $updateInfoFolderPath = Get-SharePath $Parameters $bareMetalRole.PublicInfo.UpdateInformation.UpdateVersionFile.FolderName $storageClusterName
            $updateInfoFileName = $bareMetalRole.PublicInfo.UpdateInformation.UpdateVersionFile.FileName
            $filePath = Join-Path -Path $updateInfoFolderPath -ChildPath $updateInfoFileName
            Trace-Execution "Check if version file $filePath exists"
            if(Test-Path -Path $filePath)
            {
                Trace-Execution "The version file is present. Extracting version number and override status from it"
                $versionFileContent = [xml] ( Get-Content -Path $filePath )
                if($versionFileContent)
                {
                    $versionValue = $versionFileContent.InProgressUpdateVersion.Version
                    $useThisVersion = $versionFileContent.InProgressUpdateVersion.UseThisVersion
                    if($useThisVersion -eq "True")
                    {
                        $version = $versionValue
                    }
                    else
                    {
                        Trace-Error "The version file exists but is not being used as UseThisVersion flag is set to False. This flag should be set to True if FRU is being used to recover from an update failure"
                    }
                }
                else
                {
                    Trace-Error "Version file found at $filePath but has invalid content"
                }
            }
        }
    }

    return $version
}

# Gets the file share directory containing expanded contents of a currently in-progress update package.
function Get-InProgressUpdatePackagePath
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )
    $runtimeParameters = $Parameters.RunInformation['RuntimeParameter']
    $packagePath = $runtimeParameters["UpdatePackagePath"]
    if (-not $packagePath)
    {
        Trace-Execution "The UpdatePackagePath runtime parameter is not present."
        return $null
    }

    Trace-Execution "Upgrade package @: [$packagePath]"

    # The package path is expected to be a full path to an update package file (e.g. a self-
    # extracting .exe, or a metadata.xml file). The path where the contents are extracted is
    # the directory containing this file.
    return (Split-Path $packagePath)
}

# Gets the location of the folder containing nugets in the currently in-progress update package.
function Get-InProgressUpdateNugetStore
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

    $updatePackagePath = Get-InProgressUpdatePackagePath -Parameters $Parameters
    if ($updatePackagePath)
    {
        $urpRole = $Parameters.Roles["URP"].PublicConfiguration
        $nugetSourceFolderName = ($urpRole.PublicInfo.UpdatePackagePaths.ContentMappings.ContentMapping | Where-Object { $_.Name -eq "Nugets" }).SourceFolder

        $nugetStorePath = Join-Path $updatePackagePath $nugetSourceFolderName
        if (Test-Path $nugetStorePath)
        {
            Trace-Execution "Found update nuget store at $nugetStorePath."
            return $nugetStorePath
        }

        # Fallback: check for legacy nuget store path.
        $nugetSourceFolderName = ($urpRole.PublicInfo.UpdatePackagePaths.ContentMappings.ContentMapping | Where-Object { $_.Name -eq "NugetsLegacy" }).SourceFolder
        $nugetStorePath = Join-Path $updatePackagePath $nugetSourceFolderName
        if (Test-Path $nugetStorePath)
        {
            Trace-Execution "Found legacy update nuget store at $nugetStorePath."
            return $nugetStorePath
        }

        Trace-Execution "No nuget store was found at update package path: $updatePackagePath."
    }
    else
    {
        Trace-Execution "No update is currently in progress."
    }

    return $null
}

<#
.SYNOPSIS
Expands the deployment artifacts to the destination location.
 
.DESCRIPTION
Expands all deployment artifacts into the destination root path.
 
.EXAMPLE
Expand-DeploymentArtifacts -DeploymentContentNode $Parameters.Configuration.Role.PrivateInfo.DeploymentContent -DestinationRootPath "C:"
 
.PARAMETER DeploymentContentNode
The xml node containing the deployment content to deliver to the destination.
 
.PARAMETER DestinationRootPath
The root path under which to deliver the nuget content.
#>

function Expand-DeploymentArtifacts
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [System.Xml.XmlElement]
        $DeploymentContentNode,

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

        [Parameter(Mandatory=$false)]
        [string]
        $NugetStorePath = "$env:SystemDrive\CloudDeployment\NuGetStore"
    )

    $nugetDeploymentContentItems = $DeploymentContentNode.NugetArtifact

    $installRolePackages = $false
    $rolePackageDestination = Join-Path $DestinationRootPath 'NugetStore'
    foreach( $deployContent in $nugetDeploymentContentItems )
    {
        Write-Verbose -Verbose "Copying content from nuget $($deployContent.Name) into $DestinationRootPath"
        $destination = Join-Path $DestinationRootPath $deployContent.DestinationPath

        $sourceIsFile = [bool]$deployContent.SourceIsFile
        $isNugetInstall = [bool]$deployContent.NugetInstall

        if($isNugetInstall)
        {
            $installRolePackages = $true
        }

        Write-Verbose -Verbose "$($deployContent.SourcePath) will be copied to $destination"
        Write-Verbose -Verbose "IsNugetInstall : $isNugetInstall"
        Expand-NugetContent -NugetName $deployContent.Name -SourcePath $deployContent.SourcePath -DestinationPath $destination -SourceIsFile:$sourceIsFile -Verbose:$VerbosePreference -NugetStorePath $NugetStorePath -IsNugetInstall:$isNugetInstall
        Write-Verbose -Verbose "Nuget $($deployContent.Name) with source [$($deployContent.SourcePath)] copied to $destination"
    }

    if($installRolePackages)
    {
        Write-Verbose -Verbose "Install Role Nuget packages to NugetStore"
        Install-RoleNugetPackages -NugetStorePath $NugetStorePath -Destination $rolePackageDestination
    }
}

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

        [Parameter(Mandatory=$false)]
        [string[]]
        $ExecutionRoleName,

        [Parameter(Mandatory=$false)]
        [switch]
        $DSCOnly,

        [Parameter(Mandatory=$false)]
        [switch]
        $EngineUpdateOnly,

        [Parameter(Mandatory=$false)]
        [string[]]
        $NodeName
    )

    $currentContext = $Parameters.Context.ExecutionContext
    if(-not $ExecutionRoleName)
    {
        $ExecutionRoleName = $currentContext.Roles.Role.RoleName
    }

    $vmRole = $Parameters.Roles["VirtualMachines"].PublicConfiguration

    $localExecutionHost = $env:COMPUTERNAME
    $clusterName = Get-ManagementClusterName $Parameters
    $allManagementHosts = Get-ManagementClusterNodes $Parameters

    $uncLibraryShareNugetStorePath = Get-SharePath $Parameters $vmRole.PublicInfo.LibraryShareNugetStoreFolder.Path $clusterName

    # During AsZ deployment expand nugets on all the nodes part of cluster
    # Cluster path at this time doesn't exist so use the absolute path
    if (!(Test-Path -Path $uncLibraryShareNugetStorePath))
    {
        Trace-Execution "AsZ deployment scenario so use absolute path on SeedNode"
        $uncLibraryShareNugetStorePath = "$env:SystemDrive\CloudDeployment\NuGetStore"
    }

    $localLibraryShareNugetStorePath = $null
    # Bug 15111454: Waiting for fix it should not block deployment
    # if(Test-LocalHostHasClusterStoragePath -Parameters $Parameters)
    # {
    # Trace-Execution "Getting local CSV path for $uncLibraryShareNugetStorePath"
    # $localLibraryShareNugetStorePath = Get-LocalCsvPathFromSharePath -Parameters $Parameters -UNCSharePath $uncLibraryShareNugetStorePath
    # }

    Trace-Execution "uncLibraryShareNugetStorePath: $uncLibraryShareNugetStorePath"
    Trace-Execution "localLibraryShareNugetStorePath: $localLibraryShareNugetStorePath"

    $systemDrive = $env:SystemDrive -Replace (':','$')
    $healthyNodes = $Parameters.Roles["VirtualMachines"].PublicConfiguration.Nodes.Node | where { $_.ProvisioningStatus -eq "Complete" } | % Name
    $healthyNodes += $Parameters.Roles["BareMetal"].PublicConfiguration.Nodes.Node | where { $_.ProvisioningStatus -eq "Complete" } | % Name

    foreach ($roleName in $ExecutionRoleName)
    {
        Trace-Execution "Expand-UpdateContent adding update contents for role: $roleName"
        $roleConfiguration = $Parameters.Roles[$roleName].PublicConfiguration
        if ($NodeName)
        {
            $nodes = $NodeName
        }
        else
        {
            $nodes = $roleConfiguration.Nodes.Node.Name |where { $healthyNodes.Contains($_)}
        }

        Trace-Execution "Expand-UpdateContent for Nodes : $($nodes)"

        # Skipping nodes which are provided in runtime parameters
        $nodes = Skip-NodesProvidedInRuntimeParameter -Parameters $Parameters -Nodes $nodes

        Trace-Execution "Expand-UpdateContent for Nodes (after filtering) : $($nodes)"

        # Get update content for the role
        $nugetUpdateContentItems = $roleConfiguration.PublicInfo.DeploymentContent.NugetArtifact | where {$_.Update -eq "True" -or $_.EngineUpdate -eq "True"}

        $allNodesNameIpMappingInfo = Get-NetworkMgmtIPv4FromECEForAllHosts -Parameters $Parameters
        # Copy the update content
        if ($nugetUpdateContentItems)
        {
            foreach( $updateContent in $nugetUpdateContentItems )
            {
                if($DSCOnly -and ($updateContent.DSCContent -ne "True"))
                {
                    Trace-Execution "$($updateContent.Name) is not marked as nuget with DSC content. Skipping update on the target VM(s) "
                    continue
                }
                elseif ($EngineUpdateOnly -and ($updateContent.EngineUpdate -ne "True"))
                {
                    Trace-Execution "$($updateContent.Name) is not marked as nuget with engine content. Skipping update on the target VM(s)."
                    continue
                }

                foreach ($node in $nodes)
                {
                    $libraryShareNugetStorePath = $null
                    if (($node -in $allManagementHosts) -and ($null -ne $localLibraryShareNugetStorePath))
                    {
                        Trace-Execution "For node $node - member the management cluster - will use local nuget store path $localLibraryShareNugetStorePath"
                        $libraryShareNugetStorePath = $localLibraryShareNugetStorePath
                    }
                    else
                    {
                        Trace-Execution "For node $node will use UNC nuget store path $uncLibraryShareNugetStorePath"
                        $libraryShareNugetStorePath = $uncLibraryShareNugetStorePath
                    }

                    Trace-Execution "Start process to expand nuget content:"
                    Trace-Execution "Local execution host : $localExecutionHost"
                    Trace-Execution "Nuget store Path : $libraryShareNugetStorePath"
                    Trace-Execution "Processing node : $node"

                    $nodeIp = $allNodesNameIpMappingInfo[$node]

                    $DestinationRootPath = "\\$nodeIp\$systemDrive\"
                    if ($updateContent.UpdatePath)
                    {
                        $destination = Join-Path $DestinationRootPath $updateContent.UpdatePath
                    }
                    else
                    {
                        $destination = Join-Path $DestinationRootPath $updateContent.DestinationPath
                    }

                    $isNugetInstall = $null
                    [bool]::TryParse($updateContent.NugetInstall, [ref]$isNugetInstall)

                    Expand-NugetContent -NugetName $updateContent.Name -SourcePath $updateContent.SourcePath -DestinationPath $destination -IsUnc -NugetStorePath $libraryShareNugetStorePath -IsNugetInstall:$isNugetInstall
                    Write-Verbose -Verbose "$($updateContent.SourcePath) copied to $destination"
                }
            }
        }
    }
}

#execute to a script block over a VMGroup using PSDirect instead of WinRM
function Invoke-PSDirectOnVM
{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters,

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

        [Parameter(Mandatory = $true)]
        [Object]$VMCredential,

        [Parameter(Mandatory = $true)]
        [ScriptBlock]$ScriptBlock,

        [Object[]]$ArgumentList = $null
    )
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    #retrieve the environment info
    $allPhysicalMachines = Get-ActiveClusterNodes $Parameters

    # Domain Info
    $domainAdminCredential = Get-DomainCredential -Parameters $Parameters

    # for each physical host, iterate over all the infra VMs that are part of it
    $hostTrace = @()
    foreach ($phyHost in $allPhysicalMachines) {
        Trace-Execution "For Host $($phyHost)"

        # test if the target host is reachable
        $output = $false
        $output = Test-WSMan -ComputerName $phyHost -ErrorAction:Ignore
        if (-not $output)
        {
            Write-Host "Host $($phyHost) not responding..."
            continue
        }
        $localHostTrace = Invoke-Command -ComputerName $phyHost -Credential $domainAdminCredential -ScriptBlock {
            $vmTrace = @()
            $thrownExceptions = @()
            # get the infra VMs in this physical node
            $retries = 5
            for($retry = 0; $retry -lt $retries; ++$retry)
            {
                try
                {
                    # Unfiltered calls to Get-VM may fail if a VM is being deleted from that host
                    $localVMs = (Get-VM | ? Name -In $using:VMNames).Name
                    break
                }
                catch
                {
                    if ($retry -eq $retries - 1)
                    {
                        throw
                    }
                }
            }
            if($localVMs) {
                Trace-Execution "Executing on local VMs $($localVMs -join ',')"
                try {
                    # change PS language mode to FULL to allow script block invocation
                    if (!$using:ArgumentList) {
                        $vmTrace += Invoke-Command -VMName $localVMs -Credential $using:VMCredential -ScriptBlock ([ScriptBlock]::Create($("Import-Module OpenUpSession; Set-FullLanguage; `r`n" + $using:ScriptBlock)))
                    }
                    # check if the scriptblock declares Param because special handling is needed
                    elseif ($using:ScriptBlock -match "param[ ]*\(|param[ ]*\r\n")
                    {
                        $sb = {param($ScriptBlock, [Object[]]$ArgumentList) Import-Module OpenUpSession; Set-FullLanguage; [scriptBlock]::Create($ScriptBlock).invoke($ArgumentList)}
                        $vmTrace += Invoke-Command -VMName $localVMs -Credential $using:VMCredential -ScriptBlock $sb -ArgumentList $using:ScriptBlock, $using:ArgumentList
                    }
                    else
                    {
                        $vmTrace += Invoke-Command -VMName $localVMs -Credential $using:VMCredential -ArgumentList $using:ArgumentList -ScriptBlock ([ScriptBlock]::Create($("Import-Module OpenUpSession; Set-FullLanguage; `r`n" + $using:ScriptBlock)))
                    }
                } catch {
                    Write-Host "Local exception: $($_)"
                    $thrownExceptions += $_
                }
            }
            if ($vmTrace)
            {
                Write-Host "VM(s) returned: $vmTrace"
            }
            if ($thrownExceptions.Count -gt 0) {
               throw $thrownExceptions
            }
            return $vmTrace
        }
        if ($localHostTrace)
        {
            Trace-Execution "Local Host Output: $($localHostTrace)"
        }
        $hostTrace += $localHostTrace
    }
    return $hostTrace
}

function Invoke-ScriptBlockWithRetries
{
    [CmdletBinding(DefaultParameterSetName="All")]
    param(
        [ScriptBlock]
        [parameter(Mandatory = $true)]
        $ScriptBlock,

        [Int32]
        [parameter(Mandatory = $false)]
        $RetryTimes = 10,

        [Int32]
        [parameter(Mandatory = $false)]
        $RetrySleepTimeInSeconds = 10,

        [Switch]
        [parameter(Mandatory = $false)]
        $InDisconnectedSession=$false,

        [string]
        [parameter(Mandatory = $false, ParameterSetName="ByComputerName")]
        $ComputerName = $null,

        [PSCredential]
        [parameter(Mandatory = $false)]
        $Credential = $null,

        [parameter(Mandatory = $false, ParameterSetName="BySession")]
        $Session = $null,

        [parameter(Mandatory = $false)]
        $ArgumentList = $null
    )
    $ErrorActionPreference = "Stop"

    for($retry = 0; $retry -lt $RetryTimes; ++$retry)
    {
        try
        {
            Write-Verbose "Invoke-ScriptBlockWithRetries: retry #$retry of $RetryTimes, retry sleep time is $RetrySleepTimeInSeconds seconds." -Verbose

            $invokeParams = @{
            }

            $noNewScope = $true
            if ($ComputerName)
            {
                $invokeParams.ComputerName = $ComputerName
                $noNewScope = $false
            }
            elseif ($Session)
            {
                $invokeParams.Session = $Session
                $noNewScope = $false
            }

            if ($Credential)
            {
                $invokeParams.Credential = $Credential
                $noNewScope = $false
            }

            if ($noNewScope)
            {
                $invokeParams.NoNewScope = $true
            }

            if ($InDisconnectedSession)
            {
                $invokeParams.InDisconnectedSession = $true
            }

            if ($ArgumentList)
            {
                $invokeParams.ArgumentList = $ArgumentList
            }

            $invokeParams.ScriptBlock = $ScriptBlock

            return Invoke-Command @invokeParams
        }
        catch
        {
            Write-Verbose "Invoke-ScriptBlockWithRetries failed with exception: $_" -Verbose

            if ($retry -lt ($RetryTimes - 1))
            {
                Start-Sleep -Seconds $RetrySleepTimeInSeconds
            }
            else
            {
                throw
            }
        }
    }
}

function Invoke-ScriptBlockInParallel
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string[]]
        $ComputerNames,

        [Parameter(Mandatory=$false)]
        [PSCredential]
        $Credential,

        [ScriptBlock]
        [parameter(Mandatory = $true)]
        [ValidateScript({$_ -NotLike '*$Using:*'})]
        $ScriptBlock,

        [Object[]]
        [parameter(Mandatory = $false)]
        $ArgumentList = $null,

        [Int32]
        [parameter(Mandatory = $false)]
        $RetryTimes = 2,

        [Int32]
        [parameter(Mandatory = $false)]
        $RetrySleepTimeInSeconds = 5,

        [Parameter(Mandatory = $false)]
        [string]
        $Authorization = 'Default'
    )

    $ErrorActionPreference = 'Stop'

    $computerNames = $ComputerNames
    $jobs = @()
    $sessions =@()
    $errorMessages = @()

    # Just retry for kicking off the ScriptBlock in Parallel
    for ($retry = 0; $retry -lt $RetryTimes; ++$retry)
    {
        $retryComputers = @()
        $session = $null
        $job = $null

        Trace-Execution "Invoke-ScriptBlockInParallel: retry #$retry of $RetryTimes, retry sleep time is $RetrySleepTimeInSeconds seconds, machines are $computerNames."

        foreach ($computerName in $computerNames)
        {
            try
            {
                if($Credential)
                {
                    $session = New-PSSession -ComputerName $computerName -Credential $Credential -Authentication $Authorization
                }
                else
                {
                    $session = New-PSSession -ComputerName $computerName
                }

                Initialize-ECESession -Session $session
                $job = Invoke-Command -ErrorAction Stop -Session $session -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -AsJob
                $sessions += $session
                $jobs += $job
            }
            catch
            {
                $retryComputers += $computerName
                $errorMessage = "Machine $computerName failed with error: $($_.Exception.Message)"
                $errorMessages +=  $errorMessage

                if ($session -ne $null)
                {
                    $session | Remove-PSSession -ErrorAction Ignore
                }

                if ($job -ne $null)
                {
                    $job | Remove-Job -ErrorAction Ignore
                }
            }
        }

        if (($retryComputers.Count -eq 0) -or ($retry -ge ($RetryTimes -1)))
        {
            Trace-Execution "Invoke-ScriptBlockInParallel exits from retries for kicking off jobs in $retry retries"
            break
        }

        $computerNames = $retryComputers
        Start-Sleep -Second $RetrySleepTimeInSeconds
        $errorMessages = @()
    }

    # Waiting for all jobs to finish
    try
    {
        if ($jobs.Count -eq 0)
        {
            $errorMessage = "Invoke-ScriptBlockInParallel failed: Script could not be invoked after $RetryTimes retries"
            $errorMessages +=  $errorMessage
            throw "Invoke-ScriptBlockInParallel failed: $errorMessages"
        }

        Trace-Execution "Invoke-ScriptBlockInParallel: Waiting for jobs"

        $jobs | Wait-Job | Receive-Job

        Trace-Execution "Invoke-ScriptBlockInParallel: finished jobs"

        $failedJobs =  $jobs | ? { $_.JobStateInfo.State -ne "Completed" }

        foreach ($failedJob in $failedJobs)
        {
            $newErrorMessage = $null
            try
            {
                $newErrorMessage = $failedJob.ChildJobs[0].JobStateInfo.Reason.Message
            }
            catch
            {
            }

            $errorMessages += "Machine $($failedJob.Location): Failed with error: $newErrorMessage"
        }

        if ($errorMessages.Count -gt 0)
        {
            throw "Invoke-ScriptBlockInParallel failed: $errorMessages"
        }
    }
    finally
    {
        $sessions | Remove-PSSession -ErrorAction Ignore
        $jobs | Remove-Job -ErrorAction Ignore
    }
}

<#
.SYNOPSIS
    Create a folder on management file share for host update
 
.DESCRIPTION
    This function creates a folder on management file share for host update
 
.PARAMETER Parameters
    This object is based on the customer manifest. It contains the private information and public
    information of cloud role. This Parameters object is passed down by the deployment engine.
 
#>

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

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

    Trace-ECEScript "Getting the host update share." {
        $physicalMachinesRole = $Parameters.Roles["BareMetal"].PublicConfiguration

        $clusterName = Get-ManagementClusterName $Parameters
        $managementLibraryPath = Get-SharePath $Parameters $physicalMachinesRole.PublicInfo.WindowsUpdateStagingFolder.Path $clusterName
        $managementLibraryPath = Split-Path -Path $managementLibraryPath

        $hostUpdateLogShare = Join-Path $managementLibraryPath 'Logs\HostUpdate'

        if(-not (Test-Path $hostUpdateLogShare))
        {
            Trace-Execution " Create a folder on management file share $hostUpdateLogShare "
            try
            {
                New-Item $hostUpdateLogShare -ItemType Directory -Force | Out-Null
            }
            catch
            {
                throw "Failed to create folder on management file share $hostUpdateLogShare. Error $_"
            }
        }
    }

    return $hostUpdateLogShare
}

# Returns the list of orchestrators that are currently available.
function Get-CurrentOrchestrators
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

    # Check if the code is running on DVM
    if($Parameters.Roles["DeploymentMachine"])
    {
        $dvmName = $Parameters.Roles["DeploymentMachine"].PublicConfiguration.Nodes.Node | % Name
        if($env:ComputerName -eq $dvmName)
        {
            $dvmIpAddress = $Parameters.Roles["DeploymentMachine"].PublicConfiguration.Nodes.Node | % IPv4Address
            $orchestrator = New-Object -TypeName psobject -Property @{ Name = $dvmName; IPAddress = $dvmIpAddress }
            return @($orchestrator)
        }
    }
    else
    {
        # Check if we only have one host. In that case the code should be running on the same host or the DVM (if performing bare metal POC)
        $physicalMachinesRole = $Parameters.Roles["BareMetal"].PublicConfiguration
        $allPhysicalHosts = @($physicalMachinesRole.Nodes.Node | % Name)
        if( ($allPhysicalHosts.Count -eq 1) -and ($allPhysicalHosts -eq $env:ComputerName) )
        {
            $orchestrator = New-Object -TypeName psobject -Property @{ Name = $env:ComputerName }
            return @( $orchestrator)
        }
    }

    # If the orchestrator is not the DVM or host, then return ERCS VM information
    $orchestrators = @()
    $virtualMachineNodes = $Parameters.Roles["VirtualMachines"].PublicConfiguration.Nodes
    $ercsNodes = $virtualMachineNodes.Node | where { $_.Role -eq "SeedRingServices"}

    foreach($ercsNode in $ercsNodes)
    {
        $name = $ercsNode.Name
        $ercsIpAddress = ($ercsNode.NICs.NIC.IPv4Address -split "/")[0]
        $orchestrator = New-Object -TypeName psobject -Property @{ Name = $name; IPAddress = $ercsIpAddress}
        $orchestrators += $orchestrator
    }
    return @($orchestrators)
}

# Note: This is a temporary implementation to unblock Image Based update
function Update-JEAEndpointsForUpdate {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

    # Update PXE endpoint - Remove this once PXE VM has an updated endpoint.
    # PXE Role
    Trace-Execution "Start - Update module needed by PXEEndpoint on PXE VM."
    $pxeRole = $Parameters.Roles["PXE"].PublicConfiguration
    $pxeServerName = $pxeRole.Nodes.Node.Name

    # PhysicalMachine Role
    $physicalMachinesRole = $Parameters.Roles["BareMetal"].PublicConfiguration
    $allPhysicalHosts = $physicalMachinesRole.Nodes.Node | % Name

    # Skipping nodes which are provided in runtime parameters
    $allPhysicalHosts = Skip-NodesProvidedInRuntimeParameter -Parameters $Parameters -Nodes $allPhysicalHosts

    # VirtualMachine Role
    $virtualMachinesRole = $Parameters.Roles["VirtualMachines"].PublicConfiguration

    $clusterName = Get-ManagementClusterName $Parameters
    $libraryShareNugetStorePath = Get-SharePath $Parameters $virtualMachinesRole.PublicInfo.LibraryShareNugetStoreFolder.Path $clusterName

    $tempNugetDir = Join-Path $env:temp 'AzureStackPXE.Deployment'
    Trace-Execution "Expanding Nuget Microsoft.AzureStack.Solution.Deploy.AzureStackPXE.Deployment at $tempNugetDir"
    Expand-NugetContent -NugetName 'Microsoft.AzureStack.Solution.Deploy.AzureStackPXE.Deployment' `
                        -NugetStorePath $libraryShareNugetStorePath `
                        -SourcePath 'content\Scripts' `
                        -DestinationPath $tempNugetDir

    $sourcePath = "$tempNugetDir\*"
    $destinationPath = "\\$pxeServerName\c$\Program Files\WindowsPowerShell\Modules\Microsoft.AzureStack.Solution.Deploy.AzureStackPXE.Deployment\Scripts"
    Trace-Execution "Copying $sourcePath to $destinationPath"
    New-Item -Path $destinationPath -ItemType Directory -Force -ErrorAction SilentlyContinue
    Copy-Item -Path $sourcePath -Destination $destinationPath -Force
    Remove-Item -Force -Path $tempNugetDir -Recurse

    Trace-Execution "Completed - Update module needed by PXEEndpoint on PXE VM."

    Trace-Execution "Start - Update module needed by NewManagementVMEndpoint on all the hosts"

    $tempNugetDir = Join-Path $env:temp 'NodeUtils_NewManagementVM'
    Expand-NugetContent -NugetName 'Microsoft.AzureStack.Solution.Deploy.Common.NodeUtils' `
                        -NugetStorePath $libraryShareNugetStorePath `
                        -SourcePath 'content\NewManagementVM' `
                        -DestinationPath $tempNugetDir

    foreach ($nodeName in $allPhysicalHosts)
    {
        $sourcePath = "$tempNugetDir\*"
        $destinationPath = "\\$nodeName\c$\Program Files\WindowsPowerShell\Modules\NewManagementVM"
        Trace-Execution "Copying $sourcePath to $destinationPath"
        New-Item -Path $destinationPath -ItemType Directory -Force -ErrorAction SilentlyContinue
        Copy-Item -Path $sourcePath -Destination $destinationPath -Force
    }

    Remove-Item -Force -Path $tempNugetDir -Recurse

    Trace-Execution "Completed - Update module needed by NewManagementVMEndpoint on all the hosts."
}

# Returns credentials needed for various operations during deployment and update.
function Get-DomainCredential
{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

    $cloudRole = $Parameters.Roles["Cloud"].PublicConfiguration
    $domainRole = $Parameters.Roles["Domain"].PublicConfiguration
    $securityInfo = $cloudRole.PublicInfo.SecurityInfo
    $domainAdminUser = $securityInfo.DomainUsers.User | ? Role -EQ "DomainAdmin"
    $domainAdminCredential = $Parameters.GetCredential($domainAdminUser.Credential)
    $domainAdminName = $domainAdminCredential.GetNetworkCredential().UserName
    $domainAdminPassword = $domainAdminCredential.GetNetworkCredential().Password
    $domainFqdn = $domainRole.PublicInfo.DomainConfiguration.FQDN
    $domainCredential = New-Credential "$domainFqdn\$domainAdminName" $domainAdminPassword

    return $domainCredential
}

# Returns Temporary Domain Admin credentials.
function Get-TemporaryDomainCredential
{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )
    $domainAdminCredential = Get-UserAccountCredential -Parameters $Parameters
    return $domainAdminCredential
}

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

        [Parameter(Mandatory=$false)]
        [string]
        $UserRole = "DomainAdmin"
    )

    $cloudRole = $Parameters.Roles["Cloud"].PublicConfiguration
    $domainRole = $Parameters.Roles["Domain"].PublicConfiguration
    $securityInfo = $cloudRole.PublicInfo.SecurityInfo
    $UserAccount = $securityInfo.DomainUsers.User | ? Role -EQ $UserRole
    $UserAccountCredential = $Parameters.GetCredential($UserAccount.Credential)
    $domainName = $domainRole.PublicInfo.DomainConfiguration.DomainName
    if(-not $("$($UserAccountCredential.UserName)" -like "*\*") -and [String]::IsNullOrEmpty($DomainName) -eq $false)
    {
        $UserAccountCredential = New-Credential -UserName "$domainName\$($UserAccountCredential.UserName)" -Password $UserAccountCredential.GetNetworkCredential().Password
    }
    return $UserAccountCredential
}

# Update Windows DISM features based on the configuration of Role.xml
# Including installing features and removiing unused features
function Update-WindowsDismFeature
{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $false)]
        [object]
        $WindowsFeature,

        [Parameter(Mandatory = $false)]
        [Switch]
        $Online,

        [Parameter(Mandatory = $false)]
        [string]
        $Path,

        [Parameter(Mandatory = $false)]
        [string]
        $WinSxSPath = "$env:windir\WinSxS"
    )

    Trace-Execution "Update Windows DISM features"

    if ($null -ne $WindowsFeature)
    {
        if ($Online)
        {
            $dismFeatures = (Get-WindowsOptionalFeature -Online).FeatureName
        }
        else
        {
            $dismFeatures = (Get-WindowsOptionalFeature -Path $Path).FeatureName
        }

        if ($null -ne $WindowsFeature.Feature.Name)
        {
            $featuresToInstall = $dismFeatures | Where-Object { $_ -in $WindowsFeature.Feature.Name }
            if ($null -ne $featuresToInstall -and $featuresToInstall.Count -gt 0)
            {
                Trace-Execution "Install Windows DISM features: $featuresToInstall"
                if ($Online)
                {
                    Enable-WindowsOptionalFeature -FeatureName $featuresToInstall -Online -All -Source $WinSxSPath -NoRestart
                }
                else
                {
                    Enable-WindowsOptionalFeature -FeatureName $featuresToInstall -Path $path -All
                }
            }
        }

        if ($null -ne $WindowsFeature.RemoveFeature.Name)
        {
            $featuresToRemove = $dismFeatures | Where-Object { $_ -in $WindowsFeature.RemoveFeature.Name }
            if ($null -ne $featuresToRemove -and $featuresToRemove.Count -gt 0)
            {
                Trace-Execution "Remove unused Windows DISM features: $featuresToRemove"
                if ($Online)
                {
                    Disable-WindowsOptionalFeature -FeatureName $featuresToRemove -Online -NoRestart
                }
                else
                {
                    Disable-WindowsOptionalFeature -FeatureName $featuresToRemove -Path $Path
                }
            }
        }
    }
}

<#
.SYNOPSIS
#### Reference from nuget in 1907
Returns a structured object containing information about each of the exceptions in an update summary
#>

function Read-SummaryXml {
    Param (
        [parameter(Mandatory = $false)][string] $RoutingJsonPath = $RoutingJsonSharePath,
        [parameter(Mandatory = $false, ParameterSetName = 'Path')][string] $SummaryXMLPath,
        [parameter(Mandatory = $false, ParameterSetName = 'Xml')][xml] $SummaryXML,
        [parameter(Mandatory = $false)][switch] $Routing
    )

    if ($SummaryXMLPath)
    {
        $SummaryXML = Get-Content "$SummaryXMLPath" -ErrorAction Stop
    }

    $Exceptions = $SummaryXML.SelectNodes(".//Exception")

    $returnInformation = @()

    foreach ($exception in $Exceptions)
    {
        if ($exception.ParentNode.RolePath)
        {
            $RolePath = $exception.ParentNode.RolePath | select -first 1
            $Interface = $exception.ParentNode.InterfaceType | select -first 1
            $ExecutionContextRolePath = $null
            $ExecutionContextNode = ""
            $ActionType = $null
            $ActionRolePath = ""
            $curAncestor = $exception
            while ($curAncestor -and (!$ExecutionContextRolePath -or !$ActionType))
            {
                if (!$ExecutionContextRolePath)
                {
                    $ExecutionContextRolePath = $curAncestor.ExecutionContext.Roles.Role.RolePath | select -first 1
                    $ExecutionContextNode = $curAncestor.ExecutionContext.Roles.Role.Nodes.Node.Name | select -first 1
                }

                if (!$ActionType)
                {
                    $ActionType = $curAncestor.ActionType
                    $ActionRolePath = $curAncestor.RolePath
                }

                $curAncestor = $curAncestor.ParentNode
            }

            $returnInformation += @{
                'Exception' = $exception.Raw
                'ExecutionContextNode' = $ExecutionContextNode
                'ExecutionContextRolePath' = $ExecutionContextRolePath
                'RolePath' = $RolePath
                'InterfaceType' = $Interface
                'ActionType' = $ActionType
                'ActionRolePath' = $ActionRolePath
                }
        }
    }

    $returnInformation
}

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

    $runtimeParameters = $null
    $failureList = $null
    if($Parameters.RunInformation -ne $null -and $Parameters.RunInformation.ContainsKey('RuntimeParameter'))
    {
        $runtimeParameters = $Parameters.RunInformation['RuntimeParameter']
    }
    if($runtimeParameters -ne $null -and $runtimeParameters.ContainsKey('FailureSummaryXml'))
    {
        $failureSummaryXml = $runtimeParameters['FailureSummaryXml']
        $failureList = @(Read-SummaryXml -SummaryXML $failureSummaryXml)
    }
    return $failureList
}

<#
.SYNOPSIS
    A generic wrapper function to send telemetry events.
#>

function Send-TelemetryEvent
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true)]
        [string]
        $ComponentName,

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

        [Parameter(Mandatory=$true)]
        [int]
        $EventVersion,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $EventData
    )

    $ErrorActionPreference = "Stop"
    Trace-Execution "Telemetry event = Component:[$ComponentName], EventName:[$EventName], EventVersion:[$EventVersion], EventData:$($EventData | Out-String)"

    try
    {
        $telemetryPath = Get-ASArtifactPath -NugetName "Microsoft.AzureStack.Fabric.Health.Telemetry.TelemetryReporter"
        $telemetryDll = Join-Path $telemetryPath "lib\net46\Microsoft.AzureStack.Fabric.Health.Telemetry.TelemetryReporter.dll"
        Import-Module -Name $telemetryDll -Verbose:$false -DisableNameChecking
    }
    catch
    {
        Trace-Warning "Error loading telemetry library: $_"
        return
    }

    try
    {
        Trace-Execution "Sending telemetry event."
        Send-AzureStackTelemetryEvent -Component $ComponentName -Name $EventName -Version $EventVersion -TelemetryEventData $EventData -Verbose
        Trace-Execution "Finish sending telemetry event."
    }
    catch
    {
        Trace-Warning "Error sending telemetry event: $_"
    }
}

<#
.SYNOPSIS
    A generic wrapper function checks whether the current update is pnu or not by comparing minor build version.
#>

function Test-IsMonthlyUpdate
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )
    Trace-Execution "Inside Test-IsMonthlyUpdate method."
    $isPnu = $false
    if ((Test-IsUpdate -Parameters $Parameters) -and !(Test-IsHotfixUpdate -Parameters $Parameters))
    {
        $isPnu = $true
    }
    Trace-Execution "Is it a pnu update :: $isPnu."
    return $isPnu
}

<#
.SYNOPSIS
    A generic wrapper function checks whether the current update is a Hotfix pnu or not by checking the Update Name.
#>

function Test-IsHotfixUpdate
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )
    Trace-Execution "Inside Test-IsHotfixUpdate method."
    $isHotfixPNU = $false
    # In case of update, run time information will be populated
    if($Parameters.RunInformation -ne $null)
    {
        $runtimeParameters = $Parameters.RunInformation['RuntimeParameter']
        if( $runtimeParameters -ne $null -and $runtimeParameters.ContainsKey('UpdateVersion') )
        {
            $updateMetadataXml = [xml]$runtimeParameters['metadata']
            Trace-Execution "UpdateName : $($updateMetadataXml.UpdatePackageManifest.UpdateInfo.UpdateName)"
            if($updateMetadataXml.UpdatePackageManifest.UpdateInfo.UpdateName -match 'AzS Hotfix')
            {
                $isHotfixPNU = $true;
            }
        }
    }
    Trace-Execution "Is it a Hotfix Update :: $isHotfixPNU."
    return $isHotfixPNU
}

<#
.SYNOPSIS
    A helper function to check if the current action plan is an Update action plan.
#>

function Test-IsUpdate
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )
    $updateVersion = Get-InProgressUpdateVersion -Parameters $Parameters
    if ($updateVersion -ne $null)
    {
        return $true
    }
    else
    {
        return $false
    }
}

<#
.SYNOPSIS
    Tests whether the execution context node is a member of the management cluster.
#>

function Test-ManagementClusterNode
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters
    )

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

    # Default to true if there is no BareMetal execution context
    $result = $true

    # If there is a BareMetal execution context, compare management cluster name and RefClusterId of the node
    if (
        ($nodeName = Get-ExecutionContextNodeName -Parameters $Parameters) -and
        ((Get-ExecutionContextRoleName -Parameters $Parameters) -eq 'BareMetal')
    )
    {
        $managementClusterName = Get-ManagementClusterName $Parameters
        Trace-Execution "Management cluster name is '$managementClusterName'"
        $bareMetalRole = $Parameters.Roles["BareMetal"].PublicConfiguration
        $nodeClusterName = ($bareMetalRole.Nodes.Node | Where-Object {$_.Name -eq $nodeName}).RefClusterId
        Trace-Execution "Node '$nodeName' cluster is '$nodeClusterName'"
        if ($nodeClusterName -eq $managementClusterName)
        {
            Trace-Execution "Node '$nodeName' is a member of management cluster '$managementClusterName'"
        }
        else
        {
            Trace-Execution "Node '$nodeName' is not a member of management cluster '$managementClusterName'"
            $result = $false
        }
    }

    return $result
}

<#
.SYNOPSIS
    A helper function to Skip nodes which are provided in runtime parameters.
#>

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

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

    Trace-Execution "[Skip-NodesProvidedInRuntimeParameter] List of Nodes which are getting filtered $($Nodes -join ',')."

    $unreachableNodesStr = $Parameters.RunInformation['RuntimeParameter']['UnreachableNodes']

    if([string]::IsNullOrEmpty($unreachableNodesStr))
    {
        Trace-Execution "[Skip-NodesProvidedInRuntimeParameter] No nodes are present in the list of unreachable nodes."
        return $Nodes
    }

    Trace-Execution "[Skip-NodesProvidedInRuntimeParameter] List of unreachable nodes: $unreachableNodesStr."

    $unreachableNodes = $unreachableNodesStr -split ','

    $newList = $Nodes | % { $_ -notin $unreachableNodes }

    Trace-Execution "[Skip-NodesProvidedInRuntimeParameter] List of Nodes after Filtering $($newList -join ',')."

    return $newList
}

<#
.SYNOPSIS
    A helper function to get first available node.
#>

function Get-FirstAvailableNode
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [string[]]
        $Nodes
    )

    Trace-Execution "[Get-FirstAvailableNode] List of Nodes: $($Nodes -join ',')."

    foreach($node in $Nodes)
    {
        $returnValue = Invoke-Command {"reachable"} -ComputerName $node -ErrorAction Ignore

        if($returnValue -eq "reachable")
        {
            Trace-Execution "[Get-FirstAvailableNode] Node selected with WinRM connectivity: '$node'."
            return $node
        }
        else
        {
            Trace-Execution "[Get-FirstAvailableNode] Node is not reachable via WinRM: '$node'."
        }
    }

    Trace-Warning "[Get-FirstAvailableNode] No node is reachable via WinRM, fall back to Test-NetConnection (ping)."

    foreach($node in $Nodes)
    {
        $returnValue = Test-NetConnection -ComputerName $node -ErrorAction Ignore

        if($returnValue.PingSucceeded)
        {
            Trace-Execution "[Get-FirstAvailableNode] Node selected with Test-NetConnection connectivity: '$node'."
            return $node
        }
        else
        {
            Trace-Execution "[Get-FirstAvailableNode] Node is not reachable via Test-NetConnection: '$node'."
        }
    }

    # In few situations where this code is running on ECE agent then there may be permission issues, to work that around
    # returning first node if none of the nodes are reachable.
    # TODO: Fix the permission issue with ECE agent
    Trace-Warning "[Get-FirstAvailableNode] Fall back to return first node as No node is reachable from the list: $($Nodes -join ',')."
    return $Nodes[0]
}

<#
.SYNOPSIS
    A helper function to check if the cloud admin operation status is update critical.
#>

function Test-CloudStatusUpdateCritical
{
    $ErrorActionPreference = "Stop"
    Import-Module -Name ECEClient -Verbose:$false -DisableNameChecking

    $eceClient = Create-ECEClientWithServiceResolver
    $eceParamsXml = [XML]($eceClient.GetCloudParameters().getAwaiter().GetResult().CloudDefinitionAsXmlString)

    $statusNode = $eceParamsXml.SelectNodes("//Parameter[@Name='CloudAdminOperationStatus']")
    return ($statusNode.Value -eq "UpdateCritical")
}

function Get-AzureStackHostAllVirtualSwitchNames
{
    Param(
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters] $Parameters
    )

    [System.String[]] $physicalMachines = $Parameters.Roles["BareMetal"].PublicConfiguration.Nodes.Node | Select-Object -ExpandProperty "Name"
    [PSObject[]] $vmSwitch = Get-VMSwitch -ComputerName $physicalMachines[0] -SwitchType External

    if ((-not $vmSwitch) -or $vmSwitch.Count -eq 0)
    {
        Trace-Error "Cannot get Virtual Switch Name from [ $($physicalMachines[0]) ]"
    }

    [System.String[]] $retVal = $vmSwitch.Name
    return $retVal
}

function Get-AzureStackHostDefaultVirtualSwitchName
{
    Param(
        [Parameter(Mandatory = $true)]
        [CloudEngine.Configurations.EceInterfaceParameters] $Parameters
    )

    [System.String[]] $allSwitches = Get-AzureStackHostAllVirtualSwitchNames -Parameters $Parameters

    return $allSwitches[0]
}

function Set-TrustedHosts {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $hostIp,

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

        [Parameter(Mandatory = $true)]
        [PSCredential]
        $Credential
    )
    try
    {
        Invoke-Command -ComputerName $hostIp -ScriptBlock {
            $trustedHosts = $using:hosts
            $existingTrustedHosts = (Get-Item -Path WSMan:\localhost\Client\TrustedHosts).Value
            Trace-Execution "Existing Trusted Hosts: $existingTrustedHosts."
            if (![string]::IsNullOrEmpty($existingTrustedHosts) -and $existingTrustedHosts -ne "*")
            {
                $trustedHosts = (($existingTrustedHosts.Split(",") + $trustedHosts.Split(",")) | select -uniq) -join ','
            }
            Trace-Execution "Adding Trusted Hosts: $trustedHosts."
            Set-Item WSMan:\localhost\Client\TrustedHosts -Value $trustedHosts -Force
        } -Credential $Credential
    }
    catch
    {
        Trace-Error "Failed to add Trusted Hosts: $hosts on $hostIp with exception. $_"
        throw $_
    }
}

Export-ModuleMember -Function Add-IDnsConfiguration
Export-ModuleMember -Function Add-IPAddress
Export-ModuleMember -Function Add-LoadBalancerToNetworkAdapter
Export-ModuleMember -Function Add-NetworkAdapterToNetwork
Export-ModuleMember -Function ConnectPSSession
Export-ModuleMember -Function ConvertFrom-IPAddress
Export-ModuleMember -Function Convert-IPv4IntToString
Export-ModuleMember -Function Convert-IPv4StringToInt
Export-ModuleMember -Function ConvertTo-IPAddress
Export-ModuleMember -Function ConvertTo-MacAddress
Export-ModuleMember -Function ConvertTo-PrefixLength
Export-ModuleMember -Function ConvertTo-SubnetMask
Export-ModuleMember -Function Expand-DeploymentArtifacts
Export-ModuleMember -Function Expand-NugetContent
Export-ModuleMember -Function Expand-UpdateContent
Export-ModuleMember -Function Get-FirstAvailableNode
Export-ModuleMember -Function Get-AvailableADComputerName
Export-ModuleMember -Function Get-BareMetalCredential
Export-ModuleMember -Function Get-BroadcastAddress
Export-ModuleMember -Function Get-ClusterShare
Export-ModuleMember -Function Get-ClusterShareNames
Export-ModuleMember -Function Get-CurrentOrchestrators
Export-ModuleMember -Function Get-DomainCredential
Export-ModuleMember -Function Get-DomainIPMapping
Export-ModuleMember -Function Get-EndpointAndPort
Export-ModuleMember -Function Get-ExecutionContextClusterName
Export-ModuleMember -Function Get-ExecutionContextNodeName
Export-ModuleMember -Function Get-ExecutionContextRoleName
Export-ModuleMember -Function Get-GatewayAddress
Export-ModuleMember -Function Get-HostUpdateShare
Export-ModuleMember -Function Get-InProgressUpdateVersion
Export-ModuleMember -Function Get-InProgressUpdatePackagePath
Export-ModuleMember -Function Get-InProgressUpdateNugetStore
Export-ModuleMember -Function Get-IsVirtualNetworkAlreadyConfigured
Export-ModuleMember -Function Get-JeaSession
Export-ModuleMember -Function Get-LocalCsvPathFromSharePath
Export-ModuleMember -Function Get-MacAddress
Export-ModuleMember -Function Get-MacAddressString
Export-ModuleMember -Function Get-NCAccessControlList
Export-ModuleMember -Function Get-NCCredential
Export-ModuleMember -Function Get-NCGateway
Export-ModuleMember -Function Get-NCGatewayPool
Export-ModuleMember -Function Get-NCIPPool
Export-ModuleMember -Function Get-NCLoadBalancer
Export-ModuleMember -Function Get-NCLoadbalancerManager
Export-ModuleMember -Function Get-NCLoadBalancerMux
Export-ModuleMember -Function Get-NCLogicalNetwork
Export-ModuleMember -Function Get-NCLogicalNetworkSubnet
Export-ModuleMember -Function Get-NCMACPool
Export-ModuleMember -Function Get-NCNetworkInterface
Export-ModuleMember -Function Get-NCNetworkInterfaceInstanceId
Export-ModuleMember -Function Get-NCNetworkInterfaceResourceId
Export-ModuleMember -Function Get-NCPublicIPAddress
Export-ModuleMember -Function Get-NCServer
Export-ModuleMember -Function Get-NCSwitch
Export-ModuleMember -Function Get-NCVirtualGateway
Export-ModuleMember -Function Get-NCVirtualNetwork
Export-ModuleMember -Function Get-NCVirtualServer
Export-ModuleMember -Function Get-NCVirtualSubnet
Export-ModuleMember -Function Get-NetworkAddress
Export-ModuleMember -Function Get-NetworkDefinitionForCluster
Export-ModuleMember -Function Get-NetworkDefinitions
Export-ModuleMember -Function Get-NetworkMap
Export-ModuleMember -Function Get-NetworkNameForCluster
Export-ModuleMember -Function Get-PortProfileId
Export-ModuleMember -Function Get-RangeEndAddress
Export-ModuleMember -Function Get-ScopeRange
Export-ModuleMember -Function Get-ServerResourceId
Export-ModuleMember -Function Get-SharePath
Export-ModuleMember -Function Get-StorageEndpointName
Export-ModuleMember -Function Get-TemporaryDomainCredential
Export-ModuleMember -Function Get-UserAccountCredential
Export-ModuleMember -Function Initialize-ECESession
Export-ModuleMember -Function Invoke-ECECommand
Export-ModuleMember -Function Invoke-PSDirectOnVM
Export-ModuleMember -Function Invoke-ScriptBlockInParallel
Export-ModuleMember -Function Invoke-ScriptBlockWithRetries
Export-ModuleMember -Function Invoke-WebRequestWithRetries
Export-ModuleMember -Function IsIpPoolRangeValid
Export-ModuleMember -Function IsIpWithinPoolRange
Export-ModuleMember -Function IsOneNode
Export-ModuleMember -Function IsOnVirtualMachine
Export-ModuleMember -Function IsVirtualAzureStack
Export-ModuleMember -Function JSONDelete
Export-ModuleMember -Function JSONGet
Export-ModuleMember -Function JSONPost
Export-ModuleMember -Function Mount-WindowsImageWithRetry
Export-ModuleMember -Function New-ACL
Export-ModuleMember -Function New-Credential
Export-ModuleMember -Function New-ExecutionContextXmlForNode
Export-ModuleMember -Function New-LoadBalancerVIP
Export-ModuleMember -Function New-NCAccessControlList
Export-ModuleMember -Function New-NCAccessControlListRule
Export-ModuleMember -Function New-NCBgpPeer
Export-ModuleMember -Function New-NCBgpRouter
Export-ModuleMember -Function New-NCBgpRoutingPolicy
Export-ModuleMember -Function New-NCBgpRoutingPolicyMap
Export-ModuleMember -Function New-NCCredential
Export-ModuleMember -Function New-NCGateway
Export-ModuleMember -Function New-NCGatewayPool
Export-ModuleMember -Function New-NCGreTunnel
Export-ModuleMember -Function New-NCIPPool
Export-ModuleMember -Function New-NCIPSecTunnel
Export-ModuleMember -Function New-NCL3Tunnel
Export-ModuleMember -Function New-NCLoadBalancer
Export-ModuleMember -Function New-NCLoadBalancerBackendAddressPool
Export-ModuleMember -Function New-NCLoadBalancerFrontEndIPConfiguration
Export-ModuleMember -Function New-NCLoadBalancerLoadBalancingRule
Export-ModuleMember -Function New-NCLoadBalancerMux
Export-ModuleMember -Function New-NCLoadBalancerMuxPeerRouterConfiguration
Export-ModuleMember -Function New-NCLoadBalancerOutboundNatRule
Export-ModuleMember -Function New-NCLoadBalancerProbe
Export-ModuleMember -Function New-NCLoadBalancerProbeObject
Export-ModuleMember -Function New-NCLogicalNetwork
Export-ModuleMember -Function New-NCLogicalNetworkSubnet
Export-ModuleMember -Function New-NCLogicalSubnet
Export-ModuleMember -Function New-NCMACPool
Export-ModuleMember -Function New-NCNetworkInterface
Export-ModuleMember -Function New-NCPublicIPAddress
Export-ModuleMember -Function New-NCServer
Export-ModuleMember -Function New-NCServerConnection
Export-ModuleMember -Function New-NCServerNetworkInterface
Export-ModuleMember -Function New-NCSlbState
Export-ModuleMember -Function New-NCSwitch
Export-ModuleMember -Function New-NCSwitchPort
Export-ModuleMember -Function New-NCVirtualGateway
Export-ModuleMember -Function New-NCVirtualNetwork
Export-ModuleMember -Function New-NCVirtualServer
Export-ModuleMember -Function New-NCVirtualSubnet
Export-ModuleMember -Function New-NCVpnClientAddressSpace
Export-ModuleMember -Function NormalizeIPv4Subnet
Export-ModuleMember -Function PublishAndStartDscConfiguration
Export-ModuleMember -Function PublishAndStartDscForJea
Export-ModuleMember -Function Remove-LoadBalancerFromNetworkAdapter
Export-ModuleMember -Function Remove-NCAccessControlList
Export-ModuleMember -Function Remove-NCCredential
Export-ModuleMember -Function Remove-NCGateway
Export-ModuleMember -Function Remove-NCGatewayPool
Export-ModuleMember -Function Remove-NCIPPool
Export-ModuleMember -Function Remove-NCLoadBalancer
Export-ModuleMember -Function Remove-NCLoadBalancerMux
Export-ModuleMember -Function Remove-NCLogicalNetwork
Export-ModuleMember -Function Remove-NCMACPool
Export-ModuleMember -Function Remove-NCNetworkInterface
Export-ModuleMember -Function Remove-NCPublicIPAddress
Export-ModuleMember -Function Remove-NCServer
Export-ModuleMember -Function Remove-NCSwitch
Export-ModuleMember -Function Remove-NCVirtualGateway
Export-ModuleMember -Function Remove-NCVirtualNetwork
Export-ModuleMember -Function Remove-NCVirtualServer
Export-ModuleMember -Function Remove-PortProfileId
Export-ModuleMember -Function Set-MacAndIPAddressSingleNode
Export-ModuleMember -Function Set-MacAndIPAddress
Export-ModuleMember -Function Set-NCConnection
Export-ModuleMember -Function Set-NCLoadBalancerManager
Export-ModuleMember -Function Set-PortProfileId
Export-ModuleMember -Function Set-PortProfileIdHelper
Export-ModuleMember -Function Send-TelemetryEvent
Export-ModuleMember -Function Skip-NodesProvidedInRuntimeParameter
Export-ModuleMember -Function Test-CloudStatusUpdateCritical
Export-ModuleMember -Function Test-IPConnection
Export-ModuleMember -Function Test-NetworkMap
Export-ModuleMember -Function Test-IsMonthlyUpdate
Export-ModuleMember -Function Test-IsHotfixUpdate
Export-ModuleMember -Function Test-IsUpdate
Export-ModuleMember -Function Test-ManagementClusterNode
Export-ModuleMember -Function Test-PSSession
Export-ModuleMember -Function Test-WSManConnection
Export-ModuleMember -Function Trace-Error
Export-ModuleMember -Function Trace-Execution
Export-ModuleMember -Function Trace-Warning
Export-ModuleMember -Function Update-JEAEndpointsForUpdate
Export-ModuleMember -Function Update-NCCredential
Export-ModuleMember -Function Update-NCServer
Export-ModuleMember -Function Update-NCVirtualServer
Export-ModuleMember -Function Update-WindowsDismFeature
Export-ModuleMember -Function Wait-Result
Export-ModuleMember -Function Wait-VirtualNetwork
Export-ModuleMember -Function Read-SummaryXml
Export-ModuleMember -Function Get-FailureListFromMainActionPlan
Export-ModuleMember -Function Get-AzureStackHostAllVirtualSwitchNames
Export-ModuleMember -Function Get-AzureStackHostDefaultVirtualSwitchName
Export-ModuleMember -Function Set-TrustedHosts

# SIG # Begin signature block
# MIInzQYJKoZIhvcNAQcCoIInvjCCJ7oCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCq7KjTj0Qhr0Br
# fcAa/apM/k33E0XcvQ+h4UD1Pf277qCCDYUwggYDMIID66ADAgECAhMzAAADri01
# UchTj1UdAAAAAAOuMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwODU5WhcNMjQxMTE0MTkwODU5WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQD0IPymNjfDEKg+YyE6SjDvJwKW1+pieqTjAY0CnOHZ1Nj5irGjNZPMlQ4HfxXG
# yAVCZcEWE4x2sZgam872R1s0+TAelOtbqFmoW4suJHAYoTHhkznNVKpscm5fZ899
# QnReZv5WtWwbD8HAFXbPPStW2JKCqPcZ54Y6wbuWV9bKtKPImqbkMcTejTgEAj82
# 6GQc6/Th66Koka8cUIvz59e/IP04DGrh9wkq2jIFvQ8EDegw1B4KyJTIs76+hmpV
# M5SwBZjRs3liOQrierkNVo11WuujB3kBf2CbPoP9MlOyyezqkMIbTRj4OHeKlamd
# WaSFhwHLJRIQpfc8sLwOSIBBAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUhx/vdKmXhwc4WiWXbsf0I53h8T8w
# VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzUwMTgzNjAfBgNVHSMEGDAW
# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw
# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx
# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB
# AGrJYDUS7s8o0yNprGXRXuAnRcHKxSjFmW4wclcUTYsQZkhnbMwthWM6cAYb/h2W
# 5GNKtlmj/y/CThe3y/o0EH2h+jwfU/9eJ0fK1ZO/2WD0xi777qU+a7l8KjMPdwjY
# 0tk9bYEGEZfYPRHy1AGPQVuZlG4i5ymJDsMrcIcqV8pxzsw/yk/O4y/nlOjHz4oV
# APU0br5t9tgD8E08GSDi3I6H57Ftod9w26h0MlQiOr10Xqhr5iPLS7SlQwj8HW37
# ybqsmjQpKhmWul6xiXSNGGm36GarHy4Q1egYlxhlUnk3ZKSr3QtWIo1GGL03hT57
# xzjL25fKiZQX/q+II8nuG5M0Qmjvl6Egltr4hZ3e3FQRzRHfLoNPq3ELpxbWdH8t
# Nuj0j/x9Crnfwbki8n57mJKI5JVWRWTSLmbTcDDLkTZlJLg9V1BIJwXGY3i2kR9i
# 5HsADL8YlW0gMWVSlKB1eiSlK6LmFi0rVH16dde+j5T/EaQtFz6qngN7d1lvO7uk
# 6rtX+MLKG4LDRsQgBTi6sIYiKntMjoYFHMPvI/OMUip5ljtLitVbkFGfagSqmbxK
# 7rJMhC8wiTzHanBg1Rrbff1niBbnFbbV4UDmYumjs1FIpFCazk6AADXxoKCo5TsO
# zSHqr9gHgGYQC2hMyX9MGLIpowYCURx3L7kUiGbOiMwaMIIHejCCBWKgAwIBAgIK
# YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm
# aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw
# OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD
# VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la
# UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc
# 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D
# dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+
# lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk
# kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6
# A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd
# X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL
# 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd
# sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3
# T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS
# 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI
# bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL
# BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD
# uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv
# c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF
# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h
# cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA
# YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn
# 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7
# v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b
# pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/
# KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy
# CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp
# mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi
# hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb
# BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS
# oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL
# gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX
# cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGZ4wghmaAgEBMIGVMH4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p
# Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAOuLTVRyFOPVR0AAAAA
# A64wDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIEg9
# N8mPyY/OVP/y7gqiujplE+Y8tZCyPAjsqGChMMRLMEIGCisGAQQBgjcCAQwxNDAy
# oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20wDQYJKoZIhvcNAQEBBQAEggEAXVaXSa/HhEBK4UZgohwOoN0yfWbJ3Ntrymex
# k82SGG3aKu0CPwiXLIjtvFFISKN+hNhe8nTHPR5FjTKK8Dzr5A2lbO3Tgd9KRTaK
# N+/Sjs4YiWVDV/0xxmsyV704xWwd4f55qa2EwBNhHSdC2Eh0IhFRxT5PoM7Ssx1J
# jZcRg9iHyu2nZUc5jE5EXDUctJO+41Sh6t8q1I+JBaiB/qGCScxSWjg2L83ttqLz
# KNS5PgyTV6Pw5Dq8bNfQ7bwMmvN1iXNMR0gd2qTS5Yr7U3tzuBBCf+yA2dqvLHlk
# AMKLCcU+miws5yfLAi8Y0CnCUKBc9bNHnSphKm5jHvXGGl7w0KGCFygwghckBgor
# BgEEAYI3AwMBMYIXFDCCFxAGCSqGSIb3DQEHAqCCFwEwghb9AgEDMQ8wDQYJYIZI
# AWUDBAIBBQAwggFYBgsqhkiG9w0BCRABBKCCAUcEggFDMIIBPwIBAQYKKwYBBAGE
# WQoDATAxMA0GCWCGSAFlAwQCAQUABCAhUnZN6JHqr8855HZrXgvJPhMT1kl2EkQc
# ejvHba67JAIGZdZD5kCgGBIyMDI0MDMxMTE4MTcwOC45OFowBIACAfSggdikgdUw
# gdIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsT
# JE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMd
# VGhhbGVzIFRTUyBFU046RDA4Mi00QkZELUVFQkExJTAjBgNVBAMTHE1pY3Jvc29m
# dCBUaW1lLVN0YW1wIFNlcnZpY2WgghF4MIIHJzCCBQ+gAwIBAgITMwAAAdzB4IzC
# X1hejgABAAAB3DANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z
# b2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQ
# Q0EgMjAxMDAeFw0yMzEwMTIxOTA3MDZaFw0yNTAxMTAxOTA3MDZaMIHSMQswCQYD
# VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe
# MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3Nv
# ZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBU
# U1MgRVNOOkQwODItNEJGRC1FRUJBMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1T
# dGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAi8iz
# IDWyOD2RIonN6WtRYXlKGphYvzdqafdITknIhU9QLsXqpNwumGEdn2J1/bV/RFoa
# tTwQfJ0Xw3E8xHYpU2IC0IY8lryRXUIa+fdt4YHabaW2aolqcbvWYDLCuQoBNieL
# Aos9AsnTQSRfDlNLB+Yldt2BAsWUfJ8DkqD6lSwlfOq6aQi8SvQNc++m0AaqR0Us
# rCjgFOUSCe/N5N9e6TNfy9C1MAt9Um5NSBFTvOg/9EVa3dZqBqFnpSWgjQULxeUF
# ANUNfkl4wSzHuOAkN0ScrjhjyAe4RZEOr5Ib1ejQYg6OK5NYPm6/e+USYgDJH/ut
# IW9wufACox2pzL+KpA8yUM5x3QBueI/yJrUFARSd9lPdTHIr2ssH9JGIo/IcOWDy
# hbBfKK/f5sYHp2Z0zrW6vqdS18N/nWU9wqErhWjzek4TX+eJaVWcQdBX00nn8NtR
# KpbZGpNRrY7Yq6+zJEYwSCMYkDXb9KqtGqW8TZ+I3lmZlW2pI9ZohqzHtrQYH591
# PD6B5GfoyjZLr79tkTBL/QgnmBwoaKc1t/JDXGu9Zc+1fMo5+OSHvmJG5ei6sZU9
# GqSbPlRjP5HnJswlaP6Z9warPaFdXyJmcJkMGuudmK+cSsIyHkWV+Dzj3qlPSmGN
# RMfYYKEci8ThINKTaHBY/+4cH2ASzyn/097+a30CAwEAAaOCAUkwggFFMB0GA1Ud
# DgQWBBToc9IF3Q58Rfe41ax2RKtpQZ7d2zAfBgNVHSMEGDAWgBSfpxVdAF5iXYP0
# 5dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29m
# dC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIw
# MjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUt
# U3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB
# /wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOC
# AgEA2etvwTCvx5f8fWwq3eufBMPHgCqAduQw1Cj6RQbAIg1dLfLUZRx2qwr9HWDp
# N/u03HWrQ2kqTUlO6lQl8d0TEq2S6EcD7zaVPvIhKn9jvh2onTdEJPhD7yihBdMz
# PGJ7B8StUu3xZ595udxJPSLrKkq/zukJiTEzbhtupsz9X4zlUGmkJSztH5wROLP/
# MQDUBtkv++Je0eavIDQIZ34+31z5p2xh+bup7lQydLR/9gmYQQyQSoZcLPIsr52H
# 5SwWLR3iWR1wT5mrkk2Mgd6xfXDO0ZUC29fQNgNl03ZZnWST6E4xuVRX8vyfVhbO
# E//ldCdiXTcB9cSuf7URq3KWJ/N3cKEnXG4YbvphtaCJFecO8KLAOq9Ql69VFjWr
# LjLi+VUppKG1t1+A/IZ54n9hxIE405zQM1NZuMxsvnSp4gQLSUdKkvatFg1W7eGw
# fMbyfm7kJBqM/DH0/Omxkh4VM0fJUXqS6MjhWj0287/MXw63jggyPgztRf1lrhDA
# Z/kHvXHns6NpfneDFPi/Oge8QFcX2oKYdGBcEttGiYl8OfrRqXO/t2kJVAi5DTra
# fIhkqexfHO4oVvRONdbDo4WkbVuyNek6jkMweTKyuJvEeivhjPl1mNXIcA3IqjRt
# KsCVV6KFxobkXvhJlPwW3IcBboiAtznD/cP5HWhsOEpnbVYwggdxMIIFWaADAgEC
# AhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQg
# Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0yMTA5MzAxODIyMjVa
# Fw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n
# dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y
# YXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIIC
# IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7V
# gtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64NmeF
# RiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3X
# D9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7M62AW36MEBydUv626GIl3GoP
# z130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+
# tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy1cCGMFxPLOJiss254o2I5Jas
# AUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2ci/b
# fV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQcNIIP8BDyt0cY7afomXw/TNuv
# XsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahhaYQFzymeiXtcodgLiMxhy16cg
# 8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y1BzF
# a/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV2xo3xwgVGD94q0W29R6HXtqP
# nhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEw
# IwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQWBBSf
# pxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBB
# MD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0Rv
# Y3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGC
# NxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8w
# HwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmg
# R4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWlj
# Um9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEF
# BQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29D
# ZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcNAQELBQADggIBAJ1VffwqreEs
# H2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHk
# wo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYAA7AFvonoaeC6Ce5732pvvinL
# btg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCg
# vxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWKNsId
# w2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2
# zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+c23K
# jgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep8beu
# yOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFmPWn9y8FBSX5+k77L+DvktxW/
# tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1ZyvgDbjm
# jJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/2XBj
# U02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIC1DCCAj0CAQEwggEAoYHYpIHVMIHS
# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk
# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRN
# aWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRo
# YWxlcyBUU1MgRVNOOkQwODItNEJGRC1FRUJBMSUwIwYDVQQDExxNaWNyb3NvZnQg
# VGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQAcOf9zP7fJGQhQIl9J
# svd2OdASpqCBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n
# dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y
# YXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0G
# CSqGSIb3DQEBBQUAAgUA6Zkk8jAiGA8yMDI0MDMxMTE0MzYzNFoYDzIwMjQwMzEy
# MTQzNjM0WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDpmSTyAgEAMAcCAQACAg/m
# MAcCAQACAhIFMAoCBQDpmnZyAgEAMDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQB
# hFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZIhvcNAQEFBQADgYEA
# SenkgRvdh/cYHn+5WNLjGjglkTHYp8notfXW/BbhP1WIPttYg3w2yoe4lf3ogEAP
# /Zg1qHXwyJ2F7fwEy2Zg+1A2gR34V1jF9xk33uzANMVHXhbWoY4JZ1+2/cywrz5C
# S2ER41st82etc+xea8KqlvZ/1QKxdG8rczXL5wCIfgYxggQNMIIECQIBATCBkzB8
# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk
# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N
# aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAdzB4IzCX1hejgABAAAB
# 3DANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEE
# MC8GCSqGSIb3DQEJBDEiBCDVG51U5NrV+Fv0v/Yav82lpY5BUzVYHQJDjlyamU0c
# 6TCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIFOnF4pq2UQ/jLypnOO5YvQ6
# 7QirEQsOFfZMvKXEgg03MIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB
# IDIwMTACEzMAAAHcweCMwl9YXo4AAQAAAdwwIgQgivK8Lj7RXtHMBH+QTO3q/wsK
# dwWc8sDwTJ3SGmSptaMwDQYJKoZIhvcNAQELBQAEggIAhTNaO/DUL2X5l6d8JW42
# 6oW4Hg4jer4AhsGXclW7unEmIus79pSkg9pS4I8mjYdfWm4wwGYy9kSktfl43Lu+
# stbKjrQAvJMvtAsY7rKMkAiImmEkqWpdOL1P78wCNlW/wh1kOV42TjFZq3KgMdD9
# tEI12M2R4dmCwvGYFZmCns37Dgrg/MyR5pKQo3szLmnVEK6uebHOZXRuTUKgJLYA
# u+vQP4Sw1RPE4Yo2UxhMiJYopGl1yOPi2TG31ohrwvVnX5Aq2VnP9Du/OzFPxArH
# PvIz0dYsowWtJDP9D/+ylhGpYLy+sRGPCNpwqwCi3cN+s06R3+nY+BjZlqEUJefq
# bZg+4u9+j5wu6CrymzgaalRlE9vQm9bJVR9qxZgGAVUxhc5si2WEGTVwhyUPWqqG
# CpCIF2B7Tc73lAfiVqc9GMpUH4OSvq4cHMKpp0wOkx/JdvvPDqOqez7RHkZjfw2y
# TZGCQWrtAXPn0b8oija5cyN9ERDdLsWGYjLbcK3TOOAsdIeBgpYrC/MVVsxq/8z/
# 2tVcfEtZRd1HFot06EBeBZP/TzQkelxLvivi5+HzLKqVy011trPQp52xTy7ah+cE
# qeOwL5VbE8aXiZNEJ+surwFuTRLZONmox2+2kU5WwFHOX6Z8DGcB1dyZMJA/CK1o
# ktOrKga0a4xqnV/+nvccn7A=
# SIG # End signature block