Azs.Deployment.Admin.psm1

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

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

<#
.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
    )

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

    if ($null -eq $ctx.Environment) {
        throw 'AzureRm Context 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

    try {
        $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 = [psobject]::new()
        $result | Add-Member -MemberType NoteProperty -Name 'StatusCode' -Value $response.StatusCode
        $result | Add-Member -MemberType NoteProperty -Name 'AsyncOperationStatusUri' -Value (Get-HeaderValue -Headers $response.Headers -Name 'Azure-AsyncOperation')
        $result | Add-Member -MemberType NoteProperty -Name 'LocationUri' -Value (Get-HeaderValue -Headers $response.Headers -Name 'Location')
        $result | Add-Member -MemberType NoteProperty -Name 'Content' -Value $responseContent

        if ($ThrowOnError) {
            EnsureSuccessStatusCode -Response $result
        }

        return $result
    }
    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

        EnsureSuccessStatusCode -Response $response

        $operationResult = $response.Content | ConvertFrom-Json

        if (IsOperationResultTerminalState $operationResult.status) {
            if ($operationResult.status -eq 'Succeeded') {
                return $true
            }

            return $false
        }

        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
    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-AzureRmContext).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

    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-AzureRmContext).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)) {
        if (-not (Wait-AzsAsyncOperation -OperationName 'New-AzsFileContainer' -AsyncOperationStatusUri $response.AsyncOperationStatusUri -Verbose)) {
            throw '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-AzureRmContext).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-AzureRmContext).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

    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-AzureRmContext).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)) {
        if (-not (Wait-AzsAsyncOperation -OperationName 'New-AzsProductPackage' -AsyncOperationStatusUri $response.AsyncOperationStatusUri -Verbose)) {
            throw '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-AzureRmContext).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-AzureRmContext).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

    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-AzureRmContext).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

    if (-not (Wait-AzsAsyncOperation -OperationName 'Invoke-AzsProductBootstrapAction' -AsyncOperationStatusUri $response.AsyncOperationStatusUri -Verbose)) {
        throw "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-AzureRmContext).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

    if (-not (Wait-AzsAsyncOperation -OperationName 'Invoke-AzsProductDeployAction' -AsyncOperationStatusUri $response.AsyncOperationStatusUri -Verbose)) {
        throw "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-AzureRmContext).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)) {
        if (-not (Wait-AzsAsyncOperation -OperationName 'Invoke-AzsProductExecuteRunnerAction' -AsyncOperationStatusUri $response.AsyncOperationStatusUri -Verbose)) {
            throw "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-AzureRmContext).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)) {
        if (-not (Wait-AzsAsyncOperation -OperationName 'Invoke-AzsProductRemoveAction' -AsyncOperationStatusUri $response.AsyncOperationStatusUri -Verbose)) {
            throw "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-AzureRmContext).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)) {
        if (-not (Wait-AzsAsyncOperation -OperationName 'Invoke-AzsProductRotateSecretsAction' -AsyncOperationStatusUri $response.AsyncOperationStatusUri -Verbose)) {
            throw "Unable to complete rotate secrets operation."
        }
    }
}

<#
.SYNOPSIS
    Unlock the product subscription.
 
.DESCRIPTION
    Unlock the product subscription.
 
.PARAMETER ProductId
    Product package Id to unlock the product subscription for.
 
.PARAMETER Duration
    The time duration for the product subscription to be unlocked.
 
.EXAMPLE
 
    PS C:\> Unlock-AzsProductSubscription -ProductId $ProductId -Duration ([timespan]::FromDays(5))
 
    Unlocks the product subscription for the specified product and the specified duration
 
#>

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

        [Parameter()]
        [timespan] $Duration = [timespan]::Zero
    )

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

    if ($Duration -eq [timespan]::Zero) {
        $Duration = [timespan]::FromDays(7)
    }

    $body = @{
        duration = [System.Xml.XmlConvert]::ToString($Duration)
    }

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

<#
.SYNOPSIS
    Locks the product subscription.
 
.DESCRIPTION
    Locks the product subscription.
 
.PARAMETER ProductId
    Product package Id to lock the product subscription for.
 
.EXAMPLE
 
    PS C:/> Lock-AzsProductSubscription -ProductId $ProductId
 
    Locks the product subscription for the product with ID $ProductId
 
#>

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

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

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

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

<#
.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-AzureRmContext).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

    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
    Locks the product subscription.
 
.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-AzureRmContext).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-AzureRmContext).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

    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-AzureRmContext).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

    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-AzureRmContext).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

    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'
    'Unlock-AzsProductSubscription'
    'Lock-AzsProductSubscription'
    'Get-AzsProductSecret'
    'Set-AzsProductSecret'
    'Get-AzsActionPlan'
    'Get-AzsActionPlanOperation'
    'Get-AzsActionPlanAttempt'
)
Export-ModuleMember -Function $functions

# SIG # Begin signature block
# MIIjhgYJKoZIhvcNAQcCoIIjdzCCI3MCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDMsrgiyLUU71Wt
# gl7kI7BStaUIw9sNSnLHMui60giDOqCCDYEwggX/MIID56ADAgECAhMzAAABUZ6N
# j0Bxow5BAAAAAAFRMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMTkwNTAyMjEzNzQ2WhcNMjAwNTAyMjEzNzQ2WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQCVWsaGaUcdNB7xVcNmdfZiVBhYFGcn8KMqxgNIvOZWNH9JYQLuhHhmJ5RWISy1
# oey3zTuxqLbkHAdmbeU8NFMo49Pv71MgIS9IG/EtqwOH7upan+lIq6NOcw5fO6Os
# +12R0Q28MzGn+3y7F2mKDnopVu0sEufy453gxz16M8bAw4+QXuv7+fR9WzRJ2CpU
# 62wQKYiFQMfew6Vh5fuPoXloN3k6+Qlz7zgcT4YRmxzx7jMVpP/uvK6sZcBxQ3Wg
# B/WkyXHgxaY19IAzLq2QiPiX2YryiR5EsYBq35BP7U15DlZtpSs2wIYTkkDBxhPJ
# IDJgowZu5GyhHdqrst3OjkSRAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUV4Iarkq57esagu6FUBb270Zijc8w
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDU0MTM1MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAWg+A
# rS4Anq7KrogslIQnoMHSXUPr/RqOIhJX+32ObuY3MFvdlRElbSsSJxrRy/OCCZdS
# se+f2AqQ+F/2aYwBDmUQbeMB8n0pYLZnOPifqe78RBH2fVZsvXxyfizbHubWWoUf
# NW/FJlZlLXwJmF3BoL8E2p09K3hagwz/otcKtQ1+Q4+DaOYXWleqJrJUsnHs9UiL
# crVF0leL/Q1V5bshob2OTlZq0qzSdrMDLWdhyrUOxnZ+ojZ7UdTY4VnCuogbZ9Zs
# 9syJbg7ZUS9SVgYkowRsWv5jV4lbqTD+tG4FzhOwcRQwdb6A8zp2Nnd+s7VdCuYF
# sGgI41ucD8oxVfcAMjF9YX5N2s4mltkqnUe3/htVrnxKKDAwSYliaux2L7gKw+bD
# 1kEZ/5ozLRnJ3jjDkomTrPctokY/KaZ1qub0NUnmOKH+3xUK/plWJK8BOQYuU7gK
# YH7Yy9WSKNlP7pKj6i417+3Na/frInjnBkKRCJ/eYTvBH+s5guezpfQWtU4bNo/j
# 8Qw2vpTQ9w7flhH78Rmwd319+YTmhv7TcxDbWlyteaj4RK2wk3pY1oSz2JPE5PNu
# Nmd9Gmf6oePZgy7Ii9JLLq8SnULV7b+IP0UXRY9q+GdRjM2AEX6msZvvPCIoG0aY
# HQu9wZsKEK2jqvWi8/xdeeeSI9FN6K1w4oVQM4Mwggd6MIIFYqADAgECAgphDpDS
# 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/BvW1taslScxMNelDNMYIVWzCCFVcCAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAVGejY9AcaMOQQAAAAABUTAN
# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgCykVMcLw
# b/bF1qNbw4T3HutwWdN3LfJKW70WCIPf5CcwQgYKKwYBBAGCNwIBDDE0MDKgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
# BgkqhkiG9w0BAQEFAASCAQBOTPZEBZIS46f22+nsRlVS7gCrR3MlfxpYO9+/7vWN
# 8RB9L+Blre+fRejgFZT3p6edr5sRMdt8/8L1fWI9083faQ/Zis4ZYIJxjgg/lXnC
# 5uXmO9OsOYBKN7c/C0yvBXM4zRHQ0Pf2yG8O9qDeJBO2Kq1PSw5I13QTIwrCg1rz
# MsSkmpdjXKwPiEqzRShjRxH6qtbxPUHkrrrLCDynhJSZXICkkAs30iCJlyXq4z+D
# qysMheDMJHJ01QMIyLSHKHfhLWj1BsJiM3FJTqOzdN6m/X3nd0/4ikW0U2xYODud
# Kkz+WY+pDOErWTbIb14gF40orgNhMaIthSzAZxKHvRnQoYIS5TCCEuEGCisGAQQB
# gjcDAwExghLRMIISzQYJKoZIhvcNAQcCoIISvjCCEroCAQMxDzANBglghkgBZQME
# AgEFADCCAVEGCyqGSIb3DQEJEAEEoIIBQASCATwwggE4AgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEIMxPvrjiTCicNJAQzaMnBu9WdPSzRADDqfW/Uw75
# 5wW3AgZduE8lsOkYEzIwMTkxMTE5MTkxNzI0LjQ0NVowBIACAfSggdCkgc0wgcox
# CzAJBgNVBAYTAlVTMQswCQYDVQQIEwJXQTEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQg
# SXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1Mg
# RVNOOkE4NDEtNEJCNC1DQTkzMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFt
# cCBTZXJ2aWNloIIOPDCCBPEwggPZoAMCAQICEzMAAAEOJpHynZLbgSkAAAAAAQ4w
# DQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0
# b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh
# dGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcN
# MTkxMDIzMjMxOTE4WhcNMjEwMTIxMjMxOTE4WjCByjELMAkGA1UEBhMCVVMxCzAJ
# BgNVBAgTAldBMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlv
# bnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046QTg0MS00QkI0LUNB
# OTMxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggEiMA0G
# CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVAZeiJz2rNUm5FnQV7KLoay7FXA8r
# WMadGWHVkKp5H8oV6/716Ra9o4cQxvVKouIbywDZM5OoMx1cK3UjGCJwJJ645s8N
# dUX3jQPwUzk6jDQYrQUYhHzsdJ2ZOjWXJ2AqR7YKzfXqXAcXniLSe1lfvLFPctK2
# 5h7RYHTNldEglHnEYyyUSC2KELbHyJ/x4RUlGL0Z41GCBzLxmmnQXRD8VQz9mx39
# O51Mz6QBVpIBlBhcHldUqWgslL1z25uqfYXKLpR3S2pclEj/EwrWhG/OSCZBhpg0
# dbq++nzYbdhXUctNZwMI7UrKxRtcA55DNMB/ETColAHWaBei/yuEO4TjAgMBAAGj
# ggEbMIIBFzAdBgNVHQ4EFgQUQeSkq9TmIF5Sa0eX1Ip3K/9T39kwHwYDVR0jBBgw
# FoAU1WM6XIoxkPNDe3xGG8UzaFqFbVUwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDov
# L2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljVGltU3RhUENB
# XzIwMTAtMDctMDEuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0
# cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNUaW1TdGFQQ0FfMjAx
# MC0wNy0wMS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDAN
# BgkqhkiG9w0BAQsFAAOCAQEABkb5SfYYqCqg0oIbDwzIe0nM84CqpbfjqLodi7Bd
# VtfMCCuAefUzJ9PmS2xZHq8MuNOl5y+pHDJN7ZibsyEThoeD0HopVG3PXVn9QXmj
# lbDYRxMr3e7KGeqRtJdTrDMcnx/fNy7mHj27MmmuhcHBTOyPjU+D+RnTybqgQrMi
# T0pY1LVM+PxxjCaOxSLc6eCZGNvAcRqGQJaqpGa8uCEIpGhdpbpIv1UWtIVrjOsz
# AmLVoINL/YjYYE7a/ZtmFseNgyZzvWwzNYSd4XvXtMrYc/VOyUBeigfVKW1X734L
# 51a6VViSIpuhD7x8LBlLjB687bvoH5QhPw4Rnb2Z90EESTCCBnEwggRZoAMCAQIC
# 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/edIhJEqGCAs4wggI3AgEBMIH4oYHQpIHNMIHKMQswCQYDVQQGEwJVUzELMAkG
# A1UECBMCV0ExEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9u
# cyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpBODQxLTRCQjQtQ0E5
# MzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcG
# BSsOAwIaAxUAkmNDHJXWmEV6IeyFeEIwunwyChWggYMwgYCkfjB8MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQg
# VGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAOF+fNcwIhgPMjAx
# OTExMTkyMjM4MTVaGA8yMDE5MTEyMDIyMzgxNVowdzA9BgorBgEEAYRZCgQBMS8w
# LTAKAgUA4X581wIBADAKAgEAAgIaoAIB/zAHAgEAAgISCTAKAgUA4X/OVwIBADA2
# BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIB
# AAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBALhC50IFBXDSIYBOpx8fheKdGctqs4Qv
# dXXoMrnEJk9zGaZGsQIShOmKUD/WI+fWLdIduBIJOt/uboIRFkRUE/RVnwDHKoIY
# s1rtjDul3it1b6LT/UEu8k3MchYzEkK0Wqf7KBHHOfcM7d4he33MCs5UXu7Nw3Nn
# kttfJCq3GkdXMYIDDTCCAwkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB
# IDIwMTACEzMAAAEOJpHynZLbgSkAAAAAAQ4wDQYJYIZIAWUDBAIBBQCgggFKMBoG
# CSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgfYo1DXVw
# h9SzFyIg3jfQwetfVNYegpHCDXQjYf1uHvUwgfoGCyqGSIb3DQEJEAIvMYHqMIHn
# MIHkMIG9BCBov/ePn2+tW9RhI4KYLWIbl3PTY0wpFNVTqYIUWkveMDCBmDCBgKR+
# MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT
# HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABDiaR8p2S24EpAAAA
# AAEOMCIEIDP+VPFB5KQDvjWDTZLwQnxDPfkR2aAHxUdWP97+oYgdMA0GCSqGSIb3
# DQEBCwUABIIBAG9/Vypm5zXtpJEe2nO67l1iRfevY0Am0t7rPXWj+VbA+x3uVLe+
# lyeqBtEK/QTE9RC9BweUOhd748yQGKg+w+sQT2gqJREoJ1otRqccA6HCsfS/AfFX
# NF1BNLNrXYsjzH0Tj/mWaN8jtif9QrZbJAZSjfF85QKge/+hmFFOcbHw7mhLWVXw
# VcqUI0JqRfHLi3Wupy4mSIlRI4J43I4AMabBtkC6GgDG8vsBjb08GiNaDnmig1kO
# HV1CKiU6rYEeU3t/kwnZS2op/ywqz3SPTtjYgjDYjx/0QdbDhL965X4/pNpN9rd3
# b9SBHjGFlzxPOvz4NOTb4d9lLzpEzRn2Z6Y=
# SIG # End signature block