Azs.Deployment.Admin.psm1

#-----------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
#-----------------------------------------------------------------------

# This module contains PowerShell commands providing an access to Deployment Provider functions.

class CustomResponse {
    [string]$StatusCode
    [string]$AsyncOperationStatusUri
    [string]$LocationUri
    [string]$Content
}

class WaitResult {
    [bool]$IsSuccess
    [string]$ErrorCode
    [String]$ErrorMessage
}

<#
.SYNOPSIS
    Retrieves Resource Manager access token.
#>

function Get-AzsResourceManagerAccessToken {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [object] $context
    )

    $profile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile

    $profileClient = [Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient]::new($profile)

    $token = $profileClient.AcquireAccessToken($context.Subscription.TenantId)

    return $token.AccessToken
}

<#
.SYNOPSIS
    Send a request to Azure Stack Resource Manager.
#>

function Invoke-AzsResourceManager {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('GET', 'PUT', 'POST', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE')]
        [string] $Method,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [Uri] $Uri,

        [Parameter(Mandatory = $false)]
        [object] $Body = $null,

        [Parameter(Mandatory = $false)]
        [string] $AccessToken = "",

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

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

    function Resolve-RequestUri {
        param (
            [string] $resourceManagerUrl,
            [Uri] $uri
        )

        if ($uri.IsAbsoluteUri) {
            return $uri
        }

        return [uri]::new([uri]::new($resourceManagerUrl), $Uri)
    }

    function Resolve-RequestContent {
        param (
            [object] $body
        )

        if ($null -eq $body) {
            return [NullString]::Value
        }

        if ($body -is [string]) {
            return $Body.ToString()
        }

        return ($body | ConvertTo-Json -Depth 99 -Compress)
    }

    function Resolve-AccessToken {
        param(
            [object] $context,
            [string] $accessToken
        )

        if (-not [string]::IsNullOrEmpty($accessToken)) {
            return $accessToken
        }

        return Get-AzsResourceManagerAccessToken -Context $context
    }

    function Get-HeaderValue {
        param (
            [System.Net.Http.Headers.HttpHeaders] $headers,
            [string] $name
        )

        [System.Collections.Generic.IEnumerable[string]] $values = $null

        if (-not $headers.TryGetValues($name, [ref] $values)) {
            return [NullString]::Value
        }

        return [System.Linq.Enumerable]::FirstOrDefault($values)
    }

    function Trace-HttpRequestMessage {
        param (
            [System.Net.Http.HttpRequestMessage] $request,
            [string] $content
        )

        Write-Verbose "$($request.Method) $($request.RequestUri) with $($content.Length)-char payload" -Verbose

        $sb = [System.Text.StringBuilder]::new()
        $sb.AppendLine("$($request.Method) $($request.RequestUri) HTTP/$($request.Version)") | Out-Null

        DumpHttpMessageHeaders $sb $request.Headers

        if (-not [string]::IsNullOrEmpty($content)) {
            $sb.AppendLine() | Out-Null
            $sb.Append($content) | Out-Null
        }

        Write-Debug $sb.ToString()
    }

    function Trace-HttpResponseMessage {
        param (
            [System.Net.Http.HttpResponseMessage] $response,
            [string] $content
        )

        Write-Verbose "Received $($content.Length)-char response, StatusCode = $($response.StatusCode)" -Verbose

        $sb = [System.Text.StringBuilder]::new()
        $sb.AppendLine("HTTP/$($response.Version) $([int]$response.StatusCode) $($response.ReasonPhrase)") | Out-Null

        DumpHttpMessageHeaders -Sb $sb -Headers $response.Headers

        if (-not [string]::IsNullOrEmpty($content)) {
            $sb.AppendLine() | Out-Null
            $sb.Append($content) | Out-Null
        }

        Write-Debug $sb.ToString()
    }

    function DumpHttpMessageHeaders {
        param (
            [System.Text.StringBuilder] $sb,
            [System.Net.Http.Headers.HttpHeaders] $headers
        )

        if ($null -ne $headers) {
            foreach ($header in $headers) {
                $sb.Append($header.Key) | Out-Null
                $sb.Append(": ") | Out-Null

                if ($header.Key -eq 'Authorization') {
                    $sb.AppendLine('HIDDEN') | Out-Null
                }
                else {
                    $sb.AppendLine($header.Value -join " ") | Out-Null
                }
            }
        }
    }

    #-----------------------------------------------------------------------
    
    $ctx = Get-AzContext

    if ($null -eq $ctx.Environment) {
        throw 'AzContext is not set.'
    }

    $Uri = Resolve-RequestUri -ResourceManagerUrl $ctx.Environment.ResourceManagerUrl -Uri $Uri

    [string] $requestContent = Resolve-RequestContent -Body $Body

    $AccessToken = Resolve-AccessToken -Context $ctx -AccessToken $AccessToken

    [System.Net.Http.HttpRequestMessage] $request = $null
    [System.Net.Http.HttpResponseMessage] $response = $null

    $retryable = $RetryOnError;
    $attemptCount = 1;
    $maxAttemptCount = 3;
    try {
        do {
        $request = [System.Net.Http.HttpRequestMessage]::new()
        $request.Method = [System.Net.Http.HttpMethod]::new($Method)
        $request.RequestUri = $Uri
        $request.Headers.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new('Bearer', $AccessToken)

        if ($null -ne $requestContent) {
            $request.Content = [System.Net.Http.StringContent]::new($requestContent, [System.Text.Encoding]::UTF8, 'application/json')
        }

        Trace-HttpRequestMessage -Request $request -Content $requestContent

        $task = $HttpClient.SendAsync($request, [System.Net.Http.HttpCompletionOption]::ResponseContentRead)
        $response = $task.Result

        $task = $response.Content.ReadAsStringAsync()
        [string] $responseContent = $task.Result

        if ([string]::IsNullOrEmpty($responseContent)) {
            $responseContent = [NullString]::Value
        }

        Trace-HttpResponseMessage -Response $response -Content $responseContent

        $result = [CustomResponse]::new()
        $result.StatusCode = $response.StatusCode

        if ($result.StatusCode -eq ""){
            $result.StatusCode = "RequestTimeout"
        } else {
            $result.AsyncOperationStatusUri = Get-HeaderValue -Headers $response.Headers -Name 'Azure-AsyncOperation'
            $result.LocationUri = Get-HeaderValue -Headers $response.Headers -Name 'Location'
            $result.Content = $responseContent
        }

        $retriableError = IsRetryableError -StatusCode $result.StatusCode
        if ($retryable -and $retriableError) {
            [string] $statusCode = $result.StatusCode
            Write-Verbose "Retryable error occured: ${statusCode}, retrying with attempt count number ${attemptCount}." -Verbose
            # Progresive backoff in case of a retryable error.
            $waitTime  = 5 * $attemptCount;
            Start-Sleep -Seconds $waitTime

            # Should the next attempt be retryable or not? After the
            $attemptCount++;
            $retryable = $attemptCount -le $maxAttemptCount
            Write-Verbose "retryable: ${retryable}" -Verbose
        } else {
        if ($ThrowOnError) {
            EnsureSuccessStatusCode -Response $result
        }

        return $result
    }

     # loop until the retry attempts are exhausted
    } until ($false);
}
    catch [System.AggregateException] {
        throw $_.Exception.InnerException.Message
    }
    finally {
        if ($null -ne $request) {
            $request.Dispose()
        }

        if ($null -ne $response) {
            $response.Dispose()
        }
    }
}

<#
.SYNOPSIS
    Waits for Azure Stack Resource Manager asynchronous operation to complete (Azure-AsyncOperation header style).
 
.NOTES
    Track asynchronous Azure operations
    https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-manager-async-operations
#>

function Wait-AzsAsyncOperation {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $OperationName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [Uri] $AsyncOperationStatusUri,

        [Parameter(Mandatory = $false)]
        [string] $AccessToken = ""
    )

    Write-Verbose "${OperationName}: Wait for asynchronous operation to complete." -Verbose

    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

    while ($true) {
        $response = Invoke-AzsResourceManager -Method GET -Uri $AsyncOperationStatusUri -AccessToken $AccessToken -Verbose -RetryOnError

        EnsureSuccessStatusCode -Response $response

        $operationResult = $response.Content | ConvertFrom-Json

        if (IsOperationResultTerminalState $operationResult.status) {
            $result = [WaitResult]::new()
            if ($operationResult.status -eq 'Succeeded') {
                $result.IsSuccess = $true
                return  $result
            }

            $result.IsSuccess = $false
            $result.ErrorCode = $operationResult.error.code
            $result.ErrorMessage = $operationResult.error.message
            return $result
        }

        Write-Verbose "${OperationName}: Sleeping for 5 seconds, waiting time: $($stopwatch.Elapsed)"

        Start-Sleep -Seconds 5
    }
}

function EnsureSuccessStatusCode {
    param(
        [Parameter(Mandatory = $true)]
        [psobject] $Response
    )

    if (-not (IsSuccessStatusCode -StatusCode $Response.StatusCode)) {
        Write-Verbose "HTTP error: $($Response.StatusCode)" -Verbose
        Write-Verbose $Response.Content -Verbose

        throw "HTTP error: $($Response.StatusCode)"
    }
}

function IsOperationResultTerminalState {
    param (
        [Parameter(Mandatory = $true)]
        [string] $Value
    )

    return $Value -in @('Canceled', 'Failed', 'Succeeded')
}

function IsSuccessStatusCode {
    param(
        [Parameter(Mandatory = $true)]
        [System.Net.HttpStatusCode] $StatusCode
    )

    return [int]$StatusCode -ge 200 -and [int]$StatusCode -le 299
}

<#
.SYNOPSIS
    Check if the status code is a retryable error
 
.NOTES
    List of retryable status code:
    408 // RequestTimeout
    429 // TooManyRequests (RFC 6585)
    500 // InternalServerError
    502 // BadGateway
    503 // ServiceUnavailable
    504 // GatewayTimeout
    506..599
#>

function IsRetryableError {
    param(
        [Parameter(Mandatory = $true)]
        [System.Net.HttpStatusCode] $StatusCode
    )
    switch([int]$statusCode)
    {
        408 {return $True}
        429 {return $True}
        500 {return $True}
        502 {return $True}
        503 {return $True}
        504 {return $True}
        {$_-ge 506 -and $_-le 599} {return $True}
        default {return $False}
    }
}

function ThrowOnError {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [WaitResult] $WaitResult,

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

    if (-not ($WaitResult.IsSuccess)) {
        throw "$ProblemDescription, errorCode: '$($WaitResult.ErrorCode)', errorMessage: '$($WaitResult.ErrorMessage)'"
    }
}

#-----------------------------------------------------------------------

<#
.SYNOPSIS
    Lists file containers or gets a file container properties.
.DESCRIPTION
    Lists file containers or gets a file container properties.
.PARAMETER FileContainerId
    Container ID to fetch the properties for.
.PARAMETER AsJson
    Outputs the result in Json format.
.EXAMPLE
    PS C:\> Get-AzsFileContainer
    Lists the available file containers in the subscription.
.EXAMPLE
    PS C:\> Get-AzsFileContainer -FileContainerId <ContainerID>
    Get the file container with id <ContainerID>.
#>

function Get-AzsFileContainer {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $FileContainerId = $null,

        [Parameter()]
        [ValidateSet('2019-01-01', '2018-07-01')]
        [string] $ApiVersion = '2019-01-01',

        [Parameter()]
        [switch] $AsJson
    )

    $subscriptionId = (Get-AzContext).Subscription.Id

    if ([string]::IsNullOrEmpty($FileContainerId)) {
        $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/fileContainers?api-version=$ApiVersion"
    }
    else {
        $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/fileContainers/$($FileContainerId)?api-version=$ApiVersion"
    }

    $response = Invoke-AzsResourceManager -Method GET -Uri $requestUri -Verbose -RetryOnError

    if ($response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) {
        return $null
    }

    EnsureSuccessStatusCode -Response $response

    if ($AsJson) {
        return $response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 99
    }

    return $response.Content | ConvertFrom-Json
}

<#
.SYNOPSIS
    Creates a new file container.
.DESCRIPTION
    Creates a new file container from a soucre Uri.
.PARAMETER FileContainerId
    Container ID to be given to the new container.
.PARAMETER SourceUri
    The remote file location URI for the container.
.PARAMETER PostCopyAction
    The file post copy action.
.EXAMPLE
    PS C:\> New-AzsFileContainer -FileContainerId $ContainerId -SourceUri $packageUri -PostCopyAction Unzip
    Creates a new file container from the specified values.
#>

function New-AzsFileContainer {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $FileContainerId,

        [Parameter(Mandatory = $true)]
        [Uri] $SourceUri,

        [Parameter()]
        [ValidateSet('None', 'Unzip')]
        [string] $PostCopyAction = 'None',

        [Parameter()]
        [ValidateSet('2019-01-01', '2018-07-01')]
        [string] $ApiVersion = '2019-01-01'
    )

    Write-Verbose "Create a new file container, fileContainerId = '$FileContainerId', sourceUri = '$SourceUri', postCopyAction = '$PostCopyAction'." -Verbose

    $subscriptionId = (Get-AzContext).Subscription.Id
    $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/fileContainers/$($FileContainerId)?api-version=$ApiVersion"

    $body = @{
        properties = @{
            sourceUri      = $SourceUri
            postCopyAction = $PostCopyAction
        }
    }

    $response = Invoke-AzsResourceManager -Method PUT -Uri $requestUri -Body $body -ThrowOnError -Verbose

    if (-not [string]::IsNullOrEmpty($response.AsyncOperationStatusUri)) {
        $waitAsyncOperation = Wait-AzsAsyncOperation -OperationName 'New-AzsFileContainer' -AsyncOperationStatusUri $response.AsyncOperationStatusUri -Verbose
        ThrowOnError -WaitResult $waitAsyncOperation -ProblemDescription 'Unable to create file container'
    }
}

<#
.SYNOPSIS
    Removes an existing file container.
.DESCRIPTION
    Removes an existing file container.
.PARAMETER FileContainerId
    Container ID of the container to be removed.
.EXAMPLE
    PS C:\> Remove-AzsFileContainer -FileContainerId $ContainerId
    Removes an existing file container.
#>

function Remove-AzsFileContainer {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $FileContainerId,

        [Parameter()]
        [ValidateSet('2019-01-01', '2018-07-01')]
        [string] $ApiVersion = '2019-01-01'
    )

    Write-Verbose "Remove the file container, fileContainerId = '$FileContainerId'." -Verbose

    $subscriptionId = (Get-AzContext).Subscription.Id
    $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/fileContainers/$($FileContainerId)?api-version=$ApiVersion"

    Invoke-AzsResourceManager -Method DELETE -Uri $requestUri -ThrowOnError -Verbose | Out-Null
}

# Product Packages

<#
.SYNOPSIS
    Lists product packages or gets a product package properties.
.DESCRIPTION
    Lists product packages or gets a product package properties.
.PARAMETER PackageId
    Product package Id to get the properties for.
.PARAMETER AsJson
    Outputs the result in Json format.
.EXAMPLE
    PS C:\> Get-AzsProductPackage
    Lists all the product packages in the subscription.
.EXAMPLE
    PS C:\> Get-AzsProductPackage -PackageId $PackageId
    Gets the product package properties of the product with Id.
#>

function Get-AzsProductPackage {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $PackageId = $null,

        [Parameter()]
        [ValidateSet('2019-01-01', '2018-07-01')]
        [string] $ApiVersion = '2019-01-01',

        [Parameter()]
        [switch] $AsJson
    )

    $subscriptionId = (Get-AzContext).Subscription.Id

    if ([string]::IsNullOrEmpty($PackageId)) {
        $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/productPackages?api-version=$ApiVersion"
    }
    else {
        $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/productPackages/$($PackageId)?api-version=$ApiVersion"
    }

    $response = Invoke-AzsResourceManager -Method GET -Uri $requestUri -Verbose -RetryOnError

    if ($response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) {
        return $null
    }

    EnsureSuccessStatusCode -Response $response

    if ($AsJson) {
        return $response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 99
    }

    return $response.Content | ConvertFrom-Json
}

<#
.SYNOPSIS
    Create a new product package.
.DESCRIPTION
    Create a new product package.
.PARAMETER PackageId
    ID of the product package to be created.
.PARAMETER FileContainerId
    File container resource identifier.
.EXAMPLE
    PS C:\> New-AzsProductPackage -PackageId $PackageId -FileContainerId $ContainerId
    Creates a product package with the specified values.
#>

function New-AzsProductPackage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $PackageId,

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

        [Parameter()]
        [ValidateSet('2019-01-01', '2018-07-01')]
        [string] $ApiVersion = '2019-01-01'
    )

    Write-Verbose "Create a new product package, packageId = '$PackageId', fileContainerId = '$FileContainerId'." -Verbose

    $subscriptionId = (Get-AzContext).Subscription.Id
    $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/productPackages/$($PackageId)?api-version=$ApiVersion"

    if ($ApiVersion -eq '2019-01-01') {
        $body = @{
            properties = @{
                fileContainerId = $FileContainerId
            }
        }
    }
    else {
        $body = @{
            properties = @{
                productManifestId = $FileContainerId
            }
        }
    }

    $response = Invoke-AzsResourceManager -Method PUT -Uri $requestUri -Body $body -ThrowOnError -Verbose

    if (-not [string]::IsNullOrEmpty($response.AsyncOperationStatusUri)) {
        $waitAsyncOperation = Wait-AzsAsyncOperation -OperationName 'New-AzsProductPackage' -AsyncOperationStatusUri $response.AsyncOperationStatusUri -Verbose
        ThrowOnError -WaitResult $waitAsyncOperation -ProblemDescription 'Unable to create product package'
    }
}

<#
.SYNOPSIS
    Removes an existing product package.
.DESCRIPTION
    Removes an existing product package.
.PARAMETER PackageId
    ID of the product package to be removed.
.EXAMPLE
    PS C:\> Remove-AzsProductPackage -PackageId $PackageId
    Removes a product package with Id $PackageId.
#>

function Remove-AzsProductPackage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $PackageId,

        [Parameter()]
        [ValidateSet('2019-01-01', '2018-07-01')]
        [string] $ApiVersion = '2019-01-01'
    )

    Write-Verbose "Remove the product package, packageId = '$PackageId'." -Verbose

    $subscriptionId = (Get-AzContext).Subscription.Id
    $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/productPackages/$($PackageId)?api-version=$ApiVersion"

    Invoke-AzsResourceManager -Method DELETE -Uri $requestUri -ThrowOnError -Verbose | Out-Null
}

#-----------------------------------------------------------------------

<#
.SYNOPSIS
    Lists product deployments or gets a product deployment properties.
.DESCRIPTION
    Lists product deployments or gets a product deployment properties.
.PARAMETER ProductId
    Product package Id to get the product deployment properties for.
.PARAMETER AsJson
    Outputs the result in Json format.
.EXAMPLE
    PS C:\> Get-AzsProductDeployment
    Lists all the product package deployments in the subscription.
.EXAMPLE
    PS C:\> Get-AzsProductDeployment -ProductId $ProductId
    Gets the product package deployment with the specified product Id.
#>

function Get-AzsProductDeployment {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $ProductId = $null,

        [Parameter()]
        [switch] $AsJson
    )

    $subscriptionId = (Get-AzContext).Subscription.Id

    if ([string]::IsNullOrEmpty($ProductId)) {
        $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/productDeployments?api-version=2019-01-01"
    }
    else {
        $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/productDeployments/$($ProductId)?api-version=2019-01-01"
    }

    $response = Invoke-AzsResourceManager -Method GET -Uri $requestUri -Verbose -RetryOnError

    if ($response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) {
        return $null
    }

    EnsureSuccessStatusCode -Response $response

    if ($AsJson) {
        return $response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 99
    }

    return $response.Content | ConvertFrom-Json
}

<#
.SYNOPSIS
    Invokes 'bootstrap product' action.
.DESCRIPTION
    Invokes 'bootstrap product' action.
.PARAMETER ProductId
    Product package Id to start the bootstrap action for.
.PARAMETER Version
    Product version
.EXAMPLE
    PS C:\> Invoke-AzsProductBootstrapAction -ProductId $ProductId -Version $ProductVersion
    Starts the bootstrap action for the specified product.
#>

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

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

    $subscriptionId = (Get-AzContext).Subscription.Id
    $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/productDeployments/$ProductId/bootstrap?api-version=2019-01-01"

    $body = @{
        version = $Version
    }

    $response = Invoke-AzsResourceManager -Method POST -Uri $requestUri -Body $body -ThrowOnError -Verbose

    $waitAsyncOperation = Wait-AzsAsyncOperation -OperationName 'Invoke-AzsProductBootstrapAction' -AsyncOperationStatusUri $response.AsyncOperationStatusUri -Verbose
    ThrowOnError -WaitResult $waitAsyncOperation -ProblemDescription 'Unable to complete bootstrap operation'
}

<#
.SYNOPSIS
    Invokes 'deploy product' action.
.DESCRIPTION
    Invokes 'deploy product' action.
.PARAMETER ProductId
    Product package Id to start the deploy action for.
.PARAMETER Version
    Product Version.
.PARAMETER Parameters
    Deployment parameters, value in JToken
.EXAMPLE
    PS C:\> Invoke-AzsProductDeployAction -ProductId $ProductId -Version $ProductVersion -Parameters $Parameters
    Starts the product deploy action for the specified product.
#>

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

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

        [Parameter(Mandatory = $true)]
        [psobject] $Parameters
    )

    $subscriptionId = (Get-AzContext).Subscription.Id
    $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/productDeployments/$ProductId/deploy?api-version=2019-01-01"

    $body = @{
        version    = $Version
        parameters = $Parameters
    }

    $response = Invoke-AzsResourceManager -Method POST -Uri $requestUri -Body $body -ThrowOnError -Verbose

    $waitAsyncOperation = Wait-AzsAsyncOperation -OperationName 'Invoke-AzsProductDeployAction' -AsyncOperationStatusUri $response.AsyncOperationStatusUri -Verbose
    ThrowOnError -WaitResult $waitAsyncOperation -ProblemDescription 'Unable to complete deploy operation'
}

<#
.SYNOPSIS
    Invokes 'execute runner' action.
.DESCRIPTION
    Invokes 'execute runner' action.
.PARAMETER ProductId
    Product package Id to start the execute runner action for.
.PARAMETER Parameters
    Deployment parameters, value in JToken
.EXAMPLE
    PS C:\> Invoke-AzsProductExecuteRunnerAction -ProductId $ProductId -Parameters $Parameters
    Starts the product execute runner action for the specified product.
#>

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

        [Parameter(Mandatory = $true)]
        [psobject] $Parameters
    )

    $subscriptionId = (Get-AzContext).Subscription.Id
    $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/productDeployments/$ProductId/executeRunner?api-version=2019-01-01"

    $body = $parameters

    $response = Invoke-AzsResourceManager -Method POST -Uri $requestUri -Body $body -ThrowOnError -Verbose

    if (-not [string]::IsNullOrEmpty($response.AsyncOperationStatusUri)) {
        $waitAsyncOperation = Wait-AzsAsyncOperation -OperationName 'Invoke-AzsProductExecuteRunnerAction' -AsyncOperationStatusUri $response.AsyncOperationStatusUri -Verbose
        ThrowOnError -WaitResult $waitAsyncOperation -ProblemDescription 'Unable to complete execute runner operation'
    }
}

<#
.SYNOPSIS
    Invokes 'remove product' action.
.DESCRIPTION
    Invokes 'remove product' action.
.PARAMETER ProductId
    Product package Id to start the remove product action for.
.EXAMPLE
    PS C:\> Invoke-AzsProductRemoveAction -ProductId $ProductId
    Starts the product remove action for the specified product.
#>

function Invoke-AzsProductRemoveAction {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $ProductId
    )

    $subscriptionId = (Get-AzContext).Subscription.Id
    $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/productDeployments/$ProductId/remove?api-version=2019-01-01"

    $response = Invoke-AzsResourceManager -Method POST -Uri $requestUri -ThrowOnError -Verbose

    if (-not [string]::IsNullOrEmpty($response.AsyncOperationStatusUri)) {
        $waitAsyncOperation = Wait-AzsAsyncOperation -OperationName 'Invoke-AzsProductRemoveAction' -AsyncOperationStatusUri $response.AsyncOperationStatusUri -Verbose
        ThrowOnError -WaitResult $waitAsyncOperation -ProblemDescription 'Unable to complete remove operation'
    }
}

<#
.SYNOPSIS
    Invokes 'rotate secrets' action.
.DESCRIPTION
    Invokes 'rotate secrets' action.
.PARAMETER ProductId
    Product package Id to start the product rotate secrets action for.
.EXAMPLE
    PS C:\> Invoke-AzsProductRotateSecretsAction -ProductId $ProductId
    Starts the product rotate secrets action for the specified product.
#>

function Invoke-AzsProductRotateSecretsAction {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $ProductId
    )

    $subscriptionId = (Get-AzContext).Subscription.Id
    $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/productDeployments/$ProductId/rotateSecrets?api-version=2019-01-01"

    $response = Invoke-AzsResourceManager -Method POST -Uri $requestUri -ThrowOnError -Verbose

    if (-not [string]::IsNullOrEmpty($response.AsyncOperationStatusUri)) {
        $waitAsyncOperation = Wait-AzsAsyncOperation -OperationName 'Invoke-AzsProductRotateSecretsAction' -AsyncOperationStatusUri $response.AsyncOperationStatusUri -Verbose
        ThrowOnError -WaitResult $waitAsyncOperation -ProblemDescription 'Unable to complete rotate secrets operation'
    }
}

#-----------------------------------------------------------------------

<#
.SYNOPSIS
    Lists product secrets or gets a product secret properties.
.DESCRIPTION
    Lists product secrets or gets a product secret properties.
.PARAMETER PackageId
    Product package Id to get the product secret properties for.
.PARAMETER SecretName
    Name of the secret to be retrieved.
.PARAMETER AsJson
    Outputs the result in Json format.
.EXAMPLE
    PS C:/> Get-AzsProductSecret -PackageId $PackageId -AsJson
    Lists all external secrets from package with Id $PackageId. Outputs in Json format.
     
.EXAMPLE
    PS C:/> Get-AzsProductSecret -PackageId $PackageId -SecretName AdHoc
    Gets the product secret called 'AdHoc'
#>

function Get-AzsProductSecret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $PackageId,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $SecretName = $null,

        [Parameter()]
        [switch] $AsJson
    )

    $subscriptionId = (Get-AzContext).Subscription.Id

    if ([string]::IsNullOrEmpty($SecretName)) {
        $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/productPackages/$($PackageId)/secrets?api-version=2019-01-01"
    }
    else {
        $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/productPackages/$($PackageId)/secrets/$($SecretName)?api-version=2019-01-01"
    }

    $response = Invoke-AzsResourceManager -Method GET -Uri $requestUri -Verbose -RetryOnError

    if ($response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) {
        return $null
    }

    EnsureSuccessStatusCode -Response $response

    if ($AsJson) {
        return $response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 99
    }

    return $response.Content | ConvertFrom-Json
}

<#
.SYNOPSIS
    Sets product secret value.
.DESCRIPTION
    Sets product secret value.
.PARAMETER PackageId
    Product package Id to set the product secret for.
.PARAMETER SecretName
    Name of the secret.
.PARAMETER Value
    Value of the secret.
.PARAMETER PfxFileName
    Location of the pfx file.
.PARAMETER PfxPassword
    PFX file password.
.PARAMETER Password
    Password Value.
.PARAMETER Key
    The symmetric key.
.PARAMETER Force
    Do not ask for confirmation.
     
.EXAMPLE
    PS C:/> Set-AzsProductSecret -PackageId $PackageId -SecretName AdHoc -Value $value
    Sets the product secret value to the given value.
     
.EXAMPLE
    PS C:/> Set-AzsProductSecret -PackageId $PackageId -SecretName TlsCertificate -PfxFileName .\temp\ExternalCertificate\cert.pfx -PfxPassword $pfxPassword -Force
    Sets the product secret value to the given value.
     
.EXAMPLE
    PS C:/> Set-AzsProductSecret -PackageId $PackageId -SecretName ExternalSymmetricKey -Key $key -Force
    Sets the product secret value to the given value.
#>

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

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

        [Parameter(Mandatory = $true, ParameterSetName = 'AdHoc')]
        [securestring] $Value,

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

        [Parameter(Mandatory = $true, ParameterSetName = 'Certificate')]
        [securestring] $PfxPassword,

        [Parameter(Mandatory = $true, ParameterSetName = 'Password')]
        [securestring] $Password,

        [Parameter(Mandatory = $true, ParameterSetName = 'SymmetricKey')]
        [securestring] $Key,

        [Parameter()]
        [switch] $Force
    )

    function ConvertFrom-SecureString {
        param(
            [Parameter(Mandatory = $true)]
            [securestring] $Value
        )

        return [System.Net.NetworkCredential]::new('', $Value).Password
    }

    if ($PSCmdlet.ParameterSetName -eq 'AdHoc') {
        $body = @{
            value = (ConvertFrom-SecureString -Value $Value)
        }
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'Certificate') {
        $body = @{
            data     = [System.Convert]::ToBase64String((Get-Content $PfxFileName -Encoding Byte))
            password = (ConvertFrom-SecureString -Value $PfxPassword)
        }
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'Password') {
        $body = @{
            password = (ConvertFrom-SecureString -Value $Password)
        }
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'SymmetricKey') {
        $body = @{
            key = (ConvertFrom-SecureString -Value $Key)
        }
    }

    if ($Force.ToBool()) {
        Write-Verbose 'Importing secret...' -Verbose
        $action = 'import'
    }
    else {
        Write-Verbose 'Validating secret...' -Verbose
        $action = 'validate'
    }

    $subscriptionId = (Get-AzContext).Subscription.Id
    $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/productPackages/$PackageId/secrets/$SecretName/$($action)?api-version=2019-01-01"

    Invoke-AzsResourceManager -Method POST -Uri $requestUri -Body $body -ThrowOnError -Verbose | Out-Null
}

#-----------------------------------------------------------------------

<#
.SYNOPSIS
    Gets or lists the action plans.
.DESCRIPTION
    Gets or lists the action plans.
.PARAMETER PlanId
    Action Plan Id to retrieve the properties for.
.PARAMETER AsJson
    Outputs the result in Json format.
.EXAMPLE
    PS C:/> Get-AzsActionPlan
    Lists all the action plan under the subscription.
.EXAMPLE
    PS C:/> Get-AzsActionPlan -PlanId $planId -AsJson
     
    Gets the action plan properties for plan with Id $planId.
#>

function Get-AzsActionPlan {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $PlanId = $null,

        [Parameter()]
        [switch] $AsJson
    )

    $subscriptionId = (Get-AzContext).Subscription.Id

    if ([string]::IsNullOrEmpty($PlanId)) {
        $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/actionplans?api-version=2019-01-01"
    }
    else {
        $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/actionplans/$($PlanId)?api-version=2019-01-01"
    }

    $response = Invoke-AzsResourceManager -Method GET -Uri $requestUri -ThrowOnError -Verbose -RetryOnError

    if ($AsJson) {
        return $response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 99
    }

    return $response.Content | ConvertFrom-Json
}

<#
.SYNOPSIS
    Gets or lists action plan operations.
.DESCRIPTION
    Gets or lists action plan operations.
.PARAMETER PlanId
    Action Plan Identifier.
.PARAMETER OperationId
    Operation Id to retrieve the properties for.
.PARAMETER AsJson
    Outputs the result in Json format.
.EXAMPLE
    PS C:/> Get-AzsActionPlanOperation -PlanId $planId -AsJson
    Gets the action plan operations for plan with id $planId.
#>

function Get-AzsActionPlanOperation {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $PlanId,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $OperationId = $null,

        [Parameter()]
        [switch] $AsJson
    )

    $subscriptionId = (Get-AzContext).Subscription.Id

    if ([string]::IsNullOrEmpty($OperationId)) {
        $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/actionplans/$PlanId/operations?api-version=2019-01-01"
    }
    else {
        $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/actionplans/$PlanId/operations/$($OperationId)?api-version=2019-01-01"
    }

    $response = Invoke-AzsResourceManager -Method GET -Uri $requestUri -ThrowOnError -Verbose -RetryOnError

    if ($AsJson) {
        return $response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 99
    }

    return $response.Content | ConvertFrom-Json
}

<#
.SYNOPSIS
    Gets or lists the action plan attempt
.DESCRIPTION
    Gets or lists the action plan attempts
.PARAMETER PlanId
    Plan Id of the action plan
.PARAMETER OperationId
    Operation Id of the action plan attempt
.PARAMETER AttemptNo
    Action plan attempt number
.PARAMETER AsJson
    Outputs the result in Json format.
.EXAMPLE
    PS C:/> Get-AzsActionPlanAttempt -PlanId $planId -OperationId $operationId -AsJson
    Gets or lists the action plan attempt properties for plan with id $planId and operation Id $operationId.
#>

function Get-AzsActionPlanAttempt {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $PlanId,

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

        [Parameter()]
        [int] $AttemptNo,

        [Parameter()]
        [switch] $AsJson
    )

    $subscriptionId = (Get-AzContext).Subscription.Id

    if ($AttemptNo -eq 0) {
        $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/actionplans/$PlanId/operations/$OperationId/attempts?api-version=2019-01-01"
    }
    else {
        $requestUri = "/subscriptions/$subscriptionId/providers/Microsoft.Deployment.Admin/locations/global/actionplans/$PlanId/operations/$OperationId/attempts/$($AttemptNo)?api-version=2019-01-01"
    }

    $response = Invoke-AzsResourceManager -Method GET -Uri $requestUri -ThrowOnError -Verbose -RetryOnError

    if ($AsJson) {
        return $response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 99
    }

    return $response.Content | ConvertFrom-Json
}

#-----------------------------------------------------------------------

$ErrorActionPreference = 'Stop'

[System.Reflection.Assembly]::LoadWithPartialName('System.Net.Http') | Out-Null
[System.Net.Http.HttpClient] $HttpClient = [System.Net.Http.HttpClient]::new()

$functions = @(
    'Get-AzsFileContainer'
    'New-AzsFileContainer'
    'Remove-AzsFileContainer'
    'Get-AzsProductPackage'
    'New-AzsProductPackage'
    'Remove-AzsProductPackage'
    'Get-AzsProductDeployment'
    'Invoke-AzsProductBootstrapAction'
    'Invoke-AzsProductDeployAction'
    'Invoke-AzsProductExecuteRunnerAction'
    'Invoke-AzsProductRemoveAction'
    'Invoke-AzsProductRotateSecretsAction'
    'Get-AzsProductSecret'
    'Set-AzsProductSecret'
    'Get-AzsActionPlan'
    'Get-AzsActionPlanOperation'
    'Get-AzsActionPlanAttempt'
)
Export-ModuleMember -Function $functions

# SIG # Begin signature block
# MIIjhQYJKoZIhvcNAQcCoIIjdjCCI3ICAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBLSWKVIBw/QURr
# kingqsrQNx1uRdK7A+pD2b9uq88nX6CCDYEwggX/MIID56ADAgECAhMzAAACUosz
# qviV8znbAAAAAAJSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjEwOTAyMTgzMjU5WhcNMjIwOTAxMTgzMjU5WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDQ5M+Ps/X7BNuv5B/0I6uoDwj0NJOo1KrVQqO7ggRXccklyTrWL4xMShjIou2I
# sbYnF67wXzVAq5Om4oe+LfzSDOzjcb6ms00gBo0OQaqwQ1BijyJ7NvDf80I1fW9O
# L76Kt0Wpc2zrGhzcHdb7upPrvxvSNNUvxK3sgw7YTt31410vpEp8yfBEl/hd8ZzA
# v47DCgJ5j1zm295s1RVZHNp6MoiQFVOECm4AwK2l28i+YER1JO4IplTH44uvzX9o
# RnJHaMvWzZEpozPy4jNO2DDqbcNs4zh7AWMhE1PWFVA+CHI/En5nASvCvLmuR/t8
# q4bc8XR8QIZJQSp+2U6m2ldNAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUNZJaEUGL2Guwt7ZOAu4efEYXedEw
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDY3NTk3MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAFkk3
# uSxkTEBh1NtAl7BivIEsAWdgX1qZ+EdZMYbQKasY6IhSLXRMxF1B3OKdR9K/kccp
# kvNcGl8D7YyYS4mhCUMBR+VLrg3f8PUj38A9V5aiY2/Jok7WZFOAmjPRNNGnyeg7
# l0lTiThFqE+2aOs6+heegqAdelGgNJKRHLWRuhGKuLIw5lkgx9Ky+QvZrn/Ddi8u
# TIgWKp+MGG8xY6PBvvjgt9jQShlnPrZ3UY8Bvwy6rynhXBaV0V0TTL0gEx7eh/K1
# o8Miaru6s/7FyqOLeUS4vTHh9TgBL5DtxCYurXbSBVtL1Fj44+Od/6cmC9mmvrti
# yG709Y3Rd3YdJj2f3GJq7Y7KdWq0QYhatKhBeg4fxjhg0yut2g6aM1mxjNPrE48z
# 6HWCNGu9gMK5ZudldRw4a45Z06Aoktof0CqOyTErvq0YjoE4Xpa0+87T/PVUXNqf
# 7Y+qSU7+9LtLQuMYR4w3cSPjuNusvLf9gBnch5RqM7kaDtYWDgLyB42EfsxeMqwK
# WwA+TVi0HrWRqfSx2olbE56hJcEkMjOSKz3sRuupFCX3UroyYf52L+2iVTrda8XW
# esPG62Mnn3T8AuLfzeJFuAbfOSERx7IFZO92UPoXE1uEjL5skl1yTZB3MubgOA4F
# 8KoRNhviFAEST+nG8c8uIsbZeb08SeYQMqjVEmkwggd6MIIFYqADAgECAgphDpDS
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVWjCCFVYCAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN
# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgeE1rmnIZ
# O2UsLyZ9DOw6rxNvPi08uhiQgJraABNPOsswQgYKKwYBBAGCNwIBDDE0MDKgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
# BgkqhkiG9w0BAQEFAASCAQB1TM6h6abilzwyIAzWkYHRDlu5YgrNFZjyvYY08meF
# bkW5T5etW6fOvEvssbZTUHZGA2n8uTaaHGteXJcwZIMB4922XAu+67FnwRP54L/v
# TFGlEUIeme4GX9L7rSwSZLdXc65MmWzscYKCT/NH95VhQJJHB/OL4jx9CU89aooi
# i3ZKO9xqAiVwyh8EOA1oSJoqXA61fiTTAjGKOkj7vvoS3dg7y4l2kIEESEsIwrR+
# R+v4RPVM8vo/puulpDB6ESUZ0s50fPLOLjcpIWp16eefoTn26CrlvpqP25c//hjI
# XygWZcEcWHc/CXfDoiRH1SRlXA8uM6UexFBhSWGTJ2IyoYIS5DCCEuAGCisGAQQB
# gjcDAwExghLQMIISzAYJKoZIhvcNAQcCoIISvTCCErkCAQMxDzANBglghkgBZQME
# AgEFADCCAVEGCyqGSIb3DQEJEAEEoIIBQASCATwwggE4AgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEIER89xudJI7uAYpnKNysdX7CrcW8WRVU+ZN4JY88
# FKhzAgZhkGmImRcYEzIwMjExMjExMDMzMDM0Ljk4NlowBIACAfSggdCkgc0wgcox
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1p
# Y3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1Mg
# RVNOOkFFMkMtRTMyQi0xQUZDMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFt
# cCBTZXJ2aWNloIIOOzCCBPEwggPZoAMCAQICEzMAAAFIoohFVrwvgL8AAAAAAUgw
# DQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0
# b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh
# dGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcN
# MjAxMTEyMTgyNTU2WhcNMjIwMjExMTgyNTU2WjCByjELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2Eg
# T3BlcmF0aW9uczEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046QUUyQy1FMzJCLTFB
# RkMxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggEiMA0G
# CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD3/3ivFYSK0dGtcXaZ8pNLEARbraJe
# wryi/JgbaKlq7hhFIU1EkY0HMiFRm2/Wsukt62k25zvDxW16fphg5876+l1wYnCl
# ge/rFlrR2Uu1WwtFmc1xGpy4+uxobCEMeIFDGhL5DNTbbOisLrBUYbyXr7fPzxbV
# kEwJDP5FG2n0ro1qOjegIkLIjXU6qahduQxTfsPOEp8jgqMKn++fpH6fvXKlewWz
# dsfvhiZ4H4Iq1CTOn+fkxqcDwTHYkYZYgqm+1X1x7458rp69qjFeVP3GbAvJbY3b
# Flq5uyxriPcZxDZrB6f1wALXrO2/IdfVEdwTWqJIDZBJjTycQhhxS3i1AgMBAAGj
# ggEbMIIBFzAdBgNVHQ4EFgQUhzLwaZ8OBLRJH0s9E63pIcWJokcwHwYDVR0jBBgw
# FoAU1WM6XIoxkPNDe3xGG8UzaFqFbVUwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDov
# L2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljVGltU3RhUENB
# XzIwMTAtMDctMDEuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0
# cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNUaW1TdGFQQ0FfMjAx
# MC0wNy0wMS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDAN
# BgkqhkiG9w0BAQsFAAOCAQEAZhKWwbMnC9Qywcrlgs0qX9bhxiZGve+8JED27hOi
# yGa8R9nqzHg4+q6NKfYXfS62uMUJp2u+J7tINUTf/1ugL+K4RwsPVehDasSJJj+7
# boIxZP8AU/xQdVY7qgmQGmd4F+c5hkJJtl6NReYE908Q698qj1mDpr0Mx+4LhP/t
# TqL6HpZEURlhFOddnyLStVCFdfNI1yGHP9n0yN1KfhGEV3s7MBzpFJXwOflwgyE9
# cwQ8jjOTVpNRdCqL/P5ViCAo2dciHjd1u1i1Q4QZ6xb0+B1HdZFRELOiFwf0sh3Z
# 1xOeSFcHg0rLE+rseHz4QhvoEj7h9bD8VN7/HnCDwWpBJTCCBnEwggRZoAMCAQIC
# CmEJgSoAAAAAAAIwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRp
# ZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTEwMDcwMTIxMzY1NVoXDTI1MDcwMTIx
# NDY1NVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV
# BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG
# A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggEiMA0GCSqGSIb3
# DQEBAQUAA4IBDwAwggEKAoIBAQCpHQ28dxGKOiDs/BOX9fp/aZRrdFQQ1aUKAIKF
# ++18aEssX8XD5WHCdrc+Zitb8BVTJwQxH0EbGpUdzgkTjnxhMFmxMEQP8WCIhFRD
# DNdNuDgIs0Ldk6zWczBXJoKjRQ3Q6vVHgc2/JGAyWGBG8lhHhjKEHnRhZ5FfgVSx
# z5NMksHEpl3RYRNuKMYa+YaAu99h/EbBJx0kZxJyGiGKr0tkiVBisV39dx898Fd1
# rL2KQk1AUdEPnAY+Z3/1ZsADlkR+79BL/W7lmsqxqPJ6Kgox8NpOBpG2iAg16Hgc
# sOmZzTznL0S6p/TcZL2kAcEgCZN4zfy8wMlEXV4WnAEFTyJNAgMBAAGjggHmMIIB
# 4jAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQU1WM6XIoxkPNDe3xGG8UzaFqF
# bVUwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud
# EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQwVgYD
# VR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwv
# cHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEB
# BE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9j
# ZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwgaAGA1UdIAEB/wSBlTCB
# kjCBjwYJKwYBBAGCNy4DMIGBMD0GCCsGAQUFBwIBFjFodHRwOi8vd3d3Lm1pY3Jv
# c29mdC5jb20vUEtJL2RvY3MvQ1BTL2RlZmF1bHQuaHRtMEAGCCsGAQUFBwICMDQe
# MiAdAEwAZQBnAGEAbABfAFAAbwBsAGkAYwB5AF8AUwB0AGEAdABlAG0AZQBuAHQA
# LiAdMA0GCSqGSIb3DQEBCwUAA4ICAQAH5ohRDeLG4Jg/gXEDPZ2joSFvs+umzPUx
# vs8F4qn++ldtGTCzwsVmyWrf9efweL3HqJ4l4/m87WtUVwgrUYJEEvu5U4zM9GAS
# inbMQEBBm9xcF/9c+V4XNZgkVkt070IQyK+/f8Z/8jd9Wj8c8pl5SpFSAK84Dxf1
# L3mBZdmptWvkx872ynoAb0swRCQiPM/tA6WWj1kpvLb9BOFwnzJKJ/1Vry/+tuWO
# M7tiX5rbV0Dp8c6ZZpCM/2pif93FSguRJuI57BlKcWOdeyFtw5yjojz6f32WapB4
# pm3S4Zz5Hfw42JT0xqUKloakvZ4argRCg7i1gJsiOCC1JeVk7Pf0v35jWSUPei45
# V3aicaoGig+JFrphpxHLmtgOR5qAxdDNp9DvfYPw4TtxCd9ddJgiCGHasFAeb73x
# 4QDf5zEHpJM692VHeOj4qEir995yfmFrb3epgcunCaw5u+zGy9iCtHLNHfS4hQEe
# gPsbiSpUObJb2sgNVZl6h3M7COaYLeqN4DMuEin1wC9UJyH3yKxO2ii4sanblrKn
# QqLJzxlBTeCG+SqaoxFmMNO7dDJL32N79ZmKLxvHIa9Zta7cRDyXUHHXodLFVeNp
# 3lfB0d4wwP3M5k37Db9dT+mdHhk4L7zPWAUu7w2gUDXa7wknHNWzfjUeCLraNtvT
# X4/edIhJEqGCAs0wggI2AgEBMIH4oYHQpIHNMIHKMQswCQYDVQQGEwJVUzETMBEG
# A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj
# cm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBP
# cGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpBRTJDLUUzMkItMUFG
# QzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcG
# BSsOAwIaAxUAhyuClrocWf4SIcRafAEX1Rhs6zmggYMwgYCkfjB8MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQg
# VGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAOVef8MwIhgPMjAy
# MTEyMTEwOTM5MTVaGA8yMDIxMTIxMjA5MzkxNVowdjA8BgorBgEEAYRZCgQBMS4w
# LDAKAgUA5V5/wwIBADAJAgEAAgFeAgH/MAcCAQACAhHXMAoCBQDlX9FDAgEAMDYG
# CisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEA
# AgMBhqAwDQYJKoZIhvcNAQEFBQADgYEAGXo7QWqSS+V78isIqspldM7evX+snlAX
# KLB+ZJmdsZVQpaApJjYNIQnsAtJM+eqGPuiW5ing5fD/4lkf98lJvZCj41XtEpM/
# sP0IPLMKVmIfpxLZ9DMX3LJ6pC+fOFvkPLXAlFMfSkG5GW5Qi8/4XL09bLQUFXLs
# Kzq6yOJSAMExggMNMIIDCQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0Eg
# MjAxMAITMwAAAUiiiEVWvC+AvwAAAAABSDANBglghkgBZQMEAgEFAKCCAUowGgYJ
# KoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCAYSgm7ZfwN
# k59kKdvLIKAlvJRJL4ZP+Q1iTHjXMiHCajCB+gYLKoZIhvcNAQkQAi8xgeowgecw
# geQwgb0EIKmQGuqMeaG/Jh/m1NxO8Pljhr5Xv1PBVXpPVoDB22jYMIGYMIGApH4w
# fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd
# TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAFIoohFVrwvgL8AAAAA
# AUgwIgQgpNp+Llt7q3VC2jcb1lNFhjn/Usfo0Zg+BbARu1tkuPowDQYJKoZIhvcN
# AQELBQAEggEAs4mgF6uQ0A0VKJotU7O4ds084mckPHmZvt6oqd3vMw1bGFY+GYZL
# dGJi/+N2VZ5iEW/ZPMEIQXAU7oB7N7IbkktXR297IoZq8tsQvyhBx6MwGpFIrf3+
# 80hefz/JNThHDYwYAwn4ptRgQ+WK6WRd6T5YQ0ijFZflCNc6BCuRZ0PZZY/LV/Cs
# F2gH9NvfrdYEZqNvRu5SNWgt4FHJ6XKh6rFouyuj6/BztwNjwEnql3TnCdcTzHE8
# 3sPS288JvY8zDUWPlXsQAwFUDdoRFqBk3RxrJC2ijIpuuziwWplKRFECpSbWCMuy
# pJuWHLExuh5E5Z3yPsi0IEUZSvvZcPfQuA==
# SIG # End signature block