Obs/bin/ObsDep/content/Powershell/Common/Helpers.psm1

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


Import-Module $PSScriptRoot\Tracer.psm1

# Parameters passed to Invoke-ECECommand that are not forwarded to New-PSSession
$NonPSSessionParameters = @(
    "session",
    "AsJob",
    "InDisconnectedSession",
    "SessionName",
    "HideComputerName",
    "JobName",
    "ScriptBlock",
    "NoNewScope",
    "FilePath",
    "InputObject",
    "ArgumentList",
    "PreLoadNugetName",
    "PreLoadScript",
    "RoleId")

# Parameters passed to Invoke-ECECommand which are passed to Invoke-Command
$InvokeSpecificParameters = @(
    "ArgumentList",
    "InputObject",
    "FilePath",
    "ScriptBlock",
    "JobName",
    "AsJob",
    "HideComputerName",
    "SessionName",
    "InDisconnectedSession")

function Mount-WindowsImageWithRetry
{
    <#
    .SYNOPSIS
    Call mount-windowsimage with retries
 
    .DESCRIPTION
    Retries the mount-windowsimage cmdlet to work around Windows regression in detecting system volume, tracked by AzsureStack bug 10566165.
 
    .EXAMPLE
    Mount-WindowsImageWithRetry
 
    .PARAMETER ImagePath
    The path to the image.
 
    .PARAMETER Path
    The mount path to use.
 
    .PARAMETER Index
    The index to use.
 
    #>

    [CmdletBinding()]
    PARAM
    (
        [Parameter(Mandatory=$true)]
        [string]
        $ImagePath,

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

        [Parameter(Mandatory=$true)]
        [UInt32]
        $Index
   )
    PROCESS
    {
        $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

        $isImageMounted = $false
        $retryAttempt = 0
        $maxMountRetry = 4

        while (-not($isImageMounted) -and ($retryAttempt -lt $maxMountRetry))
        {
            $retryAttempt++
            try
            {
                $hName = (hostname)
                $uName = (whoami)
                Trace-Execution "Trying to mount windows image at '$ImagePath' to folder path '$Path' with index '$Index' on machine '$hName' using creds of '$uName'. Retry attempt: '$retryAttempt'."
                $null = Mount-WindowsImage -ImagePath $ImagePath -Path $Path -Index $Index -Verbose:$false
                $isImageMounted = $true
            }
            catch
            {
                Trace-Execution "Discarding any partially mounted image. Mount Path: '$Path'"
                try
                {
                    Dismount-WindowsImage -Path $Path -Discard -ErrorAction Ignore
                }
                catch
                {                    
                    # Best effort, Dismount-WindowsImage does not respect ErrorAction
                    # More targeted dismount remediation (e.g. due to failover) will be in global remediation SeedRing:Repair
                    Trace-Execution "Best effort dismount attempt: $_"
                }
            
                if (Test-Path -Path "$ImagePath.DISM.vhdx")
                {
                    Trace-Execution "Removing '$ImagePath.DISM.vhdx'"
                    Remove-Item -Path "$ImagePath.DISM.vhdx" -Force
                }

                if ($retryAttempt -lt $maxMountRetry)
                {
                    $exceptionMessage = $_.Exception.Message
                    Trace-Execution "Failure during mount attempt: '$exceptionMessage'. Retrying."
                }
                else
                {
                    throw
                }
            }
        }
    }
}

function Find-LockedFiles
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $shareRelativePath
    )

    Get-SmbOpenFile | Where-Object path -match $shareRelativePath
}

function Mount-Wim {
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $ImagePath,

        [Parameter(Mandatory=$true)]
        [UInt32]
        $ImageIndex
    )
    $errorActionPreference = "Stop"
    Trace-Execution "Mount image '$ImagePath', image index $ImageIndex."

    $mountPath = [IO.Path]::GetTempFileName()
    Trace-Execution "Create temporary mount folder $mountPath."
    Remove-Item $mountPath -Recurse -Force
    $null = New-Item -ItemType Directory $mountPath -Force

    # At this point in Cloud:Build, there shouldn't be any other mounted images.
    # If there is anything already mounted, it's debris from a previous run.
    # Make a best attempt to clean up whatever is found (current or old) and
    # then try to move on. Failure to clean-up will not fail the run.
    $mountedImages = Get-WindowsImage -Mounted -Verbose:$false
    foreach ($mount in $mountedImages)
    {
        Trace-Execution "Attempt to discard previously mounted image '$($mount.ImagePath)', MountStatus = '$($mount.MountStatus)', Path = '$($mount.MountPath)'"
        try
        {
            Dismount-WindowsImage -Path $mount.MountPath -Discard -Verbose:$false
        }
        catch
        {
            Trace-Warning "Failed to dismount '$($mount.MountPath)' - continuing anyway. `r`nError: $_ "
            Trace-Execution "Attempting to clear possibly corrupt mount points."
            Clear-WindowsCorruptMountPoint -ErrorAction SilentlyContinue
        }
    }

    Trace-Execution "Mount $ImagePath to $mountPath."
    Mount-WindowsImageWithRetry -ImagePath $ImagePath -Path $mountPath -Index $ImageIndex

    return $mountPath
}

function Dismount-Wim {
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $MountPath,

        [switch]
        $Discard
    )

    $ImagePath = Get-WindowsImage -Mounted -Verbose:$false | ? Path -eq $MountPath | % ImagePath

    if (-not $imagePath) {
        Trace-Error "Image '$ImagePath' is not mounted."
    }

    Trace-Execution "Dismount $ImagePath."
    if ($Discard) {
        $null = Dismount-WindowsImage -Path $MountPath -Discard -Verbose:$false
    } else {
        $null = Dismount-WindowsImage -Path $MountPath -Save -Verbose:$false
    }
}

function Wait-Result {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ScriptBlock[]]
        $ValidationScript,

        # Time out in seconds.
        [Int32]
        $TimeOut = 30,

        # Time between checks.
        [Int32]
        $Interval = 1
    )
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    # Output all logging streams to a file for debugging
    $logLocation = "$env:systemdrive\Windows\Temp\WaitResultLogs"

    if(-not (Test-Path $logLocation))
    {
        $null = New-Item -Path $logLocation -Type Directory
    }

    $logFileInstance = $null
    for ($retryNumber = 0; $retryNumber -lt 10; $retryNumber++)
    {
        $logName = "WaitResult-" + (Get-Date).ToString("yyyy-MM-dd-HH-mm-ss-ffff") + ".log"
        $logfile = Join-Path -Path $logLocation -ChildPath $logName

        try
        {
            $logFileInstance = new-item $logfile -type file
            break
        }
        catch
        {
            Write-Verbose "Result log file name $logfile is already in use"
            Start-Sleep -Milliseconds 50
        }
    }

    if ($logFileInstance -eq $null)
    {
        Trace-Error "Log file could not be created for WaitResult"
    }

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

    $endTime = [DateTime]::Now.AddSeconds($TimeOut)

    while ([DateTime]::Now -lt $endTime)
    {
        $succeedAll = $true
        $newValidationScript = @($ValidationScript)
        foreach ($script in $ValidationScript) {

            $succeedOne = $false
            try {
                Write-Verbose "Validate if the script`n$($script.ToString())`nreturns $true." -Verbose *>>$logfile
                $errorCountBefore = $global:error.Count
                $succeedOne = $script.Invoke()
                Write-Verbose "Script Results:" -Verbose *>>$logfile
                $succeedOne *>>$logfile
                $errorCountAfter = $global:error.Count
                $numberOfNewErrors = $errorCountAfter - $errorCountBefore

                if ($numberOfNewErrors -gt 0)
                {
                    $global:error.GetRange(0, $numberOfNewErrors) *>>$logfile
                    $global:error.RemoveRange(0, $numberOfNewErrors)
                }
            }
            catch
            {
                $_ *>>$logfile
                $global:error.RemoveAt(0)
            }
            $succeedAll = $succeedAll -and $succeedOne
            if (-not $succeedOne) {
                Write-Verbose "-----Validation failed-----" -Verbose *>>$logfile
                # No need to check all if at least one fails.
                break
            } else {
                Write-Verbose "-----Validation passed-----" *>>$logfile
                $newValidationScript = $newValidationScript -ne $script
            }
        }
        if ($succeedAll) {
            Write-Verbose "All validations passed." -Verbose *>>$logfile
            break
        }

        $ValidationScript = $newValidationScript
        Write-Verbose "Wait for $Interval seconds." -Verbose *>>$logfile
        Start-Sleep -Seconds $Interval
    }

    return $succeedAll
}

function New-Credential
{
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $UserName,

        [string]
        $Password
    )

    $secureString = ConvertTo-SecureString $Password -AsPlainText -Force
    New-Object System.Management.Automation.PSCredential -ArgumentList $UserName, $secureString
}

# Creates the registry name value pair only if it does not exists
function New-RegistryPropertyNameValue
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Path,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Value,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ValueType
    )

    $key = Get-Item -LiteralPath $Path -ErrorAction SilentlyContinue
    if (-not ($key -and ($null -ne $key.GetValue($Name, $null))))
    {
        $null = New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $ValueType -Force
        Trace-Execution "Creating registry property key: $Path, Name: $Name with Value: $Value (Type: $ValueType) on $env:COMPUTERNAME."
    }
    else
    {
        $regValue = Get-ItemPropertyValue -Path $Path -Name $Name
        if ($regValue -ne $Value)
        {
            Set-ItemProperty -Path $Path -Name $Name -Value $Value
            Trace-Execution "Updating the registry key: $Path, Name: $Name from Value: $regValue to Value: $Value on $env:COMPUTERNAME."
        }
    }
}

function Test-WSmanForCredSSP
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [Bool]$Connect,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $ComputerName,

        [PSCredential]
        $Credential,

        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters = $null
    )

    $maxRetries = 5
    $credentialParameter = @{}
    if ($PSBoundParameters.Credential) {
        $credentialParameter.Credential = $PSBoundParameters.Credential
    }

    foreach ($eachComputerName in $ComputerName)
    {
        # to ensure reliability, if the call fails, we'll do 3 retries
        # however, if we have the parameter passed down, we'll start specific tracing to
        # understand the root cause (parameter needed for PSDirect)
        $retry = 0
        $tracingOnVM = $false
        do
        {
            try
            {
                Trace-Execution "Attempt WSMAN connect to $eachComputerName with tracing: $tracingOnVM"
                Invoke-Command -ComputerName $eachComputerName @credentialParameter -ScriptBlock {
                    Set-Item "wsman:\localhost\Service\Auth\CredSSP" $using:Connect -Force
                }
                break
            }
            catch
            {
                Trace-Execution "Exception while trying to do initial WSMan test. $_"
                if ($Parameters)
                {
                    if ($tracingOnVM -eq $true)
                    {
                        # stop any previous tracing
                        $sbTrace = {netsh wfp capture stop}
                        Trace-Execution "Stop Tracing wfp"
                        Invoke-PSDirectOnVM -Parameters $Parameters -VMName $eachComputerName -VMCredential $Credential -ScriptBlock $sbTrace
                        $sbTrace = {netsh trace stop}
                        Trace-Execution "Stop Netsh Tracing"
                        Invoke-PSDirectOnVM -Parameters $Parameters -VMName $eachComputerName -VMCredential $Credential -ScriptBlock $sbTrace
                        $tracingOnVM = $false
                    }

                    # test if the target support internetclient(core server) or internetserver(full server)
                    # the regkey below contains the server roles (only ServerCore for Core, and an array of entries for FullServer)
                    $testServerVersion = {((gci -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Server').Property -imatch "Gui").Count -gt 0}
                    $supportInternetClient = Invoke-PSDirectOnVM -Parameters $Parameters -VMName $eachComputerName -VMCredential $Credential -ScriptBlock $testServerVersion

                    $scenarioToTrace = @{$true = 'InternetClient'; $false = 'InternetServer'}[$supportInternetClient]
                    $enableDisable = @{$true = 'enableCredssp'; $false = 'disableCredssp'}[$Connect]

                    $date = get-date
                    $traceCommand1 = "netsh trace start scenario=$($scenarioToTrace) capture=yes report=disabled tracefile=c:\MASLogs\$($scenarioToTrace)_$($enableDisable)_$($date.year)_$($date.month)_$($date.day)_$($date.hour)_$($date.minute)_$($date.second).etl overwrite=yes"
                    $traceCommand2 = "start-process -filepath c:\windows\system32\netsh.exe -argumentlist `"wfp capture start file=c:\MASLogs\WFPTrace_$($enableDisable)_$($date.year)_$($date.month)_$($date.day)_$($date.hour)_$($date.minute)_$($date.second).cab`""

                    Trace-Execution "Start Tracing $($scenarioToTrace) $($traceCommand1)"
                    $sbTrace = ([scriptblock]::Create($traceCommand1))
                    Invoke-PSDirectOnVM -Parameters $Parameters -VMName $eachComputerName -VMCredential $Credential -ScriptBlock $sbTrace

                    Trace-Execution "Start Tracing WFP: $($traceCommand2)"
                    $sbTrace = ([scriptblock]::Create($traceCommand2))
                    Invoke-PSDirectOnVM -Parameters $Parameters -VMName $eachComputerName -VMCredential $Credential -ScriptBlock $sbTrace
                    $tracingOnVM = $true
                } else
                {
                    Trace-Execution "No PS Direct support, retrying..."
                    Sleep 60
                }
                $retry++
            }
        } while ($retry -lt $maxRetries)

        #ensure no tracing is leaked
        if ($tracingOnVM -eq $true)
        {
            # stop any previous tracing
            $sbTrace = {netsh wfp capture stop}
            Trace-Execution "Stop Tracing wfp"
            Invoke-PSDirectOnVM -Parameters $Parameters -VMName $eachComputerName -VMCredential $Credential -ScriptBlock $sbTrace
            $sbTrace = {netsh trace stop}
            Trace-Execution "Stop Netsh Tracing"
            Invoke-PSDirectOnVM -Parameters $Parameters -VMName $eachComputerName -VMCredential $Credential -ScriptBlock $sbTrace
            $tracingOnVM = $false
        }

        if ($retry -ge $maxRetries)
        {
            Trace-Error "Cannot establish a WSMAN session to VM: $eachComputerName"
        }
    }
}

function Enable-CredSSP
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $ComputerName,

        [PSCredential]
        $Credential,

        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters = $null
    )

    Trace-ECEScript "Enable CredSSP on $($ComputerName -join ', ')" {
        $credentialParameter = @{}
        if ($PSBoundParameters.Credential) {
            $credentialParameter.Credential = $PSBoundParameters.Credential
        }

        $displayName = $ComputerName #| % { $CloudBuilder.GetDisplayName($_) }

        Trace-Execution "Enabling server-side CredSSP on $($displayName -join ', ')."

        Test-WSmanForCredSSP -Connect $true -ComputerName $ComputerName -Credential $Credential -Parameters $Parameters

        Trace-Execution "Enabling client-side CredSSP on the local computer."
        $credsspRegistryKey = "HKLM:\Software\Policies\Microsoft\Windows\CredentialsDelegation"

        if((Get-Item $credsspRegistryKey -ErrorAction SilentlyContinue) -eq $null)
        {
            $null = New-Item -Path $credsspRegistryKey -Force
            Trace-Execution "Creating $credsspRegistryKey on $env:COMPUTERNAME."
        }
        New-RegistryPropertyNameValue -Path $credsspRegistryKey -Name "AllowFreshCredentials" -Value 1 -ValueType Dword
        New-RegistryPropertyNameValue -Path $credsspRegistryKey -Name "AllowFreshCredentialsWhenNTLMOnly" -Value 1 -ValueType Dword
        New-RegistryPropertyNameValue -Path $credsspRegistryKey -Name "ConcatenateDefaults_AllowFresh" -Value 1 -ValueType Dword
        New-RegistryPropertyNameValue -Path $credsspRegistryKey -Name "ConcatenateDefaults_AllowFreshNTLMOnly" -Value 1 -ValueType Dword

        if((Get-Item -Path "$credsspRegistryKey\AllowFreshCredentials" -ErrorAction SilentlyContinue) -eq $null)
        {
            $null = New-Item -Path "$credsspRegistryKey\AllowFreshCredentials" -Force
        }
        New-RegistryPropertyNameValue -Path "$credsspRegistryKey\AllowFreshCredentials" -Name 1 -Value wsman/* -ValueType String

        if((Get-Item -Path "$credsspRegistryKey\AllowFreshCredentialsWhenNTLMOnly" -ErrorAction SilentlyContinue) -eq $null)
        {
            $null = New-Item -Path "$credsspRegistryKey\AllowFreshCredentialsWhenNTLMOnly" -Force
        }
        New-RegistryPropertyNameValue -Path "$credsspRegistryKey\AllowFreshCredentialsWhenNTLMOnly" -Name 1 -Value wsman/* -ValueType String

        Set-Item WSMan:\localhost\Client\Auth\CredSSP $true -Force
        $waitCredSSPEnabled = Wait-Result -ValidationScript {
            Trace-Execution "Verify that CredSSP is enabled."
        Invoke-Command { $true } -ComputerName $ComputerName  @credentialParameter
        } -TimeOut 60 -Interval 1
        if (-not $waitCredSSPEnabled) {
            Trace-Error "CredSSP verification has failed on multiple retries."
        }
    }
}

function Disable-CredSSP
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $ComputerName,

        [PSCredential]
        $Credential,

        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters = $null
    )


    $displayName = $ComputerName #| % { $CloudBuilder.GetDisplayName($_) }

    Trace-Execution "Disabling server-side CredSSP on $($displayName -join ', ')."

    Test-WSmanForCredSSP -Connect $false -ComputerName $ComputerName -Credential $Credential -Parameters $Parameters
}

function Disable-RemoteClientCredSSP
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $ComputerName,

        [PSCredential]
        $Credential
    )

    $credentialParameter = @{}
    if ($PSBoundParameters.Credential) {
        $credentialParameter.Credential = $PSBoundParameters.Credential
    }

    $displayName = $ComputerName

    Trace-Execution "Disabling client-side CredSSP on $($displayName -join ', ')."

    foreach ($eachComputerName in $ComputerName)
    {
        Invoke-Command -ComputerName $eachComputerName @credentialParameter -ScriptBlock {
            Set-Item "wsman:\localhost\Client\Auth\CredSSP" $false -Force
        }
    }
}

function Restart-Machine
{
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]
    param
    (
        [Parameter(Mandatory = $true, Position=0, ValueFromPipeline=$true, ParameterSetName = 'ComputerName')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $ComputerName,

        [PSCredential]
        $Credential,

        [UInt32]
        $MinutesToWait = 10
    )

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

    $startTime = [DateTime]::Now
    $remoteStartTimes = Invoke-Command -ComputerName $ComputerName -Credential $Credential {
        [DateTime]::Now
    }

    $bootStartTimes = @{}
    foreach ($remoteStartTime in $remoteStartTimes) {
        $bootStartTimes.($remoteStartTime.PSComputerName) = $remoteStartTime
    }

    foreach ($computer in $ComputerName) {
        $displayName = $computer
        if ($Credential) {
            Trace-Execution "Restart $displayName using $($Credential.UserName) account."
            $cimSession = New-CimSession -ComputerName $computer -Credential $Credential
        } else {
            Trace-Execution "Restart $displayName."
            $cimSession = New-CimSession -ComputerName $computer
        }

        # Invoking Win32Shutdown method with two flags - Reboot and Force (2 + 4 = 6).
        $shutdownResult = Invoke-CimMethod -ClassName Win32_OperatingSystem -MethodName Win32Shutdown -Arguments @{Flags=[int32]6} -CimSession $cimSession -Verbose:$false
        if ($shutdownResult.ReturnValue) {
            Trace-Error "Win32Shutdown method has failed on $($shutdownResult.PSComputerName) with an error code $($shutdownResult.ReturnValue)."
        }
    }

    $endTime = $startTime.AddMinutes($MinutesToWait)
    $computersToFinishReboot = $ComputerName
    $computersThatFinishedReboot = @()
    while ([DateTime]::Now -lt $endTime)
    {
        foreach ($computer in $computersToFinishReboot) {
            $displayName = $computer
            $originalErrorCount = $global:error.Count
            $os = $null
            try {
                if ($Credential) {
                    $cimSession = New-CimSession -ComputerName $computer -Credential $Credential -Verbose:$false
                } else {
                    $cimSession = New-CimSession -ComputerName $computer -Verbose:$false
                }
                $os = Get-CimInstance -CimSession $cimSession -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue -Verbose:$false
            } catch {}
            $newErrorCount = $global:error.Count - $originalErrorCount
            if ($newErrorCount) {
                $global:error.RemoveRange(0, $newErrorCount)
            }
            if ($os.LastBootUpTime -gt $bootStartTimes.$computer) {
                $computersThatFinishedReboot += $computer
                Trace-Execution "Reboot complete on $displayName."
            }
        }
        $computersToFinishReboot = $computersToFinishReboot | ? {$_ -notin $computersThatFinishedReboot}
        if (-not $computersToFinishReboot) {
            break
        }
        Start-Sleep -Seconds 5 # 5 seconds between retries
    }
    if ($computersToFinishReboot) {
        $computersToFinishRebootDisplayNames = $computersToFinishReboot
        Trace-Error "Some computers have not rebooted during the expected time of $MinutesToWait minutes - $($computersToFinishRebootDisplayNames -join ', ')."
    } else {
        Trace-Execution "All computers have rebooted successfully."
    }
}

#
# Helpers used to join the DVM to the domain.
#

$REG_KEY_PATH = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"

# Enables auto-logon for a given user on the localhost.
function Enable-AutoLogon
{
    [CmdletBinding()]
    param
    (
        [PSCredential]
        $Credential,

        [string]
        $DomainName
    )

    Trace-Execution "Enabling auto-logon for user: '$($Credential.Username)'."

    Set-ItemProperty -Path $REG_KEY_PATH -Name DefaultDomainName -Value $DomainName -Force
    Set-ItemProperty -Path $REG_KEY_PATH -Name DefaultUsername -Value $Credential.UserName -Force
    Set-ItemProperty -Path $REG_KEY_PATH -Name DefaultPassword -Value ([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($Credential.Password))) -Force
    Set-ItemProperty -Path $REG_KEY_PATH -Name AutoAdminLogon -Value 1 -Force
    Set-ItemProperty -Path $REG_KEY_PATH -Name ForceAutoLogon -Value 1 -Force
    Set-ItemProperty -Path $REG_KEY_PATH -Name AutoLogonCount -Value 1 -Force
}

# Resets the registered callback on the localhost.
function Reset-RestartCallback
{
    [CmdletBinding()]
    param
    (
        [string]
        $TaskName,

        [ValidateSet('OnLogin','OnStartup')]
        [string]
        $ExecutionTime = 'OnLogin'
    )

    Trace-Execution "Deleting $ExecutionTime scheduled task."
    $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
    if ($task) {
        $task | Unregister-ScheduledTask -Confirm:$false
    }

    if ($ExecutionTime -eq 'OnLogin') {
        Trace-Execution "Disabling auto-logon."
        Remove-ItemProperty -Path $REG_KEY_PATH -Name DefaultDomainName
        Remove-ItemProperty -Path $REG_KEY_PATH -Name DefaultUsername
        Remove-ItemProperty -Path $REG_KEY_PATH -Name DefaultPassword
        Remove-ItemProperty -Path $REG_KEY_PATH -Name AutoAdminLogon
        Remove-ItemProperty -Path $REG_KEY_PATH -Name ForceAutoLogon
    }
}

# Sets a callback on machine restart on the localhost.
function Set-RestartCallback
{
    [CmdletBinding(
        DefaultParameterSetName='AutoLogonCredential'
        )]
    param
    (
        [Parameter(
            ParameterSetName = 'AutoLogonCredential',
            Mandatory = $true
            )]
        [PSCredential]
        $AutoLogonCredential,

        [Parameter(
            ParameterSetName='GMSA',
            Mandatory = $true
            )]
        [string]
        $GMSA,

        [string]
        $DomainName,

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

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

        [switch]
        $Persistent,

        [ValidateSet('OnLogin','OnStartup')]
        [string]
        $ExecutionTime = 'OnLogin',

        [Microsoft.Management.Infrastructure.CimInstance]
        $Principal
    )

    $ErrorActionPreference = 'Stop'

    if ($AutoLogonCredential) {
        $username = $AutoLogonCredential.UserName

        # needed incase user name comes in as Domain\User or just User
        $userNameArray = [Array]$username.Split('\')
        $usernameOnly = $userNameArray[-1]
    } else {
        $usernameOnly = $GMSA
    }

    $callbackString = @"
Import-Module $PSScriptRoot\..\CloudDeployment.psd1
Import-Module $PSCommandPath
 
"@


    if (-not $Persistent) {
        $callbackString += @"
Reset-RestartCallback -TaskName $TaskName -ExecutionTime $ExecutionTime
 
"@

    }

    $callbackString += @"
$Callback
"@


    $callArgs = "-ExecutionPolicy RemoteSigned -NoExit -Command $callbackString"

    Trace-Execution "Registering the callback for powershell.exe with argument: '$callArgs'."
    $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $callArgs

    $registrationParams = @{
        TaskName = $TaskName
        TaskPath = '\'
        Action = $action
        Settings = New-ScheduledTaskSettingsSet -Priority 4
        Force = $true
    }

    if ($ExecutionTime -eq 'OnLogin') {
        $registrationParams.Trigger = New-ScheduledTaskTrigger -AtLogOn
        if ($Principal) {
            $registrationParams.Principal = $Principal
        } else {
            $registrationParams.User = "$usernameOnly"
            $registrationParams.RunLevel = 'Highest'
        }
        Enable-AutoLogon -Credential $AutoLogonCredential -DomainName $DomainName
    } else {
        $registrationParams.Trigger = New-ScheduledTaskTrigger -AtStartup
        if ($AutoLogonCredential) {
            if ($Principal) {
                $registrationParams.Principal = $Principal
            } else {
                $registrationParams.User = $AutoLogonCredential.Username
                $registrationParams.Password = $AutoLogonCredential.GetNetworkCredential().Password
                $registrationParams.RunLevel = 'Highest'
            }
        } else {
            $registrationParams.Principal = New-ScheduledTaskPrincipal -UserID $GMSA -LogonType Password -RunLevel Highest
        }
    }

    Trace-Execution "Registering the scheduled task named '$TaskName' under the user '$usernameOnly'."
    Register-ScheduledTask @registrationParams
}

function Initialize-ECESession
{
    <#
    .SYNOPSIS
    Prepare Infra vm PS Session.
 
    .DESCRIPTION
    Loads all helper functions into a PSSession so that infra vm common functions can be readily used.
 
    .EXAMPLE
    Initialize-ECESession -Session $psSession
 
    .PARAMETER Session
    A PS Session to bootstrap with infra vm helper functions.
 
    .PARAMETER RoleId
    The role id to be used for a transcript recording the operations in this session on the remote machine.
 
    .PARAMETER PreLoadNugetName
    The name of a nuget to preload a script from
 
    .PARAMETER PreLoadScript
    The path of a script to preload into this session.
 
    .PARAMETER SuppressVerbose
    Prevent the redirection of verbose output from the remote session into the current runspace.
 
    #>

    [CmdletBinding()]
    PARAM
    (
        [Parameter(Mandatory=$true, Position=0)]
        [System.Management.Automation.Runspaces.PSSession[]] $Session,

        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $RoleId,

        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $PreLoadNugetName,

        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $PreLoadScript,

        [Parameter(Mandatory=$false)]
        [switch] $SuppressVerbose
    )
    PROCESS
    {
        # function purposely made a no-op and will be refactored away. This functionality is in New-AzsSession directly now.
    }
}

function Initialize-NugetScript
{
    <#
    .SYNOPSIS
    Dot source a script from a nuget into a ps session.
 
    .DESCRIPTION
    Finds a script from a nuget and loads it into the powershell session so cmdlets from the script can be used.
 
    .EXAMPLE
    Initialize-NugetScript -Session $psSession -PreLoadNugetName TestNuget -PreLoadScript "content\Scripts\Helpers.ps1"
 
    .PARAMETER Session
    A PS Session to bootstrap with infra vm helper functions.
 
    .PARAMETER PreLoadNugetName
    The name of a nuget to preload a script from
 
    .PARAMETER PreLoadScript
    The name of a script to preload into this session.
 
    #>

    [CmdletBinding()]
    PARAM
    (
        [Parameter(Mandatory=$true, Position=0)]
        [System.Management.Automation.Runspaces.PSSession[]] $Session,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $PreLoadNugetName,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $PreLoadScript
    )
    PROCESS
    {
        $ComputerName = $Session.ComputerName

        Trace-Execution "Initializing remote powershell session on $ComputerName with custom nuget script $PreLoadScript from nuget $PreLoadNugetName."
        $ScriptBlock =
        {
            $nugetPath = Get-ASArtifactPath -NugetName $using:PreLoadNugetName
            $preLoadScriptPath = Join-Path $nugetPath $using:PreLoadScript

            Write-Verbose "Loading pre load script $preLoadScriptPath"
            . $preLoadScriptPath
        }
        Invoke-Command -Session $Session -ScriptBlock $ScriptBlock
    }
}

function Invoke-ECECommand
{
    <#
    .SYNOPSIS
    Runs a command against an infra vm with helper functions and pre loaded script functions automatically available to the command.
 
    .DESCRIPTION
    Uses a PSsession and loads all helper functions into it so that infra vm common functions can be readily used. Optionally loads
    functions from the provided preLoadScript. Finally runs the command supplied in this session where preloaded functions can be readily used by the command.
 
    #>

    [CmdletBinding(DefaultParameterSetName='InProcess', HelpUri='http://go.microsoft.com/fwlink/?LinkID=135225', RemotingCapability='OwnedByCommand')]
    PARAM
    (
        [Parameter(ParameterSetName='Session', Position=0)]
        [Parameter(ParameterSetName='FilePathRunspace', Position=0)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.Runspaces.PSSession[]]
        $Session,

        [Parameter(ParameterSetName='ComputerName', Position=0)]
        [Parameter(ParameterSetName='FilePathComputerName', Position=0)]
        [Alias('Cn')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $ComputerName,

        [Parameter(ParameterSetName='ComputerName', ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='Uri', ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='FilePathComputerName', ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='FilePathUri', ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='VMId', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='VMName', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='FilePathVMId', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='FilePathVMName', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [pscredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        [Parameter(ParameterSetName='FilePathComputerName')]
        [Parameter(ParameterSetName='ComputerName')]
        [ValidateRange(1, 65535)]
        [int]
        $Port,

        [Parameter(ParameterSetName='ComputerName')]
        [Parameter(ParameterSetName='FilePathComputerName')]
        [switch]
        $UseSSL,

        [Parameter(ParameterSetName='FilePathUri', ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='Uri', ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='FilePathComputerName', ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='ComputerName', ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='ContainerId', ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='VMId', ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='VMName', ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='FilePathContainerId', ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='FilePathVMId', ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='FilePathVMName', ValueFromPipelineByPropertyName=$true)]
        [string]
        $ConfigurationName,

        [Parameter(ParameterSetName='ComputerName', ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='FilePathComputerName', ValueFromPipelineByPropertyName=$true)]
        [string]
        $ApplicationName,

        [Parameter(ParameterSetName='FilePathUri')]
        [Parameter(ParameterSetName='Session')]
        [Parameter(ParameterSetName='Uri')]
        [Parameter(ParameterSetName='FilePathComputerName')]
        [Parameter(ParameterSetName='FilePathRunspace')]
        [Parameter(ParameterSetName='ComputerName')]
        [Parameter(ParameterSetName='VMId')]
        [Parameter(ParameterSetName='VMName')]
        [Parameter(ParameterSetName='ContainerId')]
        [Parameter(ParameterSetName='FilePathVMId')]
        [Parameter(ParameterSetName='FilePathVMName')]
        [Parameter(ParameterSetName='FilePathContainerId')]
        [int]
        $ThrottleLimit,

        [Parameter(ParameterSetName='FilePathUri', Position=0)]
        [Parameter(ParameterSetName='Uri', Position=0)]
        [Alias('URI','CU')]
        [ValidateNotNullOrEmpty()]
        [uri[]]
        $ConnectionUri,

        [Parameter(ParameterSetName='FilePathRunspace')]
        [Parameter(ParameterSetName='Session')]
        [Parameter(ParameterSetName='Uri')]
        [Parameter(ParameterSetName='FilePathComputerName')]
        [Parameter(ParameterSetName='ComputerName')]
        [Parameter(ParameterSetName='FilePathUri')]
        [Parameter(ParameterSetName='VMId')]
        [Parameter(ParameterSetName='VMName')]
        [Parameter(ParameterSetName='ContainerId')]
        [Parameter(ParameterSetName='FilePathVMId')]
        [Parameter(ParameterSetName='FilePathVMName')]
        [Parameter(ParameterSetName='FilePathContainerId')]
        [switch]
        $AsJob,

        [Parameter(ParameterSetName='FilePathUri')]
        [Parameter(ParameterSetName='FilePathComputerName')]
        [Parameter(ParameterSetName='Uri')]
        [Parameter(ParameterSetName='ComputerName')]
        [Alias('Disconnected')]
        [switch]
        $InDisconnectedSession,

        [Parameter(ParameterSetName='ComputerName')]
        [Parameter(ParameterSetName='FilePathComputerName')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $SessionName,

        [Parameter(ParameterSetName='VMName')]
        [Parameter(ParameterSetName='Session')]
        [Parameter(ParameterSetName='Uri')]
        [Parameter(ParameterSetName='FilePathComputerName')]
        [Parameter(ParameterSetName='FilePathRunspace')]
        [Parameter(ParameterSetName='FilePathUri')]
        [Parameter(ParameterSetName='VMId')]
        [Parameter(ParameterSetName='ComputerName')]
        [Parameter(ParameterSetName='ContainerId')]
        [Parameter(ParameterSetName='FilePathVMId')]
        [Parameter(ParameterSetName='FilePathVMName')]
        [Parameter(ParameterSetName='FilePathContainerId')]
        [Alias('HCN')]
        [switch]
        $HideComputerName,

        [Parameter(ParameterSetName='ComputerName')]
        [Parameter(ParameterSetName='Session')]
        [Parameter(ParameterSetName='Uri')]
        [Parameter(ParameterSetName='FilePathComputerName')]
        [Parameter(ParameterSetName='FilePathRunspace')]
        [Parameter(ParameterSetName='FilePathUri')]
        [Parameter(ParameterSetName='ContainerId')]
        [Parameter(ParameterSetName='FilePathContainerId')]
        [string]
        $JobName,

        [Parameter(ParameterSetName='VMId', Mandatory=$true, Position=1)]
        [Parameter(ParameterSetName='Session', Mandatory=$true, Position=1)]
        [Parameter(ParameterSetName='Uri', Mandatory=$true, Position=1)]
        [Parameter(ParameterSetName='InProcess', Mandatory=$true, Position=0)]
        [Parameter(ParameterSetName='ComputerName', Mandatory=$true, Position=1)]
        [Parameter(ParameterSetName='VMName', Mandatory=$true, Position=1)]
        [Parameter(ParameterSetName='ContainerId', Mandatory=$true, Position=1)]
        [Alias('Command')]
        [ValidateNotNull()]
        [scriptblock]
        $ScriptBlock,

        [Parameter(ParameterSetName='InProcess')]
        [switch]
        $NoNewScope,

        [Parameter(ParameterSetName='FilePathVMId', Mandatory=$true, Position=1)]
        [Parameter(ParameterSetName='FilePathRunspace', Mandatory=$true, Position=1)]
        [Parameter(ParameterSetName='FilePathUri', Mandatory=$true, Position=1)]
        [Parameter(ParameterSetName='FilePathComputerName', Mandatory=$true, Position=1)]
        [Parameter(ParameterSetName='FilePathVMName', Mandatory=$true, Position=1)]
        [Parameter(ParameterSetName='FilePathContainerId', Mandatory=$true, Position=1)]
        [Alias('PSPath')]
        [ValidateNotNull()]
        [string]
        $FilePath,

        [Parameter(ParameterSetName='Uri')]
        [Parameter(ParameterSetName='FilePathUri')]
        [switch]
        $AllowRedirection,

        [Parameter(ParameterSetName='ComputerName')]
        [Parameter(ParameterSetName='Uri')]
        [Parameter(ParameterSetName='FilePathComputerName')]
        [Parameter(ParameterSetName='FilePathUri')]
        [System.Management.Automation.Remoting.PSSessionOption]
        $SessionOption,

        [Parameter(ParameterSetName='FilePathComputerName')]
        [Parameter(ParameterSetName='ComputerName')]
        [Parameter(ParameterSetName='Uri')]
        [Parameter(ParameterSetName='FilePathUri')]
        [System.Management.Automation.Runspaces.AuthenticationMechanism]
        $Authentication,

        [Parameter(ParameterSetName='FilePathComputerName')]
        [Parameter(ParameterSetName='ComputerName')]
        [Parameter(ParameterSetName='Uri')]
        [Parameter(ParameterSetName='FilePathUri')]
        [switch]
        $EnableNetworkAccess,

        [Parameter(ParameterSetName='ContainerId')]
        [Parameter(ParameterSetName='FilePathContainerId')]
        [switch]
        $RunAsAdministrator,

        [Parameter(ValueFromPipeline=$true)]
        [psobject]
        $InputObject,

        [Alias('Args')]
        [System.Object[]]
        $ArgumentList,

        [Parameter(ParameterSetName='FilePathVMId', Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='VMId', Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)]
        [Alias('VMGuid')]
        [ValidateNotNullOrEmpty()]
        [guid[]]
        $VMId,

        [Parameter(ParameterSetName='VMName', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='FilePathVMName', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $VMName,

        [Parameter(ParameterSetName='FilePathContainerId', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='ContainerId', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $ContainerId,

        [Parameter(ParameterSetName='ComputerName')]
        [Parameter(ParameterSetName='Uri')]
        [string]
        $CertificateThumbprint,

        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $PreLoadNugetName,

        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $PreLoadScript,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $RoleId
    )
    PROCESS
    {
        $errorActionPreference = 'stop'
        $outBuffer = $null
        if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
        {
            $PSBoundParameters['OutBuffer'] = 1
        }

        $sessionParameters = @{} + $PSBoundParameters;
        foreach ($parameter in $NonPSSessionParameters)
        {
            # Remove all parameters which do not get forwarded to New-PSSession.
            if ($sessionParameters.Contains($parameter))
            {
                $sessionParameters.Remove($parameter)
            }
        }

        $invokeParameters = @{}
        Trace-Execution "Passing the following parameters:"
        foreach ($parameter in $InvokeSpecificParameters)
        {
            # Add all parameters which should be forwarded to the wrapped Invoke-Command.
            if ($PSBoundParameters.ContainsKey($parameter))
            {
                $invokeParameters.Add($parameter, $PSBoundParameters[$parameter])

                # Trace the Argument List only for now
                if ($parameter.Equals('ArgumentList'))
                {
                    Trace-Execution "[$parameter] = [$($PSBoundParameters[$parameter] | ConvertTo-Json -Depth 1)]"
                }
            }
        }

        if (-not $Session)
        {
            Trace-Execution "Creating remote powershell session on $ComputerName"
            $SessionCreated = $true
            $Session = & Microsoft.PowerShell.Core\New-PSSession @sessionParameters
        }

        $DebuggingCallStack = Get-PSCallStack

        # Make available Get-ASArtifactPath cmdlet in the session.
        $ComputerName = $Session.ComputerName
        Trace-Execution "Initializing remote powershell session on $ComputerName with common functions."

        $scriptPath = Join-Path $PSScriptRoot "InfraVmHelpers.psm1"
        Trace-Execution "Loading infra vm helpers ($scriptPath) to session on $ComputerName"
        Microsoft.PowerShell.Core\Invoke-Command -Session $Session -ScriptBlock { Import-Module CloudCommon }

        try
        {
            $TimeString = Get-Date -Format "yyyyMMdd-HHmmss"
            $RemoteLOGFILE = "$env:systemdrive\MASLogs\$RoleId_$callingCommand_$TimeString.log"

            if ($PreLoadNugetName -and $PreLoadScript)
            {
                Initialize-NugetScript -Session $Session -PreLoadNugetName $PreLoadNugetName -PreLoadScript $PreLoadScript -Verbose:$VerbosePreference
            }

            $invokeParameters.Add("Session", $Session);

            Trace-Execution "Invoking command on remote session..."
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Core\Invoke-Command', [System.Management.Automation.CommandTypes]::Cmdlet)
            & $wrappedCmd @invokeParameters
        }
        finally
        {
            # Cleanup ps session on failure.
            if ($Session -and $SessionCreated)
            {
                Remove-PSSession -ErrorAction Ignore -Session $Session
            }
        }
    }
    <#
 
    .ForwardHelpTargetName Microsoft.PowerShell.Core\Invoke-Command
    .ForwardHelpCategory Cmdlet
 
    #>

}

function Get-NugetVersions
{
    <#
    .SYNOPSIS
    Gets all versions of Nuget packages that exist in a specified source location.
 
    .EXAMPLE
    Get-NugetVersions -NugetName "MyNuget" -SourcePath "content"
 
    .PARAMETER NugetName
    The name of the nuget to filter by.
 
    .PARAMETER MostRecent
    Whether to report the most recent version only
 
    .PARAMETER NugetStorePath
    The path from which the nuget packages should be derived.
 
    #>

    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $NugetName,

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

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $NugetStorePath = "$env:SystemDrive\CloudDeployment\NuGetStore"
    )
    PROCESS
    {
        $ErrorActionPreference = "stop"

        Trace-Execution "Finding nuget package $NugetName from store $NugetStorePath"

        $nugetPackageList = Find-Package -Source $NugetStorePath -Name $NugetName -ProviderName "nuget" -AllVersions:$(-not $MostRecent) -Verbose -Force

        if ($nugetPackageList)
        {
            $versionArray = $nugetPackageList.Version
        }
        else
        {
            $versionArray = @()
        }

        return $versionArray
    }
}

function Expand-NugetContent
{
    <#
    .SYNOPSIS
    Expands the content of a nuget to a destination.
 
    .DESCRIPTION
    Copies files or folders from a nuget to destination. Folders are copied excluding the folder name given, that is content under the source folder and not the folder itself.
    Empty string can be given for the entire nuget.
 
    .EXAMPLE
    Expand-NugetContent -NugetName "MyNuget" -SourcePath "content" -DestinationPath "C:\temp\"
 
    .EXAMPLE
    Expand-NugetContent -NugetName "MyNuget" -SourcePath "content\Folder\MyFile.txt" -DestinationPath "C:\temp\" -SourceIsFile
 
    .PARAMETER NugetName
    The name of the nuget to source the content from.
 
    .PARAMETER SourcePath
    The relative path inside the nuget to a folder or file. If empty string is passed all nuget contents will be expanded.
 
    .PARAMETER DestinationPath
    A destination folder under which all the nuget content will be copied.
 
    .PARAMETER NugetStorePath
    The source path for the nuget package
 
    .PARAMETER SourceIsFile
    A flag indicating the source path represents a file and not a folder and should be copied as an individual file to the destination.
 
    .PARAMETER Version
    The specific version of the nuget package, null if the latest version is required
 
    #>

    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]
        $NugetName,

        [Parameter(Mandatory=$true)]
        [string]
        [AllowEmptyString()]
        $SourcePath,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $DestinationPath,

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

        [Parameter()]
        [switch]
        $SourceIsFile,

        [Parameter()]
        [string]
        $Version = $null,

        [Parameter()]
        [switch]
        $IsNugetInstall,

        [Parameter()]
        [switch]
        $IsUnc,

        [Parameter()]
        [PSCredential]
        $Credential = $null
    )
    PROCESS
    {
        $ErrorActionPreference = "stop"

        Write-Verbose -Verbose "Finding nuget package $NugetName with version [$Version] from store $NugetStorePath"

        #Handle the unavailability of Nuget store path gracefully. Avoid throwing exception in this case. Any unwanted error record in the error stream will cause ECE to fail.
        if ($Version)
        {
            $nugetPackage = Find-Package -Source $NugetStorePath -Name $NugetName -ProviderName "nuget" -Verbose -Force -ForceBootstrap:$false -ErrorAction Ignore -RequiredVersion $Version
        }
        else
        {
            $nugetPackage = Find-Package -Source $NugetStorePath -Name $NugetName -ProviderName "nuget" -Verbose -Force -ForceBootstrap:$false -ErrorAction Ignore
        }

        if($nugetPackage)
        {
            if(!$IsNugetInstall.IsPresent)
            {
                [Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") | Out-Null

                $nugetPackageFileLocation = Join-Path $NugetStorePath $nugetPackage.PackageFileName
                Write-Verbose -Verbose "Opening nuget package $nugetPackageFileLocation"
                $nugetZipFile = [IO.Compression.ZipFile]::OpenRead($nugetPackageFileLocation)
                try
                {
                    # Check for an exact file match or for a folder match.
                    # If the source path is empty string we take every entry.
                    # If the source path is a file we take the file.
                    # If the source path is a folder we append a "/" and then pull all items under that folder.
                    $entriesToCopy = $nugetZipFile.Entries

                    if (![string]::IsNullOrEmpty($SourcePath))
                    {
                        $normalizedSourcePath = [System.IO.Path]::GetFullPath($SourcePath)
                        $exactFileMatch = $entriesToCopy | Where-Object { [string]::Equals([System.IO.Path]::GetFullPath($_.FullName), $normalizedSourcePath, [System.StringComparison]::OrdinalIgnoreCase) }

                        if ($exactFileMatch)
                        {
                            $entriesToCopy = $exactFileMatch
                        }
                        else
                        {
                            if (!$normalizedSourcePath.EndsWith([System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::Ordinal))
                            {
                                $normalizedSourcePath += [System.IO.Path]::DirectorySeparatorChar
                            }

                            $entriesToCopy = $entriesToCopy | Where-Object { [System.IO.Path]::GetFullPath($_.FullName).StartsWith($normalizedSourcePath, [System.StringComparison]::OrdinalIgnoreCase) }
                        }
                    }

                    Write-Verbose -Verbose "Copying $($entriesToCopy.Count) files from $SourcePath to $DestinationPath"

                    if ($SourceIsFile.IsPresent)
                    {
                        $SourcePath = [System.IO.Path]::GetDirectoryName($SourcePath)
                    }

                    # if($Credential)
                    # {
                    # Trace-Execution "Creating PS drive 'NugetTempPSDrive' with root $DestinationPath and user $($Credential.UserName)."
                    # New-PSDrive -Name NugetTempPSDrive -PSProvider FileSystem -Root $DestinationPath -Credential $Credential -ErrorAction Stop
                    # }

                    foreach ($entryToCopy in $entriesToCopy)
                    {
                        $finalDestinationPath = $entryToCopy.FullName.SubString($SourcePath.Length)
                        $finalDestinationPath = [uri]::UnescapeDataString($finalDestinationPath)
                        $finalDestinationPath = Join-Path $DestinationPath $finalDestinationPath
                        # create the destination directory structure
                        $finalDestinationPathParent = Split-Path $finalDestinationPath -Parent

                        New-Item -ItemType Directory -Force -Path $finalDestinationPathParent | Out-Null

                        if( -not $IsUnc)
                        {
                            $finalDestinationPath = '\\?\' + $finalDestinationPath
                        }

                        try
                        {
                            [IO.Compression.ZipFileExtensions]::ExtractToFile($entryToCopy, $finalDestinationPath, $true)
                        }
                        catch
                        {
                            Trace-Warning "Failed to extract file to $finalDestinationPath"
                            throw
                        }
                    }
                }
                finally
                {
                    Write-Verbose -Verbose "Closing nuget package"
                    $nugetZipFile.Dispose()
                }
            }
            else
            {
                Write-Verbose -Verbose "Installing Nuget package $NugetName with version [$Version] at $DestinationPath"
                Import-Module $PSScriptRoot\..\Roles\Cloud\Cloud.psm1 -DisableNameChecking
                Install-Nuget -NugetName $NugetName -NugetSourcePath $NugetStorePath -NugetDestinationPath $DestinationPath -Version $Version | Out-Null
            }
        }
        else
        {
            Write-Verbose -Verbose "Could not find package $NugetName with version [$Version] from store $NugetStorePath. Looking for package using Get-ASArtifactPath."
            $expandedNugetLocation = Get-ASArtifactPath -NugetName $NugetName -Version $Version
            $fullSourcePath = Join-Path $expandedNugetLocation $SourcePath
            if((!$SourceIsFile.IsPresent) -and (Test-Path -Path $DestinationPath -PathType Container))
            {
                #Append \* only if the destination is directory and already exists.
                $fullSourcePath += '\*'
            }

            #Enable support for long paths > 260 chars in the source and destination.
            $fullSourcePath = '\\?\' + $fullSourcePath
            $DestinationPath = '\\?\' + $DestinationPath
            Write-Verbose -Verbose "Copy nuget contents from $fullSourcePath to $DestinationPath."
            Copy-Item -Path $fullSourcePath -Destination $DestinationPath -Recurse -Force -Verbose
        }
    }
}

function Get-NugetStorePath
{
    <#
    .SYNOPSIS
    Gets the current Nuget Store.
 
    .DESCRIPTION
    Gets the current Nuget Store which is on the managment share if it has been built or on the DVM directly if its early in deployment.
 
    .PARAMETER Parameters
    This object is based on the customer configuration. It contains the private information of the Key Vault role, as well as
    public information of all other roles. It is passed down by the deployment engine.
 
    #>

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

    Trace-ECEScript "Getting the current Nuget Store" {
        $virtualMachinesRole = $Parameters.Roles["VirtualMachines"].PublicConfiguration

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

        # prefer the library store but if it hasn't been created yet use the local store.
        if(-not (Test-Path $nugetStorePath))
        {
            $nugetStorePath = "$env:SystemDrive\CloudDeployment\NuGetStore"
        }
    }

    return $nugetStorePath
}

function ConnectPSSession
{
    <#
    .SYNOPSIS
    Start PS session to one of the provided machines.
 
    .DESCRIPTION
    Return the first successful connection. Throw if no PS session can be made.
 
    .EXAMPLE
    $RemoteSession = ConnectPSSession $Machines $Credential
 
    .PARAMETER Machines
    The list of machine names.
 
    .PARAMETER Credential
    The credential used to connect to machines.
 
    #>

    [CmdletBinding()]
    PARAM
    (
        [Parameter(Position=0, Mandatory=$true)]
        [string[]]
        $Machines,

        [Parameter(Position=1, Mandatory=$true)]
        [PSCredential]
        $Credential
   )
    PROCESS
    {
        $RemoteSession = $null
        foreach ($Machine in $Machines)
        {
            Trace-Execution "Try to start PS remote session to $Machine with user $($Credential.UserName)"
            try
            {
                $RemoteSession = New-PSSession -ComputerName $Machine -Credential $Credential -Authentication Credssp -ErrorAction Stop
                if ($RemoteSession)
                {
                    Trace-Execution "Connection to $Machine was successful."
                    return $RemoteSession
                }
            }
            catch
            {
                Trace-Warning "Failed to connect to $Machine with exception $($_ | Out-String)"
            }
        }

        throw "Could not create a PSSession to any of the specified machines."
    }
}

# This function works around a bug in PowerShell where cmdlets that
# return a CimInstance don't actually stop when $ErrorActionPreference is
# set to 'Stop'. You have to specify it on the cmdlet itself.
function PublishAndStartDscConfiguration
{
    [CmdletBinding(DefaultParameterSetName='PublishAndStart')]
    Param(
        [Parameter(Mandatory=$true, ParameterSetName="PublishOnly")]
        [Parameter(Mandatory=$true, ParameterSetName="PublishAndStart")]
        [String]$Path,

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

        [int]$RetryCount = 3,

        [PSCredential]$Credential = $null,

        [Parameter(ParameterSetName="PublishOnly")]
        [switch] $PublishOnly,

        [Parameter(ParameterSetName="StartOnly")]
        [switch] $StartOnly,

        # By default, applied configurations will be validated by running an additional Test-DscConfiguration after
        # Start-DscConfiguration has completed successfully. However, some configurations use DSC resources with non-
        # functional Test() implementations (by design). When invoking configs with such resources, callers can pass
        # this flag to allow all the Set() resources to be executed, but without performing the additional Test check.
        [switch] $SkipTestDscConfiguration
    )

    Import-Module "$PSScriptRoot\..\Roles\Common\RemoteSessionHelpers.psm1" -DisableNameChecking

    try
    {
        # The DCOM session is necessary because attempting to query DSC state through WSMan while JEA endpoints
        # are being registered by DSC can result in a deadlock of the WinRM service on the target node.
        $sessionParams = @{
            ComputerName = $ComputerName
        }

        if ($Credential)
        {
            $sessionParams.Credential = $Credential
        }

        $sessionDcom = New-CimSessionWithDcom @sessionParams

        # Deployment code sometimes compiles DSC configuration mofs for multiple target nodes into the same output directory.
        # We don't have an easy way to separate these without parsing the MOF, so the wait for LCM state will block until
        # all nodes have stopped processing existing DSC configs (if any). This can be optimized in the future if we change the
        # publishing mechanism to target MOFs on a per-node basis.
        Wait-LCMState -CimSession $sessionDcom -TimeoutMinutes 30

        if (-not $StartOnly)
        {
            Trace-Execution "Publishing DSC configuration from $Path to the following target nodes: $($ComputerName -join ', ')"
            $publishParams = @{
                Path = $Path
                ErrorAction = "Stop"
                Force = $true
            }

            if ($Credential)
            {
                $publishParams.Credential = $Credential
            }

            Publish-DscConfiguration @publishParams
        }

        if ($PublishOnly)
        {
            Trace-Execution "Publish-only mode specified. Not starting DSC config execution."
            return
        }

        $targetNodesRemaining = $ComputerName
        for ($i = 1; $i -le $RetryCount; $i++)
        {
            Trace-Execution "DSC attempt #$i. Starting DSC configuration on the following target nodes: $($targetNodesRemaining -join ', ')"
            $jobs = @()
            $mostRecentExceptions = @{}
            foreach ($targetNode in $targetNodesRemaining)
            {
                # Start-DscConfiguration returns a PS job, which we use to track the progress of the DSC configuration.
                # We do not pass the -Force parameter, which means that if another DSC configuration has pre-empted this attempt,
                # the job will be marked as Failed and we will retry on the next pass.
                $startParams = @{
                    JobName = $targetNode
                    CimSession = $sessionDcom | where ComputerName -eq $targetNode
                    UseExisting = $true
                    Verbose = $true
                    ErrorAction = "Stop"
                }

                $jobs += Start-DscConfiguration @startParams
            }

            # Wait for jobs for up to 30 minutes.
            $timeoutSeconds = 30 * 60
            Trace-Execution "Waiting for up to $timeoutSeconds seconds for jobs to complete on $($targetNodesRemaining -join ', ')"
            $jobs | Wait-Job -Timeout $timeoutSeconds
            foreach ($job in $jobs)
            {
                Trace-Execution "Output from job $($job.Name), which is in state: $($job.State):"
                $job.ChildJobs[0].Warning | Trace-Warning
                $job.ChildJobs[0].Verbose | Trace-Execution
                $job.ChildJobs[0].Error | Trace-Warning

                if ($job.State -eq "Completed")
                {
                    $nodeName = $job.Name
                    Trace-Execution "DSC configuration converged successfully on $($nodeName)."

                    if (-not $SkipTestDscConfiguration)
                    {
                        Trace-Execution "Validating DSC configuration on $($nodeName)."
                        $nodeSession = $sessionDcom | where ComputerName -eq $nodeName
                        $result = Test-DscConfiguration -Detailed -CimSession $nodeSession -Verbose

                        if (-not $result -or -not $result.InDesiredState) 
                        {
                            $msg = "Even though Start-DscConfiguration completed without errors, Test-DscConfiguration indicates that not all resources have converged on $($nodeName)."
                            $msg += "Resources not in desired state:`r`n$($result.ResourcesNotInDesiredState | Out-String)"
                            Trace-Execution $msg
                            $mostRecentExceptions[$nodeName] = $msg

                            continue
                        }
                    }
                    else
                    {
                        Trace-Execution "Additional validation (Test-DscConfiguration) skipped because -SkipTestDscConfiguration was specified."
                    }

                    Trace-Execution "DSC configuration in desired state on $($nodeName)."
                    $targetNodesRemaining = @( $targetNodesRemaining | where { $_ -ne $nodeName } )
                }
                elseif ($job.State -eq "Running")
                {
                    Trace-Execution "DSC configuration on $($job.Name) did not complete in $timeoutSeconds seconds. Stopping DSC configuration."
                    $job | Stop-Job
                }
                elseif ($job.State -eq "Failed")
                {
                    # Capturing exception here only. The rest of the output can is already traced above, so piping it to Out-Null.
                    $job | Receive-Job -ErrorVariable "jobError" -ErrorAction SilentlyContinue | Out-Null

                    # Since the session is using DCOM protocol, the detailed error message is not returned. We need to get the message from Windows event logs.
                    # There is no easy way to deterministically map the event with the DSC operation, so a workaround would be getting the latest five error message from the target node.
                    # The events are filtered by Event Id 4097 as this type of event gives us the most complete information of an error.
                    Start-Sleep 5 # give some time for Windows event logs to be processed.
                    $event = Get-WinEvent -LogName "Microsoft-Windows-Dsc/Operational" -ComputerName $job.Name -FilterXPath "*[System[(EventID=4097)]]"
                    $jobError += "`nLatest five DSC errors within $timeoutSeconds seconds in Windows Event logs on $($job.Name):"
                    $jobError += $event | where {$_.TimeCreated.AddSeconds($timeoutSeconds) -ge $job.PSBeginTime} | sort TimeCreated | select TimeCreated, Message -Last 5 | fl | Out-String

                    Trace-Execution "DSC configuration on $($job.Name) failed to converge. Exception from job:`r`n$jobError"
                    $mostRecentExceptions[$job.Name] = $jobError
                }
                else
                {
                    $msg = "Unexpected job state for $($job.Name). Details: $($job | fl * | Out-String)"
                    Trace-Execution $msg
                    $mostRecentExceptions[$job.Name] = $msg
                }
            }

            if (-not $targetNodesRemaining)
            {
                Trace-Execution "Completed DSC configuration on all target nodes: $($ComputerName -join ', ')"
                return
            }
        }
    }
    catch
    {
        $invocationException = $_
    }
    finally
    {
        $jobs | Stop-Job -ErrorAction Ignore
        $jobs | Remove-Job -Force -ErrorAction Ignore
        $sessionDcom | Remove-CimSession -ErrorAction Ignore
    }

    $failureMessage = $null
    
    if ($invocationException)
    {
        $failureMessage += "$($invocationException | Out-String)"
    }
    
    if ($targetNodesRemaining)
    {
        Trace-Execution "The following target nodes have failed to converge DSC configuration: $($targetNodesRemaining -join ',')"
        $failureMessage += "DSC failures:`r`n"
        foreach ($targetNode in $targetNodesRemaining)
        {
            if ($mostRecentExceptions.$targetNode)
            {
                $failureMessage += "$($targetNode): $($mostRecentExceptions.$targetNode)`r`n"
            }
            else
            {
                $failureMessage += "$($targetNode): DSC did not converge, DSC configuration was cancelled.`r`n"
            }
        }
    }

    if ($failureMessage)
    {
        throw $failureMessage
    }
}

<#
.SYNOPSIS
    Wait for LCM at the target session to be in the specified state (Idle by default).
    If the state is not reached within the specified timeout, this method throws an exception.
#>

function Wait-LCMState
{
    param
    (
        [Parameter(Mandatory=$true)]
        [CimSession[]]
        $CimSession,

        [int]
        $TimeoutMinutes = 30
    )

    function Trace-LCMState ([array] $ConfigManager)
    {
        $ConfigManager | % {
            Trace-Execution "$($_.PSComputerName): $($_.LCMState)"
        }
    }

    Trace-Execution "Waiting up to $TimeoutMinutes minutes for LCM on $($CimSession.ComputerName) to exit the Busy state."
    $configManager = @( Get-DscLocalConfigurationManager -CimSession $CimSession )

    $endTime = (Get-Date).AddMinutes($TimeoutMinutes)
    while ($configManager.LCMState -contains "Busy" -and ((Get-Date) -lt $endTime))
    {
        Trace-Execution "Waiting for LCM to exit Busy state..."
        Trace-LCMState $configManager

        Start-Sleep -Seconds 10
        $configManager = Get-DscLocalConfigurationManager -CimSession $CimSession
    }

    if ($configManager.LCMState -contains "Busy")
    {
        throw "LCM State still busy after $TimeoutMinutes minutes."
    }

    Trace-LCMState $configManager
}

function Test-WSManConnection
{
    <#
    .SYNOPSIS
    Tests if WSMan is operational on a machine.
 
    .DESCRIPTION
    Tests if wsmans is operational on a machine using WSMan Identify. This supports retries if there is an expectation that WSMan will come up on a machine after some time. TimeoutMs is the time to wait per attempt for WSMan to be ready.
 
    .EXAMPLE
    Test-WSManConnection -ComputerName $ComputerName -TimeoutMs 1000 -RetryCount 10
 
    .PARAMETER ComputerName
    The name of the computer(s) on which to test WinRM connectivity
 
    .PARAMETER RetryDelaySec
    The time in seconds to wait between attempts.
 
    .PARAMETER RetryCount
    The number of times to try connecting before giving up.
 
    .PARAMETER TimeoutMs
    The time in milliseconds to wait for WSMan to respond per attempt.
 
    #>

    [CmdletBinding()]
    PARAM (
    [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
    [string[]]
    $ComputerName,

    [Parameter(Mandatory=$false)]
    [long]
    $RetryDelaySec = 5,

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

    [Parameter(Mandatory=$false)]
    [long]
    $TimeoutMs = 5
    )

    $wsMan = New-Object -ComObject "WSMan.Automation"
    $connectionOptions = $wsMan.CreateConnectionOptions()
    $sessionFlags = $wsMan.SessionFlagUseNoAuthentication() -bor $wsman.SessionFlagUTF8()
    $remainingComputers = New-Object System.Collections.ArrayList
    $remainingComputers.AddRange($ComputerName) | Out-Null
    $currentTry = 0
    $totalTimeoutMs = ($RetryDelaySec * 1000 + $TimeoutMs * $ComputerName.LongLength) * $RetryCount
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

    try
    {
        while (($currentTry -lt ($RetryCount + 1)) -and ($remainingComputers.Count -gt 0) -and ($stopwatch.ElapsedMilliseconds -le $totalTimeoutMs))
        {
            $testComputers = New-Object System.Collections.ArrayList
            $testComputers.AddRange($remainingComputers) | Out-Null
            foreach ($testComputer in $testComputers)
            {
                try
                {
                    $iWSManSession = $wsMan.CreateSession($testComputer, $sessionFlags, $connectionOptions);
                    $iWSManSession.Timeout = $TimeoutMs
                    $iWSManSession.Identify(0) | Out-Null
                    $remainingComputers.Remove($testComputer)
                }
                catch [System.Exception]
                {
                    Trace-Execution "WSMan is not operational on $testComputer. Attempt $($currentTry + 1) of $($RetryCount + 1)."

                    if ( $currentTry -ge $RetryCount )
                    {
                        Trace-Execution ($_ | Format-List * | Out-String)
                    }
                }
            }

            $currentTry++

            if ($remainingComputers.Count -gt 0)
            {
                Start-Sleep -Seconds $RetryDelaySec
            }
        }

        return $remainingComputers
    }
    finally
    {
        if ( $wsMan )
        {
            $null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($wsMan)
            $wsMan = $null
        }
    }
}

function Test-PSSessionConnection
{
    <#
    .SYNOPSIS
    Test remote PS connectivity to one or more VMs and returns the names of any VMs for which the check failed.
 
    .DESCRIPTION
    Test remote PS connectivity to the VM. Use a PSJob to allow us to set a timeout on the operation as it can
    hang indefinitely if the VM is in a hung state.
 
    .EXAMPLE
    Test-PSSessionConnection -ComputerName $ComputerName -TimeoutMinutes 10
 
    .PARAMETER ComputerName
    The name of the computer(s) on which to test PSSession connectivity
 
    .PARAMETER TimeoutMinutes
    The time in minutes to wait for the PSJob to finish before timing out.
    #>

    [CmdletBinding()]
    PARAM (
    [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
    [string[]]
    $ComputerName,

    [Parameter(Mandatory=$false)]
    [int]
    $TimeoutMinutes = 3,

    [Parameter(Mandatory=$false)]
    [int]
    $Retries = 5
    )

    $ErrorActionPreference = "Stop"

    # Invoke-Command tests authentication and process creation.
    [system.collections.arraylist]$computerNameList = $computerName
    $attempts = 0
    while ($attempts -lt $Retries)
    {
        $attempts++
        Trace-Execution "Attempt $attempts/$Retries : Testing PSSession connectivity on $computerNameList."
        $successfulConnections = @()
        foreach ($name in $computerNameList)
        {
            Trace-Execution "Testing remote PS connectivity to $name with a timeout of $TimeoutMinutes minutes."
            try
            {
                $session = New-AzsSession -ComputerName $name -ErrorAction Stop
                $job = Invoke-Command -Session $session -ScriptBlock { $true } -ErrorAction Stop -AsJob
                $job | Wait-Job -Timeout ($TimeoutMinutes * 60) | Out-Null
                $jobDuration = [System.Math]::Ceiling(($job.PSEndTime - $job.PSBeginTime).TotalSeconds)
                if ($job.State -eq "Completed")
                {
                    $output = $job | Receive-Job
                    if ($output)
                    {
                        Trace-Execution "Remote PS connection to $name succeeded in $jobDuration seconds."
                        $successfulConnections += $name
                    }
                    else
                    {
                        Trace-Execution "Remote PS job to $name succeeded, but produced no output, so the job will be retried. Expected output: '$true'."
                    }
                }
                elseif ($job.State -eq "Failed")
                {
                    $job | Receive-Job -ErrorAction SilentlyContinue -ErrorVariable "exceptionDetails"

                    Trace-Execution "Remote PS connection to $name failed in $jobDuration seconds. Exception: $exceptionDetails"
                }
                else
                {
                    Trace-Execution "Remote PS connection to $name timed out after $TimeoutMinutes minutes."
                }
            }
            catch
            {
                Trace-Execution "Failure to create session or invoke a command to $name. Exception: $_"
            }
            finally
            {
                $session | Remove-PSSession -ErrorAction "Ignore"
            }
        }

        $successfulConnections | % {
            $computerNameList.Remove($_)
        }

        if ($computerNameList.Count -eq 0)
        {
            Trace-Execution "All VMs have PSSession connectivity."
            return $computerNameList
        }

        Start-Sleep -Seconds 30
    }

    Trace-Execution "Some VMs still do not have PSSession connectivity. These are: $computerNameList"
    return $computerNameList
}

function Test-HostPhysicalDiskSize([string[]] $PhysicalHostNames, [UInt64] $MinimumDiskSize)
{
    <#
    .SYNOPSIS
    Determine if physical hosts have boot disks as large or better than the minimum specified.
 
    .DESCRIPTION
    Certain configurations require sufficient physical disk space to be enabled.
    All drives are expected to be roughly equivalent, but all are checked anyway.
 
    .PARAMETER physicalHostNames
    The name of the computer(s) to check.
    Caller is expected to pass in only available nodes.
 
    .PARAMETER MinimumDiskSize
    Minimum disk size in bytes for test to evaluate true.
    #>


    [UInt64[]] $sizes = Invoke-Command -ComputerName $physicalHostNames `
    {
        $disk = Get-Disk | Where-Object { $_.BootFromDisk }
        return $disk.Size
    }

    # Compare all the returned sizes from hosts, and if any are smaller than minimum the test will fail.
    if ($sizes -lt $minimumDiskSize)
    {
        Trace-Warning "One or more physical host disks is below $($minimumDiskSize/1GB) GiB."
        return $false
    }
    else
    {
        return $true
    }
}

function Get-VMWSManDiagnostics
{
    <#
    .SYNOPSIS
    Capture diagnostic information after a remote command failure
 
    .DESCRIPTION
    If a caller is unable to reach a VM, this function will capture diagnostics information
    such as cluster node availablity, VM status and network connectivity from the caller to the VM
 
    .PARAMETER VMName
    The name of the computer to check.
    This is expected to be a single computer name from within the domain as a string.
 
    .PARAMETER HostClusterName
    The name of the cluster hosting the VM specified
    #>


    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [string]
        $VMName,

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

    # Cluster cmdlets and net cmdlets throw a nonterminating error, so we must still explicitly call ErrorAction on each command
    # This preference captures global powershell failures
    $ErrorActionPreference = "Stop"

    # If we fail to retrieve the cluster information, it should not fail ECE
    try
    {
        Trace-Execution "Cluster information for cluster '$HostClusterName' is:"
        $cluster = Get-Cluster $HostClusterName -ErrorAction Stop
        Trace-Execution "$($cluster | Format-Table | Out-String)"

        Trace-Execution "Cluster Node information is:"
        $clusterNodes = $cluster | Get-ClusterNode -ErrorAction Stop
        Trace-Execution "$($clusterNodes | Format-Table | Out-String)"

        Trace-Execution "Cluster Group information is:"
        $clusterGroups = $cluster | Get-ClusterGroup -ErrorAction Stop
        Trace-Execution "$($clusterGroups | Format-Table | Out-String)"

        Trace-Execution "Cluster Resource information is:"
        $clusterResources = $cluster | Get-ClusterResource -ErrorAction Stop
        Trace-Execution "$($clusterResources | Format-Table | Out-String)"

        Trace-Execution "Hyper-V VM information is:"
        $hypervVM = Get-VM -ComputerName $clusterNodes -ErrorAction Stop
        Trace-Execution "$($hypervVM | Format-Table | Out-String)"
    }
    catch
    {
        Trace-Warning "An error occurred while retrieving HostCluster information: $_"
    }

    # Likewise, failing to test remote connection should not fail ECE
    # Note that these commands rely on open ports in the destination VM's firewall (ex, ICMP) that may be disabled in the future
    # These cmdlets raise them as nonterminating errors, so they will not break execution of the try block
    try
    {
        Trace-Execution "Attempting a ping to $VMName"
        $pingResult = Test-Connection $VMName -ErrorAction Stop
        Trace-Execution "$($pingResult | Format-Table | Out-String)"

        Trace-Execution "Tracing route to $VMName"
        $traceRouteResult = tracert $VMName -ErrorAction Stop
        Trace-Execution "$($traceRouteResult | Format-Table | Out-String)"

        Trace-Execution "Attempting a NetConnection via WinRM"
        $winRMTestResults = Test-NetConnection -CommonTCPPort WinRM -ComputerName $VMName -ErrorAction Stop
        Trace-Execution "$($winRMTestResults | Format-Table | Out-String)"
    }
    catch
    {
        Trace-Warning "An error occurred while testing remote connectivity to '$VMName': $_"
    }
}

function Set-DriveLetterForSpecificLabel
{
     <#
    .SYNOPSIS
    Sets the specified drive letter for the partition with given file system label.
 
    .DESCRIPTION
    While attaching a VHD to an offline VM, it is not possible to set the default drive letter.
    This cmdlet could be called from the VM after it comes online to set the drive letter of a partition using the file system label.
 
    .PARAMETER FileSystemLabel
    The file system label for which the drive letter needs to be set.
 
    .PARAMETER DriveLetter
    The new drive letter to be set for the FileSystemLabel.
    #>

   
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [string]
        $FileSystemLabel,

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

    # List of existing volumes
    $volumes = Get-Volume
    Trace-Execution "List of existing volumes: $($volumes | out-string)"

    # If file system label volume does not exist.
    $volume = $volumes | Where-Object FileSystemLabel -ieq $FileSystemLabel
    if([string]::IsNullOrEmpty($volume))
    {
        Trace-Warning "A volume with file system label '$FileSystemLabel' does not exist."
        return
    }

    # If file system label partition does not exist.
    $partition = $volume | Get-Partition
    Trace-Execution "Partition with '$FileSystemLabel' file system label: $($partition | out-string)"
    if([string]::IsNullOrEmpty($partition.DriveLetter))
    {
        Trace-Warning "A partition for file system label '$FileSystemLabel' does not exist."
        return
    }

    # If drive letter is already in use.
    if (Test-Path -Path "$($DriveLetter + ':')")
    {
        Trace-Warning "Drive letter '$DriveLetter' is already in use. Cannot reuse it without removing existing drive."
        return
    }

    # Set new drive letter for partiion with given file system label
    Trace-Execution "The current partition for file system label '$FileSystemLabel' exists at drive letter '$($partition.DriveLetter)'."
    Trace-Execution "Setting the partition with file system label '$FileSystemLabel' to drive letter '$DriveLetter'."
    Set-Partition -DriveLetter $partition.DriveLetter -NewDriveLetter $DriveLetter -Verbose -ErrorAction Stop
}


<#
.Synopsis
    Stops a service with retry logic.
    This function first tries to stop a service gracefully and waits for it to exit a pending state
    with retry logic up to 60 seconds. It also support forcefully stopping it if enabled.
#>

function Stop-ServiceWithRetries
{
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.String]
        $ServiceName,

        [Switch]
        $Force
    )

    $retryCount = 0
    $ServiceNotStopped = $true
    while ($retryCount -lt 7 -and $ServiceNotStopped)
    {
        try {

            $service = get-service -Name $ServiceName -ErrorAction Ignore
            if ($service -eq $null)
            {
                Trace-Execution "Service not found."
                return
            }
            Trace-Execution "Found Service in $($service.Status)"

            switch ($service.Status)
            {
                { "Running", "Paused" -contains $_ } {
                    Stop-Service -Name $ServiceName
                    $ServiceNotStopped = $false
                    break
                }
                "Stopped" {
                    $ServiceNotStopped = $false
                    break
                }
                default {
                    start-sleep -Seconds 10
                }
            }
        }
        catch {
            Trace-Warning "Failed to stop service : $($_.Exception.Message)"
        }
        finally
        {
           $retryCount++
        }
    }

    $service = Get-Service -Name $ServiceName
    if ($service.Status -ne "Stopped" -and $Force)
    {
        Write-Log ("Failure stopping service '{0}'" -f $ServiceName)
        try
        {
            Write-Log("Attempting to stop underlying process of service '{0}'" -f $ServiceName)
            $process = Get-WmiObject -Class Win32_Service -Filter "Name='$ServiceName'" | Select-Object

            if($process -ne $null)
            {
                Stop-Process -Id $process.ProcessId -Force

                $stoppedProcess = Get-WmiObject -Class Win32_Service -Filter "Name='$ServiceName'" | Select-Object

                if($stoppedProcess.State -eq "Stopped")
                {
                    Write-Log ("Process '{0}' with id '{1}' stopped" -f $process.Name,$process.ProcessId)

                    $retries = 0
                    while($retries -lt 5)
                    {
                        try
                        {
                            # Tell SCM to stop the service again so that it doesn't restart after we kill the underlying process (if startup type is automatic)
                            Stop-Service $ServiceName -Force
                            break
                        }
                        catch
                        {
                            Write-Log("Failure trying to stop service '{0}' after stopping process '{1}'. Retrying..." -f $ServiceName,$process.Name)
                            Start-Sleep -Seconds 3
                            $retries++
                        }
                    }
                    Stop-Service $ServiceName -Force
                    $service = Get-Service -Name $ServiceName

                    if($service.Status -ne "Stopped")
                    {
                        throw "Failed to stop service $ServiceName after process stop. Exiting..."
                    }
                    else
                    {
                        Write-Log("Process '{0}' and service '{1}' stopped successfully." -f $process.Name,$ServiceName)
                    }
                }
            }
            else
            {
                Write-Log("No underlying process of service '{0}' found. Failured to stop service.")
            }
        }
        catch
        {
            Write-Log ("Failure stopping process '{0}'. Message: '{1}'" -f $process.Name,$_.Exception.Message)
            throw
        }
    }
}

# Helper function to create execution context XML for node-wise operation.
# This would mainly be used in conditional action expansion for custom execution context.
# Arbitrary custom XML can still be passed directly in the conditional evaluation function if needed.
function New-ExecutionContextXmlForNode
{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String[]]
        $NodeNames,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $RoleName,

        [Parameter(Mandatory = $false)]
        [System.String]
        $RolePath
    )

    Trace-Execution "Constructing execution context XML with NodeName: '$NodeNames', RoleName: '$RoleName', RolePath: '$RolePath'"

    $nodesExecutionContext = @"
<ExecutionContext>
  <Roles>
    <Role RoleName="$RoleName">
      <Nodes />
    </Role>
  </Roles>
</ExecutionContext>
"@


    $ecXml = [xml]$nodesExecutionContext
    if ($RolePath)
    {
        # RolePath is not strictly required (also not easily constructed from the script)
        $ecXml.SelectSingleNode("ExecutionContext/Roles/Role").SetAttribute("RolePath", $RolePath) | Out-Null
    }

    foreach ($node in $NodeNames)
    {
        $nodeElement = $ecXml.CreateElement("Node")
        $nodeElement.SetAttribute("Name", $node) | Out-Null
        $ecXml.SelectSingleNode("ExecutionContext/Roles/Role/Nodes").AppendChild($nodeElement) | Out-Null
    }

    return $ecXml.OuterXml
}

<#
.SYNOPSIS
    Imports certificate from pfx file if it's not installed.
#>

function Import-PfxCertificateSafe
{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [string]
        $FilePath,

        [Parameter(Mandatory=$true)]
        [SecureString]
        $Password,

        [Parameter(Mandatory=$false)]
        [string]
        $CertStoreLocation = "Cert:\LocalMachine\My",

        [Parameter(Mandatory=$false)]
        [Switch]
        $Exportable 
    )

    $ErrorActionPreference = "Stop"
    $certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($FilePath, $Password)
    if (($existingCert = Get-ChildItem $CertStoreLocation | Where Thumbprint -EQ $certificate.Thumbprint ))
    {
        Trace-Execution "Certificate '$CertStoreLocation\$($certificate.Thumbprint)' already imported from location '$FilePath'" -Verbose
        $existingCert
    }
    else
    {
        if ($Exportable)
        {
            Import-PfxCertificate -FilePath $FilePath -CertStoreLocation $CertStoreLocation -Password $Password -Exportable -Verbose -ErrorAction Stop
        }
        else
        {
            Import-PfxCertificate -FilePath $FilePath -CertStoreLocation $CertStoreLocation -Password $Password -Verbose -ErrorAction Stop
        }

        if (($installedCertificate = Get-ChildItem $CertStoreLocation | Where Thumbprint -EQ $certificate.Thumbprint ))
        {
            Trace-Execution "Successfully installed Certificate '$CertStoreLocation\$($installedCertificate.Thumbprint)' from location '$FilePath'" -Verbose
        }
        else
        {
            throw "Import-PfxCertificate failed to install certificate from location '$FilePath'"
        }
    }
}
function Install-RoleNugetPackages

{    
    [CmdletBinding()]
    Param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $NugetStorePath = "$env:SystemDrive\CloudDeployment\NuGetStore",
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Destination = "$env:SystemDrive\NuGetStore"
    )     
    
    if(!(Test-Path $destination))
    {
        md $destination -Force
    }

    #Install the ProductNugets manifest package
    Expand-NugetContent -NugetName 'Microsoft.AzureStack.Solution.Deploy.ProductNugets'  -SourcePath '' -DestinationPath $destination -Verbose:$VerbosePreference -IsNugetInstall 
    $productNugetXmlPath = Join-Path -Path (Get-ASArtifactPath -NugetName 'Microsoft.AzureStack.Solution.Deploy.ProductNugets') -ChildPath 'ProductNuGets.xml'

    if(!(Test-Path -path $productNugetXmlPath))
    {
        #To support backward compatibility for ProductNugets package between Solution-Deploy package and AsZ-Assembly package.
        $productNugetXmlPath = Join-Path -Path (Get-ASArtifactPath -NugetName 'Microsoft.AzureStack.Solution.Deploy.ProductNugets') -ChildPath 'content\ProductNuGets.xml'
    }

    $productNugetXml = [xml](Get-Content -Path $productNugetXmlPath)

    $roleNugetPackageNames = ($productNugetXml.Manifest.Packages.NuGetPackage| where {$_.ECERole -eq 'true'} ).Name
    foreach($roleNugetName in $roleNugetPackageNames)
    {
        Expand-NugetContent -NugetName $roleNugetName -SourcePath '' -DestinationPath $destination -NugetStorePath $NugetStorePath -Verbose:$VerbosePreference -IsNugetInstall
    }
}

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

    # If an end step is specified, propagate that information to the invocation after reboot.
    $endStepValue = $Parameters.RunInformation.EndStep
    if ( ($endStepValue) -and ($endStepValue -ne 'MAX') )
    {
        $endStepSpecification = "-End $endStepValue "
    }

    $retriesValue = $Parameters.RunInformation.Retries
    if ( ($retriesValue) )
    {
        $retriesValueSpecification = "-Retries $retriesValue "
    }

    $ecEngineModulePath = (Resolve-Path -Path "$PSScriptRoot\..\ECEngine\EnterpriseCloudEngine.psd1").Path
    $callbackString = @"
                        Import-Module $ecEngineModulePath
                        Invoke-EceAction -RolePath Cloud -ActionType CloudDeployment -Rerun $endStepSpecification $retriesValueSpecification -Verbose
"@


    return $callbackString
}


Export-ModuleMember -Function Install-RoleNugetPackages
Export-ModuleMember -Function ConnectPSSession
Export-ModuleMember -Function Disable-CredSSP
Export-ModuleMember -Function Disable-RemoteClientCredSSP
Export-ModuleMember -Function Dismount-Wim
Export-ModuleMember -Function Enable-AutoLogon
Export-ModuleMember -Function Enable-CredSSP
Export-ModuleMember -Function Expand-NugetContent
Export-ModuleMember -Function Find-LockedFiles
Export-ModuleMember -Function Get-NugetStorePath
Export-ModuleMember -Function Get-NugetVersions
Export-ModuleMember -Function Get-VMWSManDiagnostics
Export-ModuleMember -Function Import-PfxCertificateSafe
Export-ModuleMember -Function Initialize-ECESession
Export-ModuleMember -Function Initialize-NugetScript
Export-ModuleMember -Function Invoke-ECECommand
Export-ModuleMember -Function Mount-Wim
Export-ModuleMember -Function Mount-WindowsImageWithRetry
Export-ModuleMember -Function New-Credential
Export-ModuleMember -Function New-ExecutionContextXmlForNode
Export-ModuleMember -Function New-RegistryPropertyNameValue
Export-ModuleMember -Function PublishAndStartDscConfiguration
Export-ModuleMember -Function Reset-RestartCallback
Export-ModuleMember -Function Restart-Machine
Export-ModuleMember -Function Set-DriveLetterForSpecificLabel
Export-ModuleMember -Function Set-RestartCallback
Export-ModuleMember -Function Stop-ServiceWithRetries
Export-ModuleMember -Function Test-HostPhysicalDiskSize
Export-ModuleMember -Function Test-PSSessionConnection
Export-ModuleMember -Function Test-WSManConnection
Export-ModuleMember -Function Test-WSmanForCredSSP
Export-ModuleMember -Function Trace-Error
Export-ModuleMember -Function Trace-Execution
Export-ModuleMember -Function Trace-Warning
Export-ModuleMember -Function Wait-Result
Export-ModuleMember -Function Create-CallBackStringForAsZ
# SIG # Begin signature block
# MIInlgYJKoZIhvcNAQcCoIInhzCCJ4MCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCRtsVtqKuc2njA
# 0zLbxaJynKke9PnufwlsSJBLRulHQqCCDXYwggX0MIID3KADAgECAhMzAAADTrU8
# esGEb+srAAAAAANOMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMwMzE2MTg0MzI5WhcNMjQwMzE0MTg0MzI5WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDdCKiNI6IBFWuvJUmf6WdOJqZmIwYs5G7AJD5UbcL6tsC+EBPDbr36pFGo1bsU
# p53nRyFYnncoMg8FK0d8jLlw0lgexDDr7gicf2zOBFWqfv/nSLwzJFNP5W03DF/1
# 1oZ12rSFqGlm+O46cRjTDFBpMRCZZGddZlRBjivby0eI1VgTD1TvAdfBYQe82fhm
# WQkYR/lWmAK+vW/1+bO7jHaxXTNCxLIBW07F8PBjUcwFxxyfbe2mHB4h1L4U0Ofa
# +HX/aREQ7SqYZz59sXM2ySOfvYyIjnqSO80NGBaz5DvzIG88J0+BNhOu2jl6Dfcq
# jYQs1H/PMSQIK6E7lXDXSpXzAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUnMc7Zn/ukKBsBiWkwdNfsN5pdwAw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMDUxNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAD21v9pHoLdBSNlFAjmk
# mx4XxOZAPsVxxXbDyQv1+kGDe9XpgBnT1lXnx7JDpFMKBwAyIwdInmvhK9pGBa31
# TyeL3p7R2s0L8SABPPRJHAEk4NHpBXxHjm4TKjezAbSqqbgsy10Y7KApy+9UrKa2
# kGmsuASsk95PVm5vem7OmTs42vm0BJUU+JPQLg8Y/sdj3TtSfLYYZAaJwTAIgi7d
# hzn5hatLo7Dhz+4T+MrFd+6LUa2U3zr97QwzDthx+RP9/RZnur4inzSQsG5DCVIM
# pA1l2NWEA3KAca0tI2l6hQNYsaKL1kefdfHCrPxEry8onJjyGGv9YKoLv6AOO7Oh
# JEmbQlz/xksYG2N/JSOJ+QqYpGTEuYFYVWain7He6jgb41JbpOGKDdE/b+V2q/gX
# UgFe2gdwTpCDsvh8SMRoq1/BNXcr7iTAU38Vgr83iVtPYmFhZOVM0ULp/kKTVoir
# IpP2KCxT4OekOctt8grYnhJ16QMjmMv5o53hjNFXOxigkQWYzUO+6w50g0FAeFa8
# 5ugCCB6lXEk21FFB1FdIHpjSQf+LP/W2OV/HfhC3uTPgKbRtXo83TZYEudooyZ/A
# Vu08sibZ3MkGOJORLERNwKm2G7oqdOv4Qj8Z0JrGgMzj46NFKAxkLSpE5oHQYP1H
# tPx1lPfD7iNSbJsP6LiUHXH1MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGXYwghlyAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAANOtTx6wYRv6ysAAAAAA04wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEINSdtsGBTWPj9IDwKIfyi1PT
# vwGzA8XO6mzpj5hJzkVZMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAjyg0aB1R6YdQzCm6ya4/8yyV/y6fZAjw3Q8HY4droyO+Qtz55dHbL2mm
# aQYrbgjsdiPVySlJWlw/CtrXo2IQWmpp8uo5eHVjF4fGZZA7TN2Yl5prsJMfww2a
# Bed8ZWhIOnN+64c+0HYrAj/+ErYxTxfvmUbPN8oQtG9p0ew5rduJzJ5HjCu/OMn7
# vZyasT9DCGvRV9P3CkQfOWdWuLkpppO2g0pbcpe66xYzDppyk5b2iRVeELlLw7tX
# 1Vy9uHpyywpHNYqTMZo8r1AqkS2BkgmQCSRM5VOMNrLHMSm5Hho33Po3qqMnAWrM
# TIW85tjLlq3SJVHz0njyr/IVvobSnqGCFwAwghb8BgorBgEEAYI3AwMBMYIW7DCC
# FugGCSqGSIb3DQEHAqCCFtkwghbVAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFRBgsq
# hkiG9w0BCRABBKCCAUAEggE8MIIBOAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCC7am9nADrOznFXvVTa1XrmQ1neP0a8et9kseOWbB00uQIGZMl+wlhZ
# GBMyMDIzMDgwMzA4MjA0NC45ODFaMASAAgH0oIHQpIHNMIHKMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l
# cmljYSBPcGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpBRTJDLUUz
# MkItMUFGQzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCC
# EVcwggcMMIIE9KADAgECAhMzAAABv99uuQQVUihYAAEAAAG/MA0GCSqGSIb3DQEB
# CwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH
# EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV
# BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTIyMTEwNDE5MDEy
# NFoXDTI0MDIwMjE5MDEyNFowgcoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMx
# JjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkFFMkMtRTMyQi0xQUZDMSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAuExh0n1UxKMzBvkPHer47nryD4UK2GVy1X6bOVC+hLVh
# DlsIWQ1uX/9a8IRI3zXo/y1oTDuj+rJHyX4OZQn42E0iu7x6swPvM34zIOSPn8lg
# nWzGEAsRtz9zBrLW9+4w/YhWlXI8hvc7ovqupuL3TXte8BbmNOUDSL+Ou2bBfObG
# zsH3yY/BELvqwO13KZ9Z1OxKacnqq1u9E9Rhai90STog22lR2MVRSx55FHi/emnZ
# A/IKvsAtEH2K6JmgOyQ7/mDQrWNEA5roUjhQqLQw1/3wz/CIvc9+FPxX2dxR0nvv
# Ye5VLqv8Q99cOkO6z6V4stGDyFDuO8CwtiSvCC3QrOOugAl33aPD9YZswywWRk+Y
# GyLI+Fw+kCCUY6h1qOjTj5glz0esmds3ue45WaI2hI9usForM8gy//5tDZXj0KKU
# 1BxA04xpfEy91RZUbc6pdAvEkpYrN2jlpXhMvTD7pgdYyxkVSaWZv7kWp5y9NjWP
# /CTDGXTC6DWiGcXwPQO66QdVNWxuiGdpfPaEUnWXcKnDVua1khBAxO4m9wg/1qM6
# f7HwXf/pHifMej+qB7SUZOiJScX+1HmffmZRAFiJXS0qUDk0ZAZW3oX2xLyl0044
# eHI7Y95GPaw8OlSTeNiNAKl+MyH5OaifsUuyVHOf4rsrE+ZyAuS9e9ERqu5H/10C
# AwEAAaOCATYwggEyMB0GA1UdDgQWBBRVAolUT3eV3wK/+Luf/wawCPMYpzAfBgNV
# HSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5o
# dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBU
# aW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwG
# CCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRz
# L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNV
# HRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IC
# AQAjCREvjT6yXwJYdvkFUqTGGh6RizAY+ciuB6UOBUm0yqq5QC+5pCEa9WSMvbUG
# zxDCEFBgD93gWGnkiyYcHCazlgZK+E7WxtI3bP++Fb4RJZiWLo/IC9hX12hCZZwY
# XIGVzC9BVAcNx/zsFqI/9u8u/bhGjDHPad47C4OQNCHrkNqzGYxb4GQq6Psw6o7c
# Ety3MU3Jd4uzBazaFhPRvmBfSn+Ufd6pTNZLgIX9BjrLmZblc/d2LIAurEr5W29W
# fW5RMRIEZzO9TaMr/zzdmW/cV6VdaDTygy5g4O3UXadt1DraUpn5jcD10TVWNnyz
# /paeleHojrGCCksqexpelMkUsiYP0HX9pFUgNglWU10r1wEzFwZM9aX2Rqq3fFRr
# N3gu8tCX+H1nKK2AobW1vmsKLTH6PyX1LkyvRwTj45a1paeHIR8TGzm3+iY7wpC1
# MHuzqAqAdDeaIVdVlch807VJJ4hDive6AiOQCV9MwiUyhf5v4P8jTGof8CqjDb3P
# nLlNSnFm2BFhMZ35oNTEosc37GZHScM83hTN1E481sLYJrrhhcdtcyNB60juMjqG
# UD6uQ/7DbMvtv93tFj5WjxVhMCkkY66EEYgpfFLOCb2ngJJWFuJCIGsCiDfDxGwE
# 4RVYAnoFzoa2OfSqijYg2drdZfpptRRvKxMsAzu3oxkS/TCCB3EwggVZoAMCAQIC
# EzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS
# b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoX
# DTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0
# b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh
# dGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC
# 0/3unAcH0qlsTnXIyjVX9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VG
# Iwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP
# 2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/P
# XfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361
# VI/c+gVVmG1oO5pGve2krnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwB
# Sru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9
# X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269e
# wvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDw
# wvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr
# 9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+e
# FnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAj
# BgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+n
# FV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEw
# PwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9j
# cy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3
# FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAf
# BgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBH
# hkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNS
# b29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUF
# BzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0Nl
# ckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4Swf
# ZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTC
# j/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu
# 2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/
# GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3D
# YXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbO
# xnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqO
# Cb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I
# 6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0
# zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaM
# mdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNT
# TY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggLOMIICNwIBATCB+KGB0KSBzTCByjEL
# MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v
# bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWlj
# cm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEmMCQGA1UECxMdVGhhbGVzIFRTUyBF
# U046QUUyQy1FMzJCLTFBRkMxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFNlcnZpY2WiIwoBATAHBgUrDgMCGgMVADgEd+JNrp4dpvFKMZi91txbfic3oIGD
# MIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV
# BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG
# A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEF
# BQACBQDodU62MCIYDzIwMjMwODAzMDU1MjU0WhgPMjAyMzA4MDQwNTUyNTRaMHcw
# PQYKKwYBBAGEWQoEATEvMC0wCgIFAOh1TrYCAQAwCgIBAAICCw0CAf8wBwIBAAIC
# EhwwCgIFAOh2oDYCAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAK
# MAgCAQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOBgQAlgSPrXAwb
# DHwKImAnyK1n6ONHJS+1YrBXgzSFlj6xf6J4/RUxkK78tJVepluq07slTwMypXGM
# JBSC/7SQkZPCcdrvNeSHrYFPpuD4DovHH0xBQ/6hr299XQy1/OrfBwAuAm1eVT73
# 0qXiYR2Y81XpIvbL0pmtElR8iJy8c6e5xDGCBA0wggQJAgEBMIGTMHwxCzAJBgNV
# BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w
# HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m
# dCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABv99uuQQVUihYAAEAAAG/MA0GCWCG
# SAFlAwQCAQUAoIIBSjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZI
# hvcNAQkEMSIEIJz2PupRvpCC2muXwh3Ewp+fR6kESIxLsT6tlhaDWijZMIH6Bgsq
# hkiG9w0BCRACLzGB6jCB5zCB5DCBvQQg/Q4tRz63EiRj4K+19yNUwogBIOsp44CI
# uBfnZHCvBa4wgZgwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAIT
# MwAAAb/fbrkEFVIoWAABAAABvzAiBCBpSVdmsfmVWBB+AYsk2RK6VyKfK87V+I1U
# rbAAylRoHDANBgkqhkiG9w0BAQsFAASCAgAggTMm9Lje7NhUNgQtPcztlKe9Ozq/
# 4HfObQANhamEyq6WQLw9iRK3LJyrFIZl8FFTkQp35mVtLYKN5CdISlfCSuaiyMpA
# v60GDveEa0rDQXKStffa5F6lDPeiE7huk37uTjZEYhgsY6LulA/hdAHcKNq+0Xlv
# GIu2YVeFP3xQ8y4DLwFl2sSDjqXRuIYtCapst2EyCnF+t/V/Dn/tx3WC10YrlNGJ
# 3niSYh8Z2xaC21FbAa6xrO5KxqOCqfkcadW9npxdKr8iXIX48FxIHEwMvXvuIpz8
# Hnt+s/rWaLy16vuij4m0eiQy//5wF50Ndm9qxHOJL7Dx9HaeYz0QIXwMcwoeKDw3
# aw64lVAeH7Hx+VziqVMp+PvB43lT+Ot4SsLP9o7px6q9RP4dGdJleahAaXOqraHv
# Ml4/DlYJj1pI+eB9eVhIZStk/7GoGYa6rUMhx9LIubGr1HyKB7gM6tTbJ78e7LA2
# uqkcI0+ISjyW3BpHnQgGpXH0/b21fAB0hWjTsxWl+qpzNmNHyrjBkvKa9TBSRbyq
# RQVmdBqoo8uF2voT/ZWCBmiTVOfICT0BSFrdpEumD8bQzTOoK3w4tf1YJEV89BLV
# zTc5dElW7bOtjgqDZ181NG8TAINcks4xBmI5FJzvE8v1qFcLHTQfG0q7DjhE0foZ
# 5p14qZtOUlPCdw==
# SIG # End signature block