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 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
}

function Initialize-ECESession
{
    [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 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:LocalRootFolderPath\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 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
    )
    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)
                    }

                    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 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
        }
    }
}

<#
.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 -NugetStorePath $NugetStorePath -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' -or $_.InstallInBoostrap -eq 'true'} ).Name
    foreach($roleNugetName in $roleNugetPackageNames)
    {
        Expand-NugetContent -NugetName $roleNugetName -SourcePath '' -DestinationPath $destination -NugetStorePath $NugetStorePath -Verbose:$VerbosePreference -IsNugetInstall
    }
}

function New-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
}

<#
 .SYNOPSIS
  TEMP helper function for investigating binary corruption in FE build
#>

function Test-BinaryHash
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string] $FileSystemRoot,

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

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

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

    $FileSystemRoot =  $FileSystemRoot.TrimEnd('\')
    Trace-Execution "Test-BinaryHash with FileSystemRoot: $FileSystemRoot, OutputFile: $OutputFileName, BaselineFile: $BaselineFileName, ThrowIfCorrupted: $ThrowIfCorrupted"

    # A pre-defined list of folders to scan
    $foldersToScan = @(
        @{ RelativePath = '\Windows\cluster'; Recursive = $true; Include = @('*.exe','*.dll') },
        @{ RelativePath = '\Windows\System32\drivers\*'; Recursive = $false; Include = @('*.sys')},
        @{ RelativePath = '\Windows\System32\wbem\*'; Recursive = $false; Include = @('*.dll')},
        @{ RelativePath = '\Program Files\WindowsPowerShell\Modules'; Recursive = $true; Include = @('*.dll','*.ps*')}
    )

    # Whether set baseline or test against baseline
    $setBaseline = $OutputFileName -eq $BaselineFileName

    # Read hashes from baseline file, skip test if baseline file not exists
    $baselineHashes = $null
    if (-not $setBaseline)
    {
        $baselineHashFile = Join-Path $FileSystemRoot $BaselineFileName
        if (Test-Path $baselineHashFile)
        {
            $baselineHashes = Get-Content -Path $baselineHashFile | ConvertFrom-Json
        }
        else
        {
            Trace-Execution "Baseline file not found: $baselineHashFile, skip testing."
            return
        }
    }

    $outPutHashFile = Join-Path $FileSystemRoot $OutputFileName
    $outPutHashes = @{}
    $errorMessages = ''
    $hashTested = 0
    $hashFailed = 0

    foreach ($folder in $foldersToScan) {
        $path = Join-Path $FileSystemRoot $folder.RelativePath
        $recursive =  $folder.Recursive
        $include = $folder.Include

        Get-ChildItem -Path $path -File -Recurse:$recursive -Include $include | % {
            # Compute file hash
            $fileHash = $_ | Get-FileHash -Algorithm MD5

            $key = $fileHash.Path.Substring($fileSystemRoot.Length)
            $value = $fileHash.Hash
            $outPutHashes[$key] = $value

            # Compare with baseline
            if ($baselineHashes -and $baselineHashes.$key)
            {
                if($baselineHashes.$key -ne $value)
                {
                    $errorMessages += "`n$key file hash does not match the baseline."
                    $hashFailed++
                }
                $hashTested++
            }
        }
    }

    # Save hashes to output file
    $outPutHashes | ConvertTo-Json | Out-File -FilePath $outPutHashFile

    Trace-Execution "Test-BinaryHash completed with HashOutputed: $($outPutHashes.Count), HashTested: $hashTested, HashFailed: $hashFailed"

    if ($errorMessages)
    {
        if ($ThrowIfCorrupted) {
            throw $errorMessages
        } else {
            Trace-Warning $errorMessages
        }
    }

}

<#
 .SYNOPSIS
 Wrapper for [Environment]::SetEnvironmentVariable to bypass ConstrainedLanguage issue
#>

function Set-Env {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string] $VariableName,

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

        [Parameter(Mandatory=$false)]
        [System.EnvironmentVariableTarget] $Target = [System.EnvironmentVariableTarget]::Machine
        )

        [Environment]::SetEnvironmentVariable($variableName, $variableValue, $target)
}

function Update-ECEAgentSecrets {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [hashtable]
        $EceSecrets,

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

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

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

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

        [Switch]
        $Disable
    )
    # Deprecated
}

<#
 .SYNOPSIS
 Wrapper for force flushing registry keys to disk.
#>

function Flush-RegistryKeysToDisk
{
$regFlusKeySignature = @"
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
public static extern int RegFlushKey(IntPtr hKey);
"@

    $ErrorActionPreference = "Stop"
    $advapi32 = Add-Type -MemberDefinition $regFlusKeySignature -Name "AdvApi32" -Namespace "RegistryFunctions" -PassThru

    $hklm = [Microsoft.Win32.Registry]::LocalMachine
    $classRoot = [Microsoft.Win32.Registry]::ClassesRoot
    $currentConfig = [Microsoft.Win32.Registry]::CurrentConfig
    $currentUser = [Microsoft.Win32.Registry]::CurrentUser

    Trace-Execution "Flushing LocalMachine, ClassesRoot, CurrentConfig and CurrentUser reg hives"
    if ($advapi32::RegFlushKey($hklm.Handle.DangerousGetHandle()) -ne 0)
    {
        Trace-Error "Unable to flush hklm hive"
    }

    if ($advapi32::RegFlushKey($classRoot.Handle.DangerousGetHandle()) -ne 0)
    {
        Trace-Error "Unable to flush classRoot hive"
    }

    if ($advapi32::RegFlushKey($currentConfig.Handle.DangerousGetHandle()) -ne 0)
    {
        Trace-Error "Unable to flush currentConfig hive"
    }

    if ($advapi32::RegFlushKey($currentUser.Handle.DangerousGetHandle()) -ne 0)
    {
        Trace-Error "Unable to flush currentUser hive"
    }
    Trace-Execution "Successfully flushed LocalMachine, ClassesRoot, CurrentConfig and CurrentUser reg hives"
}

<#
 .SYNOPSIS
  Helper function to get Deployment Launch type
#>

function Get-DeploymentLaunchype()
{
    $ErrorActionPreference = "Stop"
    $deploymentLaunchType = "NonCloudDeployment"
    $stampInformationPath = "HKLM:\Software\Microsoft\AzureStackStampInformation"
    try
    {
        if (Test-Path $stampInformationPath)
        {
            $deploymentLaunchType = (Get-ItemProperty -Path $stampInformationPath -Name DeploymentLaunchType).DeploymentLaunchType
        }
    }
    catch
    {
        # if deployment launch type not set, return default
        Write-Verbose "Deployment Launch type not set: $( $_.Exception.Message )"
    }
    return $deploymentLaunchType
}

Export-ModuleMember -Function Install-RoleNugetPackages
Export-ModuleMember -Function ConnectPSSession
Export-ModuleMember -Function Expand-NugetContent
Export-ModuleMember -Function Import-PfxCertificateSafe
Export-ModuleMember -Function Initialize-ECESession
Export-ModuleMember -Function Invoke-ECECommand
Export-ModuleMember -Function Mount-WindowsImageWithRetry
Export-ModuleMember -Function New-Credential
Export-ModuleMember -Function New-ExecutionContextXmlForNode
Export-ModuleMember -Function PublishAndStartDscConfiguration
Export-ModuleMember -Function Stop-ServiceWithRetries
Export-ModuleMember -Function Test-WSManConnection
Export-ModuleMember -Function Trace-Error
Export-ModuleMember -Function Trace-Execution
Export-ModuleMember -Function Trace-Warning
Export-ModuleMember -Function Wait-Result
Export-ModuleMember -Function New-CallBackStringForAsZ
Export-ModuleMember -Function Test-BinaryHash
Export-ModuleMember -Function Set-Env
Export-ModuleMember -Function Update-ECEAgentSecrets
Export-ModuleMember -Function Flush-RegistryKeysToDisk
Export-ModuleMember -Function Get-DeploymentLaunchype
# SIG # Begin signature block
# MIInvwYJKoZIhvcNAQcCoIInsDCCJ6wCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCvzzTx/mkYoxaf
# cxEJC24hEw5HJV5sjmrtny0hH/N8YKCCDXYwggX0MIID3KADAgECAhMzAAADrzBA
# DkyjTQVBAAAAAAOvMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwOTAwWhcNMjQxMTE0MTkwOTAwWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDOS8s1ra6f0YGtg0OhEaQa/t3Q+q1MEHhWJhqQVuO5amYXQpy8MDPNoJYk+FWA
# hePP5LxwcSge5aen+f5Q6WNPd6EDxGzotvVpNi5ve0H97S3F7C/axDfKxyNh21MG
# 0W8Sb0vxi/vorcLHOL9i+t2D6yvvDzLlEefUCbQV/zGCBjXGlYJcUj6RAzXyeNAN
# xSpKXAGd7Fh+ocGHPPphcD9LQTOJgG7Y7aYztHqBLJiQQ4eAgZNU4ac6+8LnEGAL
# go1ydC5BJEuJQjYKbNTy959HrKSu7LO3Ws0w8jw6pYdC1IMpdTkk2puTgY2PDNzB
# tLM4evG7FYer3WX+8t1UMYNTAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQURxxxNPIEPGSO8kqz+bgCAQWGXsEw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMTgyNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAISxFt/zR2frTFPB45Yd
# mhZpB2nNJoOoi+qlgcTlnO4QwlYN1w/vYwbDy/oFJolD5r6FMJd0RGcgEM8q9TgQ
# 2OC7gQEmhweVJ7yuKJlQBH7P7Pg5RiqgV3cSonJ+OM4kFHbP3gPLiyzssSQdRuPY
# 1mIWoGg9i7Y4ZC8ST7WhpSyc0pns2XsUe1XsIjaUcGu7zd7gg97eCUiLRdVklPmp
# XobH9CEAWakRUGNICYN2AgjhRTC4j3KJfqMkU04R6Toyh4/Toswm1uoDcGr5laYn
# TfcX3u5WnJqJLhuPe8Uj9kGAOcyo0O1mNwDa+LhFEzB6CB32+wfJMumfr6degvLT
# e8x55urQLeTjimBQgS49BSUkhFN7ois3cZyNpnrMca5AZaC7pLI72vuqSsSlLalG
# OcZmPHZGYJqZ0BacN274OZ80Q8B11iNokns9Od348bMb5Z4fihxaBWebl8kWEi2O
# PvQImOAeq3nt7UWJBzJYLAGEpfasaA3ZQgIcEXdD+uwo6ymMzDY6UamFOfYqYWXk
# ntxDGu7ngD2ugKUuccYKJJRiiz+LAUcj90BVcSHRLQop9N8zoALr/1sJuwPrVAtx
# HNEgSW+AKBqIxYWM4Ev32l6agSUAezLMbq5f3d8x9qzT031jMDT+sUAoCw0M5wVt
# CUQcqINPuYjbS1WgJyZIiEkBMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGZ8wghmbAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIGCv5vl/UPtDmYqzjJfKLdUN
# NZNSad2htPIa5lkVWYHhMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAfth0eadHsdCnPGVrJIIsjllkydhxku4cdwE4luFmf8cDqs5BTRmmP7mV
# oz5bpWW4x4Pfss6Td83Dxidh8QqvPlW4gZ0a1AcoYsPqida64bZBHb0wRy9tjwju
# CecjEekHO98dlvWO9VZhI1Bja4bbiyMIE1QQ9ZaxmuVDNOm31yPygTHndWJq+SQB
# Rv3VzfCor+ja9rpSl0PDONheEbm2xqHdswveA0KcvAEOD8ElDixsVrrdPPpVDZMt
# crAyAmhbNFjVwwQYf4LvTy5zTk/yF+NlicAPF4cyMw0Lo+d0QtavNCr5Wt2nGkZM
# 12Ckzp/N05iEDvAcDxiMCWpe+BtloqGCFykwghclBgorBgEEAYI3AwMBMYIXFTCC
# FxEGCSqGSIb3DQEHAqCCFwIwghb+AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsq
# hkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCBimAmKmEE30SNB3DFy4xo4a3UU0WxJHx8Tx9oxcAceAgIGZjOqtK6Z
# GBMyMDI0MDUxNjE4NDUyOS42OTZaMASAAgH0oIHYpIHVMIHSMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNO
# OjNCRDQtNEI4MC02OUMzMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT
# ZXJ2aWNloIIReDCCBycwggUPoAMCAQICEzMAAAHlj2rA8z20C6MAAQAAAeUwDQYJ
# KoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjMx
# MDEyMTkwNzM1WhcNMjUwMTEwMTkwNzM1WjCB0jELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl
# cmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjozQkQ0LTRC
# ODAtNjlDMzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKl74Drau2O6LLrJO3HyTvO9
# aXai//eNyP5MLWZrmUGNOJMPwMI08V9zBfRPNcucreIYSyJHjkMIUGmuh0rPV5/2
# +UCLGrN1P77n9fq/mdzXMN1FzqaPHdKElKneJQ8R6cP4dru2Gymmt1rrGcNe800C
# cD6d/Ndoommkd196VqOtjZFA1XWu+GsFBeWHiez/PllqcM/eWntkQMs0lK0zmCfH
# +Bu7i1h+FDRR8F7WzUr/7M3jhVdPpAfq2zYCA8ZVLNgEizY+vFmgx+zDuuU/GChD
# K7klDcCw+/gVoEuSOl5clQsydWQjJJX7Z2yV+1KC6G1JVqpP3dpKPAP/4udNqpR5
# HIeb8Ta1JfjRUzSv3qSje5y9RYT/AjWNYQ7gsezuDWM/8cZ11kco1JvUyOQ8x/JD
# kMFqSRwj1v+mc6LKKlj//dWCG/Hw9ppdlWJX6psDesQuQR7FV7eCqV/lfajoLpPN
# x/9zF1dv8yXBdzmWJPeCie2XaQnrAKDqlG3zXux9tNQmz2L96TdxnIO2OGmYxBAA
# ZAWoKbmtYI+Ciz4CYyO0Fm5Z3T40a5d7KJuftF6CToccc/Up/jpFfQitLfjd71cS
# +cLCeoQ+q0n0IALvV+acbENouSOrjv/QtY4FIjHlI5zdJzJnGskVJ5ozhji0YRsc
# v1WwJFAuyyCMQvLdmPddAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQU3/+fh7tNczEi
# fEXlCQgFOXgMh6owHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYD
# VR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j
# cmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwG
# CCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIw
# MjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD
# CDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBADP6whOFjD1ad8Gk
# EJ9oLBuvfjndMyGQ9R4HgBKSlPt3pa0XVLcimrJlDnKGgFBiWwI6XOgw82hdolDi
# MDBLLWRMTJHWVeUY1gU4XB8OOIxBc9/Q83zb1c0RWEupgC48I+b+2x2VNgGJUsQI
# yPR2PiXQhT5PyerMgag9OSodQjFwpNdGirna2rpV23EUwFeO5+3oSX4JeCNZvgyU
# OzKpyMvqVaubo+Glf/psfW5tIcMjZVt0elswfq0qJNQgoYipbaTvv7xmixUJGTbi
# xYifTwAivPcKNdeisZmtts7OHbAM795ZvKLSEqXiRUjDYZyeHyAysMEALbIhdXgH
# Eh60KoZyzlBXz3VxEirE7nhucNwM2tViOlwI7EkeU5hudctnXCG55JuMw/wb7c71
# RKimZA/KXlWpmBvkJkB0BZES8OCGDd+zY/T9BnTp8si36Tql84VfpYe9iHmy7Pqq
# xqMF2Cn4q2a0mEMnpBruDGE/gR9c8SVJ2ntkARy5SfluuJ/MB61yRvT1mUx3lypp
# O22ePjBjnwoEvVxbDjT1jhdMNdevOuDeJGzRLK9HNmTDC+TdZQlj+VMgIm8ZeEIR
# NF0oaviF+QZcUZLWzWbYq6yDok8EZKFiRR5otBoGLvaYFpxBZUE8mnLKuDlYobjr
# xh7lnwrxV/fMy0F9fSo2JxFmtLgtMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJ
# mQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNh
# dGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1
# WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEB
# BQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjK
# NVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhg
# fWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJp
# rx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/d
# vI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka9
# 7aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKR
# Hh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9itu
# qBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyO
# ArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItb
# oKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6
# bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6t
# AgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQW
# BBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacb
# UzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYz
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnku
# aHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIA
# QwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2
# VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwu
# bWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEw
# LTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYt
# MjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/q
# XBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6
# U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVt
# I1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis
# 9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTp
# kbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0
# sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138e
# W0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJ
# sWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7
# Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0
# dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQ
# tB1VM1izoXBm8qGCAtQwggI9AgEBMIIBAKGB2KSB1TCB0jELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxh
# bmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjoz
# QkQ0LTRCODAtNjlDMzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIjCgEBMAcGBSsOAwIaAxUA942iGuYFrsE4wzWDd85EpM6RiwqggYMwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF
# AOnwnA4wIhgPMjAyNDA1MTYyMjUxNThaGA8yMDI0MDUxNzIyNTE1OFowdDA6Bgor
# BgEEAYRZCgQBMSwwKjAKAgUA6fCcDgIBADAHAgEAAgIInjAHAgEAAgISkzAKAgUA
# 6fHtjgIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAID
# B6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAEhfyUrk9b+9Kt+lrPpg
# YJxi+4pmWZTyg0Jk7bSudJ+76TwxvqPRrLYEK/G1CblUW8RxFX+Bee8qMdJ95oyz
# OJdqExPTRtJLsMvybfn3+NBhyv2bMtPnZTf+9S+qP3nvJ9BLwV4wsRGB3qkN+Our
# YvbMgsgYU5X7sKIZfD8pT9W/MYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgUENBIDIwMTACEzMAAAHlj2rA8z20C6MAAQAAAeUwDQYJYIZIAWUDBAIB
# BQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQx
# IgQgfzbrStPIBKao8wjTIicKbHGLP9Xm6ldT3NL9VhWSbc4wgfoGCyqGSIb3DQEJ
# EAIvMYHqMIHnMIHkMIG9BCAVqdP//qjxGFhe2YboEXeb8I/pAof01CwhbxUH9U69
# 7TCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB5Y9q
# wPM9tAujAAEAAAHlMCIEILQN5mDnlRhubNmBSFAU07ycaLGMWiQzOaGluwUwxnMX
# MA0GCSqGSIb3DQEBCwUABIICAIQl5/VOekvP8f3oSzBFFYF6o1ewCbhHsvuDZU60
# f3LGcLl8kl69JDvkDYVoi2oHKxIP+Cp7S2qjkP6qmlVYgf51kbPjWbXbK9dSe3kb
# 5WLK+DHrKUpAcib17OaL9KDmU286Q6o2KtfPtdD9iy+/9e1jMEo5+phlqf/mS7Hk
# M6Si9+ntbCD8nET6EbGl/vqQWIsLMtEZ6jH3FWcfKR7QvtGqJJ7NxWedpGfoKwc2
# 1Eq54GQMTiJXEWVPyPk4x3qxCdjkPdf8bTkyqitF/kcQmIe2qrKp/szGzsL2jZoB
# oBCnYvOoCARRaouijv9LFNBd6oFu2cMh9GaTqGBRAqVjGK6rsPq+0CMPizFCb/t6
# mLUOYsBODErusa3QTG1E9XRAA3zBV6gA14j8CwE4uQd/Jnlbqbke3HH1fP206dyA
# p579gsx6hMpqSaa6COTG1c+lNCloJbpUVqtIFutKhtJi1MDqVEjG+xrj8JQ2tk5M
# +IZ9gptmszCpukg5IRp6wieBv0GwQw490Nctc6SwsI6qZD/H9Ay1pt2brB5mCyVP
# 5eQiPFqmSMS2M1MjucyFHQ4RYRENcQ+QwqAZMZGMhoxiTwSVYQvCX1D47orZPje8
# 7n24rE/ZPKf6IKzSwpTCFXQ9daOoirlKO8Lw4dXjxwuI6KBn599QWv9Umf8lWK+N
# 4Cxb
# SIG # End signature block