PureStorage.CBS.AVS.psm1

 . $PSScriptRoot/PureStorage.CommonUtil.ps1
 . $PSScriptRoot/PureStorage.RunCommandLauncher.ps1
 . $PSScriptRoot/PureStorage.CBS.AVS.VMFS.ps1
 . $PSScriptRoot/PureStorage.CBS.AVS.Configuration.ps1

function Build-PCBSCluster {
    <#
    .SYNOPSIS
     Build or update settings for a cluster of ESXi servers
    .DESCRIPTION
     Build or update settings for a cluster of ESXi servers. Creates a hostgroup in Pure Cloud Block Store if it does not exists and
     updates iSCSI settings. Can be used when creating a new cluster or when adding hosts to a cluster.
    .PARAMETER ClusterName
     Cluster name
    .PARAMETER PureCloudBlockStoreConnection
     Pure Cloud Block Store Connection. The connection can be created using Connect-Pf2Array cmdlet
    .PARAMETER PureCloudBlockStoreCredential
     Pure Cloud Block Store Credentials
    .PARAMETER AVSCloudName
     AVS cloud name
    .PARAMETER AzureSubscriptionId
     Azure subscription Id. If not provided, default subscription will be used
    .PARAMETER AVSResourceGroup
     AVS ResourceGroup

    .EXAMPLE
    Build-PCBSCluster -ClusterName "mycluster" -PureCloudBlockStoreConnection $CBSConnection `
      -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [String]$ClusterName,

        [Parameter(Mandatory=$false)]
        $PureCloudBlockStoreConnection,

        [Parameter(Mandatory=$false)]
        [String]$AVSCloudName,

        [Parameter(Mandatory=$false)]
        [String]$AzureSubscriptionId,

        [Parameter(Mandatory=$false)]
        [String]$AVSResourceGroup
    )

    Write-Progress -Activity "Building cluster" -Status "0% Complete:" -PercentComplete 1
    Connect-AVSvCenter -AVSResourceGroupName $AVSResourceGroup -AVSPrivateCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId
    $cluster = Get-Cluster -Name $ClusterName
    if (-not $cluster) {
        throw "Could not find cluster '$ClusterName'..."
    }

    $fa = Connect-PureCloudBlockStore -PureCloudBlockStoreConnection $PureCloudBlockStoreConnection

    Write-Progress -Activity "Configuring iSCSI" -Status "25% Complete:" -PercentComplete 25
    $updated_hosts = New-PCBSHostGroupfromVcCluster -FlashArray $fa -Cluster $cluster `
       -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup
    Write-Progress -Activity "Removing unused hosts" -Status "50% Complete:" -PercentComplete 50
    Remove-PCBSUnusedHosts -FlashArray $fa -Cluster $cluster | Out-Null

    Write-Progress -Activity "Refreshing iSCSI targets" -Status "75% Complete:" -PercentComplete 75
    if ($updated_hosts) {
        $ethList = (Get-Pfa2NetworkInterface -Array $FlashArray | Where-Object {$_.services -eq "iscsi"} | Where-Object {$_.enabled -eq $true} | Where-Object {$null -ne $_.Eth.address}).Eth
        $ISCSIAddressList = @()
        foreach ($eth in $ethList) {
          $ISCSIAddressList += $eth.address
        }

        $params = @{
            ClusterName = $ClusterName
            # Need to join the list as string as RunCommand does not support array type
            ISCSIAddress = $ISCSIAddressList -join ","
        }

        Invoke-RunScript -CmdletName  "Remove-VMHostStaticiSCSITargets" -Parameters $params `
              -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup
    }
    Write-Host "Cluster '$ClusterName' is successfully built"
    Write-Progress -Activity "Operation is done" -Status "100% Complete:" -PercentComplete 100
}

function New-PCBSVmfsDatastore {
    <#
    .SYNOPSIS
      Creates a new VMFS datastore and mounts to a VMware cluster
    .DESCRIPTION
      Creates a new VMFS datastore and mounts to a VMware cluster
    .PARAMETER ClusterName
      Cluster name
    .PARAMETER DatastoreName
      Datastore name
    .PARAMETER Size
      Datastore capacity size in bytes
    .PARAMETER PureCloudBlockStoreConnection
     Pure Cloud Block Store Connection. The connection can be created using Connect-Pf2Array cmdlet
    .PARAMETER AVSCloudName
     AVS cloud name
    .PARAMETER AzureSubscriptionId
     Azure subscription Id. If not provided, default subscription will be used
    .PARAMETER AVSResourceGroup
     AVS ResourceGroup

    .EXAMPLE
      New-PCBSVmfsDatastore -ClusterName myClusterName -PureCloudBlockStoreConnection $CBSConnection -DatastoreName MyVMFSStore -Size 4GB `
        -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup

      Create a datastore "MyVMFS"
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [String]$ClusterName,

        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [String]$DatastoreName,

        [Parameter(Mandatory=$true)]
        [UInt64]$Size,

        [Parameter(Mandatory=$false)]
        $PureCloudBlockStoreConnection,

        [Parameter(Mandatory=$false)]
        [String]$AVSCloudName,

        [Parameter(Mandatory=$false)]
        [String]$AzureSubscriptionId,

        [Parameter(Mandatory=$false)]
        [String]$AVSResourceGroup
    )
    Write-Progress -Activity "Creating datastore" -Status "0% Complete:" -PercentComplete 1
    Connect-AVSvCenter -AVSResourceGroupName $AVSResourceGroup -AVSPrivateCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId

    $Cluster = Get-Cluster -Name $ClusterName -ErrorAction Ignore
    if (-not $Cluster) {
        throw "Cluster $ClusterName does not exist."
    }

    $fa = Connect-PureCloudBlockStore -PureCloudBlockStoreConnection $PureCloudBlockStoreConnection
    New-PfaVmfs -Cluster $Cluster -Flasharray $fa -Name $DatastoreName -Size $Size `
        -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup

    Write-Host "Datastore '$DatastoreName' is successfully created"
    Write-Progress -Activity "Operation is done" -Status "100% Complete:" -Completed

}

function Restore-PCBSVmfsDatastore {
    <#
    .SYNOPSIS
      Mounts a copy of a VMFS datastore to a VMware cluster from a Pure Cloud Block Store snapshot, volume, or pod.
    .DESCRIPTION
      Takes in a snapshot/volume/pod name, the corresponding Pure Cloud Block Store, and a cluster. The VMFS copy will be resignatured and mounted.
    .PARAMETER ClusterName
      Cluster name
    .PARAMETER VolumeSnapshotName
      Volume snapshot name. A volume will be created from the volume snapshot. A datastore will be created from the volume
    .PARAMETER VolumeName
      Volume name. A datastore will be created from the volume. No volume copy will be created, the volume specified will be directly used for the datastore
    .PARAMETER ProtectionGroupSnapshotName
      Protection group snapshot name. All of volume snapshots of the protection group snapshot will be used for restoring. The snapshot will be skipped if not supported.
    .PARAMETER PodName
      Pod name. All of volumes of the pod will be used for restoring. The volume will be skipped if not supported
    .PARAMETER PureCloudBlockStoreConnection
      Pure Cloud Block Store Connection. The connection can be created using Connect-Pf2Array cmdlet
    .PARAMETER DatastoreName
      Datastore name. Optional parameter. If the parameter is not specified, a generated name will be used.
    .PARAMETER AVSCloudName
     AVS cloud name
    .PARAMETER AzureSubscriptionId
     Azure subscription Id. If not provided, default subscription will be used
    .PARAMETER AVSResourceGroup
     AVS ResourceGroup
    .EXAMPLE
      Restore-PCBSVmfsDatastore -ClusterName myClusterName -VolumeSnapshotName mySnapshotName -PureCloudBlockStoreConnection $CBSConnection `
        -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup

      Takes in a snapshot name, the corresponding Pure Cloud Block Store, and a cluster. The VMFS copy will be resignatured and mounted.
    .EXAMPLE
      Restore-PCBSVmfsDatastore -ClusterName myClusterName -VolumeName myVolumeName -PureCloudBlockStoreConnection $CBSConnection `
        -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup

      Takes in a volume name, the corresponding Pure Cloud Block Store, and a cluster. The VMFS copy will be resignatured and mounted.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [String]$ClusterName,

        [Parameter(Mandatory = $true, ParameterSetName = 'VolumeSnapshot')]
        [String]$VolumeSnapshotName,

        [Parameter(Mandatory = $true, ParameterSetName = 'Volume')]
        [String]$VolumeName,

        [Parameter(Mandatory = $true, ParameterSetName = 'ProtectionGroupSnapshot')]
        [String]$ProtectionGroupSnapshotName,

        [Parameter(Mandatory = $true, ParameterSetName = 'Pod')]
        [String]$PodName,

        [Parameter(Mandatory = $false, ParameterSetName = 'Volume')]
        [Parameter(Mandatory = $false, ParameterSetName = 'VolumeSnapshot')]
        [String]$DatastoreName,

        [Parameter(Mandatory=$false)]
        $PureCloudBlockStoreConnection,

        [Parameter(Mandatory=$false)]
        [String]$AVSCloudName,

        [Parameter(Mandatory=$false)]
        [String]$AzureSubscriptionId,

        [Parameter(Mandatory=$false)]
        [String]$AVSResourceGroup
    )
    Write-Progress -Activity "Restoring datastore" -Status "0% Complete:" -PercentComplete 1
    Connect-AVSvCenter -AVSResourceGroupName $AVSResourceGroup -AVSPrivateCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId
    $fa = Connect-PureCloudBlockStore -PureCloudBlockStoreConnection $PureCloudBlockStoreConnection

    $Cluster = Get-Cluster -Name $ClusterName -ErrorAction Ignore
    if (-not $Cluster) {
        throw "Cluster '$ClusterName' does not exist."
    }

    if (-not [string]::IsNullOrEmpty($DatastoreName)) {
        $Datastore = Get-Datastore -Name $DatastoreName -ErrorAction Ignore
        if ($Datastore) {
            throw "Datastore '$Datastore' already exists."
        }
    }

    switch ($PSCmdlet.ParameterSetName) {
        'Volume' {
            Write-Host "Creating datastore from volume..."
            $NewDatastore = Restore-PfaVmfsFromVolume -FlashArray $fa -ClusterName $ClusterName -VolumeName $VolumeName -DatastoreName $DatastoreName `
                                -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup

            break
        }
        'VolumeSnapshot' {
            Write-Host "Creating datastore from volume snapshot..."
            $NewDatastore = Restore-PfaVmfsFromVolumeSnapshot -FlashArray $fa -ClusterName $ClusterName -VolumeSnapshotName $VolumeSnapshotName -DatastoreName $DatastoreName `
                                -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup

            break
        }
        'ProtectionGroupSnapshot' {
            Write-Host "Creating datastore from protection group snapshot..."
            $NewDatastore = Restore-PfaVmfsFromProtectionGroupSnapshot -FlashArray $fa -ClusterName $ClusterName -ProtectionGroupSnapshotName $ProtectionGroupSnapshotName `
                                -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup
            break
        }
        'Pod' {
            Write-Host "Creating datastore from pod..."
            $NewDatastore = Restore-PfaVmfsFromPod -FlashArray $fa -ClusterName $ClusterName -PodName $PodName `
                                -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup
            break
        }
    }
    Write-Progress -Activity "Operation is done" -Status "100% Complete:" -Completed

    Write-Host "Datastore '$($NewDatastore.Name)' is successfully restored"
    return $NewDatastore
}


function Remove-PCBSVmfsDatastore {
  <#
    .SYNOPSIS
     Detaches and unmount a VMFS datastore from a cluster. Remove the datastore from the host group
    .DESCRIPTION
     Detaches and unmount a VMFS datastore from a cluster. Remove the datastore from the host group. The connection configured for the volume and cluster host/host group will be removed from Pure Cloud Block Store. The volume will be destroyed if there is no other connection left.
    .PARAMETER ClusterName
     Cluster name
    .PARAMETER DatastoreName
     Datastore name
    .PARAMETER PureCloudBlockStoreConnection
     Pure Cloud Block Store Connection. The connection can be created using Connect-Pf2Array cmdlet
    .PARAMETER AVSCloudName
     AVS cloud name
    .PARAMETER AzureSubscriptionId
     Azure subscription Id. If not provided, default subscription will be used
    .PARAMETER AVSResourceGroup
     AVS ResourceGroup
    .EXAMPLE
     Remove-PCBSVmfsDatastore -ClusterName "mycluster" -DatastoreName "myDatastore" PureCloudBlockStoreConnection $CBSConnection `
        -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup

     Takes in a datastore name, datastore would be removed.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [String]$ClusterName,

        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [String]$DatastoreName,

        [Parameter(Mandatory=$false)]
        $PureCloudBlockStoreConnection,

        [Parameter(Mandatory=$false)]
        [String]$AVSCloudName,

        [Parameter(Mandatory=$false)]
        [String]$AzureSubscriptionId,

        [Parameter(Mandatory=$false)]
        [String]$AVSResourceGroup
    )
    Write-Progress -Activity "Removing datastore" -Status "0% Complete:" -PercentComplete 1

    Connect-AVSvCenter -AVSResourceGroupName $AVSResourceGroup -AVSPrivateCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId

    $FlashArray = Connect-PureCloudBlockStore -PureCloudBlockStoreConnection $PureCloudBlockStoreConnection
    Remove-PfaVmfsDatastore -ClusterName $ClusterName -DatastoreName $DataStoreName -FlashArray $FlashArray `
        -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup

    Write-Host "Datastore '$DatastoreName' is successfully removed"
    Write-Progress -Activity "Operation is done" -Status "100% Complete:" -Completed
}


function Set-PCBSVmfsCapacity {
  <#
    .SYNOPSIS
     Increase the size of a Pure Cloud Block Store-based VMFS datastore.
    .DESCRIPTION
     Takes in a datastore, the corresponding Pure Cloud Block Store, and a new size. Both the volume and the VMFS will be grown.
    .PARAMETER ClusterName
     Cluster name
    .PARAMETER DatastoreName
     Datastore name
    .PARAMETER Size
     New datastore capacity size in bytes
    .PARAMETER PureCloudBlockStoreConnection
     Pure Cloud Block Store Connection. The connection can be created using Connect-Pf2Array cmdlet
    .PARAMETER AVSCloudName
     AVS cloud name
    .PARAMETER AzureSubscriptionId
     Azure subscription Id. If not provided, default subscription will be used
    .PARAMETER AVSResourceGroup
     AVS ResourceGroup

    .EXAMPLE
     Set-PCBSVmfsCapacity -ClusterName "mycluster" -DatastoreName myDatastore -Size 3GB -PureCloudBlockStoreConnection $CBSConnection `
        -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup

     Expand size of the datastore myDatastore to 3GB.
    #>

    [CmdletBinding()]
    Param (
          [Parameter(Mandatory=$true)]
          [String]$ClusterName,

          [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
          [string]$DatastoreName,

          [Parameter(Mandatory=$true)]
          [UInt64]$Size,

          [Parameter(Mandatory=$false)]
          $PureCloudBlockStoreConnection,

          [Parameter(Mandatory=$false)]
          [String]$AVSCloudName,

          [Parameter(Mandatory=$false)]
          [String]$AzureSubscriptionId,

          [Parameter(Mandatory=$false)]
          [String]$AVSResourceGroup
    )
    Write-Progress -Activity "Resizing datastore" -Status "0% Complete:" -PercentComplete 1
    Connect-AVSvCenter -AVSResourceGroupName $AVSResourceGroup -AVSPrivateCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId
    $FlashArray = Connect-PureCloudBlockStore -PureCloudBlockStoreConnection $PureCloudBlockStoreConnection
    $Cluster = Get-Cluster -Name $ClusterName -ErrorAction Ignore
    if (-not $Cluster) {
        throw "Cluster $ClusterName does not exist."
    }
    Set-PfaVmfsCapacity -ClusterName $ClusterName -FlashArray $FlashArray -DatastoreName $DatastoreName -SizeInByte $Size `
        -AVSCloudName $AVSCloudName -AzureSubscriptionId $AzureSubscriptionId -AVSResourceGroup $AVSResourceGroup

    Write-Host "Datastore '$DatastoreName' is successfully resized"
    Write-Progress -Activity "Operation is done" -Status "100% Complete:" -Completed
}

<#
.SYNOPSIS
Fully remove a Pure Storage CBS AVS monitor deployment from Azure infrastructure

.DESCRIPTION
Fully remove a Pure Storage CBS AVS monitor deployment from Azure infrastructure. The resource group and its resources will be destroyed. The vNet subnet used by the monitor will also be remove from the vNet.

.PARAMETER MonitorResourceGroup
ResourceGroup to host monitor infrastructure components

.EXAMPLE
Remove-PCBSAVSMonitor -MonitorResourceGroup "myAVSMonitorResourceGroup"

.NOTES
Must be logged in to Azure (using Connect-AzAccount) before running this cmdlet
#>

function Remove-PCBSAVSMonitor {
  [CmdletBinding()]
    Param (
      [Parameter(Mandatory=$true)]
      [String]$MonitorResourceGroup
    )

    Import-Module Az.Resources
    $ResourceGroup = Get-AzResourceGroup $MonitorResourceGroup -ErrorAction ignore

    if (-not $ResourceGroup) {
        throw "Pure Storage CBS AVS monitor $MonitorResourceGroup does not exist"
    }

    if ([string]::IsNullOrEmpty($ResourceGroup.Tags["PureStorage.CBS.AVS"])) {
        throw "The resource group provided is not Pure Storage CBS AVS monitor resource group. Only Pure Storage CBS AVS monitor resource group can be removed by this command"
    }

    # smartDetector is auto configured without tag. Ignore this component
    $NonMonitorResources = Get-AzResource -ResourceGroupName $MonitorResourceGroup | Where-Object { [string]::IsNullOrEmpty($_.tags["AVSMonitorResourceGroupName"]) -and $_.ResourceType -ne "microsoft.alertsmanagement/smartDetectorAlertRules"}
    if ($NonMonitorResources.Count -ge 1) {
        throw "Non Pure Storage CBS AVS monitor resource $($MonitorResources.Name) detected. Please manually remove the resource before removing the whole monitor"
    }

    $MonitorFuncApp = Get-AzFunctionApp -ResourceGroupName $MonitorResourceGroup
    $MonitorKeyVault = Get-AzKeyVault -ResourceGroupName $MonitorResourceGroup

    # Vnet name here is constructed as {vNetResouceGUI}-{SubnetName}
    # eg. fece391b-8f4e-4e05-a203-e5961cdd9fd1_subnet-avsfuncappsbqzuuqxofe2q
    $vNetResourceGUID = $MonitorFuncApp.SiteConfig.VnetName.Split("_")[0]
    $MonitorSubnetName = $MonitorFuncApp.SiteConfig.VnetName.Split("_")[1]
    $MonitorVNet = get-AzVirtualNetwork | Where-Object {$_.ResourceGuid -eq $vNetResourceGUID}

    Write-Host "Removing resource group $MonitorResourceGroup..."
    Remove-AzResourceGroup $MonitorResourceGroup -Force | Out-Null

    # Remove subnet
    Write-Host "Removing subnet $MonitorSubnetName from vNet $($MonitorVNet.Name)..."
    Remove-AzVirtualNetworkSubnetConfig -Name $MonitorSubnetName -VirtualNetwork $MonitorVNet | Set-AzVirtualNetwork | Out-Null

    # Purge key vault
    Write-Host "Purging key vault $($MonitorKeyVault.VaultName)..."
    Remove-AzKeyVault -Name $MonitorKeyVault.VaultName -InRemovedState -Force -Location $ResourceGroup.Location | Out-Null
}

<#
.SYNOPSIS
Deploy or update a Pure Storage CBS AVS monitor to Azure infrastructure

.DESCRIPTION
Deploy or update a Pure Storage CBS AVS monitor to Azure infrastructure. The monitor will scan periodically for AVS Cluster/Host changes and will make sure to change iSCSI configuration accordingly.

.PARAMETER MonitorResourceGroup
Resource group to host monitor infrastructure components. The resource group will be created if not exists

.PARAMETER MonitorResourceGroupLocation
Resource group location to host monitor infrastructure components.

.PARAMETER AVSCloudName
AVS cloud name

.PARAMETER AVSResourceGroup
AVS ResourceGroup

.PARAMETER PureCloudBlockStoreEndpoint
Pure Cloud Block Store endpoint address

.PARAMETER PureCloudBlockStoreCredential
Pure Cloud Block Store credentials

.PARAMETER VNetName
An existing VNet name. The VNey specified should have access to AVS as well as Pure Storage Cloud Block Store (CBS) array.

.PARAMETER VNetResourceGroup
VNet REsourceGroup

.PARAMETER VNetSubnetAddress
The VNet subnet address range in CIDR notation (e.g. 192.168.1.0/24). It must be contained by the address space of the virtual network.

.EXAMPLE
Deploy-PCBSAVSMonitor -PureCloudBlockStoreEndpoint "192.168.2.100" -PureCloudBlockStoreCredential (Get-Credential) -AVSCloudName "my-avs" -AVSResourceGroup "avs-resourcegroup" `
              -MonitorResourceGroup "NewResourceGroup" -VNetName "my-vnet" -VNetResourceGroup "vnet-resourcegroup" -VNetSubnetAddress "192.168.3.0/24"

.NOTES
Must be logged in to Azure (using Connect-AzAccount) before running this cmdlet
#>

function Deploy-PCBSAVSMonitor {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [String]$MonitorResourceGroup,

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

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

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

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

        [Parameter(Mandatory=$true)]
        [pscredential]$PureCloudBlockStoreCredential,

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

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

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

        [Parameter(Mandatory=$false)]
        [ValidateScript({ $_ -ge 10 }, ErrorMessage = "The minimum interval for the monitor is 10 minutes.")]
        [int]$MoinitorIntervalInMinute = 10
    )
    Import-Module Az.Resources
    $ProductVersion = (Get-Module "PureStorage.CBS.AVS").Version.ToString()

    $ResourceGroup = Get-AzResourceGroup $MonitorResourceGroup -ErrorAction ignore
    if (-not $ResourceGroup) {
        Write-Host "Resource group $MonitorResourceGroup does not exist. Creating the resource group..."
        New-AzResourceGroup $MonitorResourceGroup -Location $MonitorResourceGroupLocation -Tag @{'PureStorage.CBS.AVS' = $ProductVersion} | Out-Null
    } else {
        if ($ResourceGroup.location -ne $MonitorResourceGroupLocation) {
            throw "The resource group $MonitorResourceGroup exists but its location $($ResourceGroup.location) does not match provided location $MonitorResourceGroupLocation"
        }

        # If the resource group exists and it's empty, we'll use the resource group even though there is no tag
        $Resources = Get-AzResource -ResourceGroupName $MonitorResourceGroup
        if ($Resources.Count -eq 0) {
            Set-AzResourceGroup -Name $MonitorResourceGroup -Tag @{'PureStorage.CBS.ACS' = $ProductVersion}
        } elseif (-not $ResourceGroup.Tags["PureStorage.CBS.AVS"]) {
            throw "The resource group $MonitorResourceGroup exists but not used by Pure Storage CBS AVS monitor. Please select another name for Pure Storage CBS AVS monitor"
        }
    }

    $DeploymentTemplatePath = Join-Path -Path $PSScriptRoot -ChildPath 'templates' -AdditionalChildPath 'Main.bicep'
    $DeploymentId = (New-Guid).ToString()
    $DeploymentParams = @{
        "PureCloudBlockStoreEndpoint" = $PureCloudBlockStoreEndpoint;
        "PureCloudBlockStoreUsername" = $PureCloudBlockStoreCredential.UserName;
        "PureCloudBlockStorePassword" = $PureCloudBlockStoreCredential.Password;
        "AVSCloudName" = $AVSCloudName;
        "AVSResourceGroup" = $AVSResourceGroup;
        "VNetName" = $VNetName;
        "VNetResourceGroupName" = $VNetResourceGroup;
        "SubnetAddressRange" = $VNetSubnetAddress;
        "MoinitorIntervalInMinute" = $MoinitorIntervalInMinute
        "DeploymentId" = $DeploymentId
    }

    Write-Host "Deploying AVS monitoring infrastructure to Azure..."
    New-AzResourceGroupDeployment -Name "PCBSMonitorDeployment_$DeploymentId" -ResourceGroupName $MonitorResourceGroup -TemplateFile $DeploymentTemplatePath  -TemplateParameterObject $DeploymentParams
}

<#
.SYNOPSIS
List the existing Pure Cloud Block Store in the Pure Storage CBS AVS monitor

.DESCRIPTION
List the existing Pure Cloud Block Store in the Pure Storage CBS AVS monitor

.PARAMETER MonitorResourceGroup
ResourceGroup to host monitor infrastructure components

.EXAMPLE
Get-PCBSAVSMonitorArray -MonitorResourceGroup myMonitor

.NOTES
Must be logged in to Azure (using Connect-AzAccount) before running this cmdlet
#>


function Get-PCBSAVSMonitorArray {
    [CmdletBinding()]
    Param (
      [Parameter(Mandatory=$true)]
      [String]$MonitorResourceGroup
    )

    $UserPrincipalName = (Get-AzContext).Account.Id
    $KeyVault = Get-AzKeyVault -ResourceGroupName $MonitorResourceGroup
    Set-AzKeyVaultAccessPolicy -VaultName $KeyVault.VaultName  -UserPrincipalName $UserPrincipalName -PermissionsToSecrets set,delete,get,purge,list

    $Arrays = @()
    $Secrets = Get-AzKeyVaultSecret -VaultName $KeyVault.VaultName | where-object {$_.Name -like "*-$($KeyVault.VaultName)-username"}
    foreach ($Secret in $Secrets) {
        $ArrayName = ($Secret.name -Split "-$($KeyVault.VaultName)-username")[0]
        # If the string matches the format like "172-168-1-0", the array ip addressed was processed because secret name does not allow "."
        if ($ArrayName -match "^\d+-\d+-\d+-\d+$") {
            $ArrayName = $ArrayName.Replace("-", ".")
        }
        $Arrays += $ArrayName
    }

    return $Arrays
}


<#
.SYNOPSIS
Add a Pure Cloud Block Store to an existing Pure Storage CBS AVS monitor

.DESCRIPTION
Add a Pure Cloud Block Store to an existing Pure Storage CBS AVS monitor. If the Pure Block store already exists, it will be overwritten

.PARAMETER MonitorResourceGroup
ResourceGroup to host monitor infrastructure components

.PARAMETER PureCloudBlockStoreEndpoint
Pure Cloud Block Store endpoint address

.PARAMETER PureCloudBlockStoreCredential
Pure Cloud Block Store credential

.EXAMPLE
Add-PCBSAVSMonitorArray -MonitorResourceGroup myMonitorGroup -PureCloudBlockStoreEndpoint myArray -PureCloudBlockStoreCredential (Get-Credential)

.NOTES
Must be logged in to Azure (using Connect-AzAccount) before running this cmdlet
#>

function Add-PCBSAVSMonitorArray {
    [CmdletBinding()]
    Param (
      [Parameter(Mandatory=$true)]
      [String]$MonitorResourceGroup,

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

      [Parameter(Mandatory=$true)]
      [pscredential]$PureCloudBlockStoreCredential
    )
    $PureCloudBlockStoreEndpointOrigin = $PureCloudBlockStoreEndpoint
    Write-Host "Adding Pure Cloud Block Store $PureCloudBlockStoreEndpointOrigin to monitor resource group $MonitorResourceGroup..."

    $UserPrincipalName = (Get-AzContext).Account.Id
    $KeyVault = Get-AzKeyVault -ResourceGroupName $MonitorResourceGroup
    Set-AzKeyVaultAccessPolicy -VaultName $KeyVault.VaultName  -UserPrincipalName $UserPrincipalName -PermissionsToSecrets set,delete,get,purge,list

    # Make sure the credential works before adding to the monitor
    $Array = Connect-Pfa2array -Endpoint $PureCloudBlockStoreEndpoint -Credential $PureCloudBlockStoreCredential  -IgnoreCertificateError -ErrorAction Stop
    if (-not $Array) {
        throw "Failed to connect to the Pure Cloud Block Store. Please check the endpoint and credential of the Pure Cloud Block Store."
    }


    if ($PureCloudBlockStoreEndpoint -match "^\d+.\d+.\d+.\d+$") {
        $PureCloudBlockStoreEndpoint = $PureCloudBlockStoreEndpoint.Replace(".", "-")
    }

    $Secret = Get-AzKeyVaultSecret -VaultName $KeyVault.VaultName | where-object {$_.Name -eq "$($PureCloudBlockStoreEndpoint)-$($KeyVault.VaultName)-username"}
    if ($Secret) {
        Write-Host "Overriding the existing credential for Pure Cloud Block Store $PureCloudBlockStoreEndpointOrigin..."
    }

    Set-AzKeyVaultSecret -VaultName $KeyVault.VaultName -Name "$($PureCloudBlockStoreEndpoint)-$($KeyVault.VaultName)-username" -SecretValue (ConvertTo-SecureString -String $PureCloudBlockStoreCredential.UserName -AsPlainText -Force)
    Set-AzKeyVaultSecret -VaultName $KeyVault.VaultName -Name "$($PureCloudBlockStoreEndpoint)-$($KeyVault.VaultName)-password" -SecretValue $PureCloudBlockStoreCredential.Password

    Write-Host "The Pure Cloud Block Store $PureCloudBlockStoreEndpointOrigin is successfully added to monitor resource group $MonitorResourceGroup."
}

<#
.SYNOPSIS
Remove an existing Pure Cloud Block Store from a Pure Storage CBS AVS monitor

.DESCRIPTION
Remove an existing Pure Cloud Block Store from a Pure Storage CBS AVS monitor. If the Pure Block store already exists, it will be overwritten

.PARAMETER MonitorResourceGroup
ResourceGroup to host monitor infrastructure components

.PARAMETER PureCloudBlockStoreEndpoint
Pure Cloud Block Store endpoint address

.EXAMPLE
Remove-PCBSAVSMonitorArray -MonitorResourceGroup myMonitorGroup -PureCloudBlockStoreEndpoint myArray

.NOTES
Must be logged in to Azure (using Connect-AzAccount) before running this cmdlet
#>

function Remove-PCBSAVSMonitorArray {
    [CmdletBinding()]
    Param (
      [Parameter(Mandatory=$true)]
      [String]$MonitorResourceGroup,

      [Parameter(Mandatory=$true)]
      [String]$PureCloudBlockStoreEndpoint
    )
    $PureCloudBlockStoreEndpointOrigin = $PureCloudBlockStoreEndpoint
    $KeyVault = Get-AzKeyVault -ResourceGroupName $MonitorResourceGroup
    Write-Host "Removing Pure Cloud Block Store $PureCloudBlockStoreEndpointOrigin from monitor resource group $MonitorResourceGroup..."

    $UserPrincipalName = (Get-AzContext).Account.Id
    Set-AzKeyVaultAccessPolicy -VaultName $KeyVault.VaultName  -UserPrincipalName $UserPrincipalName -PermissionsToSecrets set,delete,get,purge,list

    if ($PureCloudBlockStoreEndpoint -match "^\d+.\d+.\d+.\d+$") {
        $PureCloudBlockStoreEndpoint = $PureCloudBlockStoreEndpoint.Replace(".", "-")
    }

    $Secret = Get-AzKeyVaultSecret -VaultName $KeyVault.VaultName | where-object {$_.Name -eq "$($PureCloudBlockStoreEndpoint)-$($KeyVault.VaultName)-username"}
    if (-not $Secret) {
        throw "Pure Cloud Block Store $PureCloudBlockStoreEndpointOrigin does not exist in the monitor resource group $MonitorResourceGroup"
    }

    Remove-AzKeyVaultSecret -VaultName $KeyVault.VaultName -Name "$($PureCloudBlockStoreEndpoint)-$($KeyVault.VaultName)-username" -Force
    Remove-AzKeyVaultSecret -VaultName $KeyVault.VaultName -Name "$($PureCloudBlockStoreEndpoint)-$($KeyVault.VaultName)-password" -Force

    # Purge secret
    Purge-AzureSecretWithRetry -KeyVaultName $KeyVault.VaultName -SecretName "$($PureCloudBlockStoreEndpoint)-$($KeyVault.VaultName)-username"
    Purge-AzureSecretWithRetry -KeyVaultName $KeyVault.VaultName -SecretName "$($PureCloudBlockStoreEndpoint)-$($KeyVault.VaultName)-password"


    Write-Host "The Pure Cloud Block Store $PureCloudBlockStoreEndpointOrigin is successfully removed."
}

Export-ModuleMember -Function Build-PCBSCluster
Export-ModuleMember -Function New-PCBSVmfsDatastore
Export-ModuleMember -Function Restore-PCBSVmfsDatastore
Export-ModuleMember -Function Remove-PCBSVmfsDatastore
Export-ModuleMember -Function Set-PCBSVmfsCapacity
Export-ModuleMember -Function Deploy-PCBSAVSMonitor
Export-ModuleMember -Function Remove-PCBSAVSMonitor
Export-ModuleMember -Function Add-PCBSAVSMonitorArray
Export-ModuleMember -Function Remove-PCBSAVSMonitorArray
Export-ModuleMember -Function Get-PCBSAVSMonitorArray
# SIG # Begin signature block
# MIIjUAYJKoZIhvcNAQcCoIIjQTCCIz0CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUETNiAS6wOBfqLGIUOdHwjR6X
# f0eggh12MIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1b5VQCDANBgkqhkiG9w0B
# AQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk
# IElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgxMDIyMTIwMDAwWjByMQsw
# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
# ZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQg
# Q29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
# +NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLXcep2nQUut4/6kkPApfmJ
# 1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSRI5aQd4L5oYQjZhJUM1B0
# sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXiTWAYvqrEsq5wMWYzcT6s
# cKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5Ng2Q7+S1TqSp6moKq4Tz
# rGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8vYWxYoNzQYIH5DiLanMg
# 0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYDVR0TAQH/BAgwBgEB/wIB
# ADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwMweQYIKwYBBQUH
# AQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYI
# KwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFz
# c3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0
# LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaG
# NGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RD
# QS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUHAgEWHGh0
# dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZIAYb9bAMwHQYDVR0OBBYE
# FFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6en
# IZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPzItEVyCx8JSl2qB1dHC06
# GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRupY5a4l4kgU4QpO4/cY5j
# DhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKNJK4kxscnKqEpKBo6cSgC
# PC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmifz0DLQESlE/DmZAwlCEIy
# sjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN3fYBIM6ZMWM9CBoYs4Gb
# T8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKyZqHnGKSaZFHvMIIFNzCC
# BB+gAwIBAgIQC4jZOitkx57ksuMgsWXX0jANBgkqhkiG9w0BAQsFADByMQswCQYD
# VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGln
# aWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ29k
# ZSBTaWduaW5nIENBMB4XDTIwMDczMDAwMDAwMFoXDTIzMTAwNDEyMDAwMFowdDEL
# MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50
# YWluIFZpZXcxGzAZBgNVBAoTElB1cmUgU3RvcmFnZSwgSW5jLjEbMBkGA1UEAxMS
# UHVyZSBTdG9yYWdlLCBJbmMuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
# AQEA6nefE6+A0nNMY82xtkQb+akwI0oxLqEbRY65bE4re+CVQV2xP89/7FIAXooq
# jxNOvrtWicWTGOZjBdAFEXXAUEyu9CkWFOXGLV3/QkcEfY3e3Z3jypa6h1EznSEp
# 3wSQEIVbigi6jR2s7NDnDoSDAKnzcGcSZ8Nz7akXFrN8PAmg3gy8a/rwm2Ko7ClR
# ZUHj1C/OMPXUqiN0Q4FyAsaeFmvg2PX2twxo192WRdNro1dkKNfmDvym2ss6MXcq
# gFEjBcZtHDv3e/i0BjT24Jm1C27IVYZzVf28dqmVfPo7l8bLEHsVKgrRf0VzS2wI
# R08gXiMrohf9tv3AbvtPk7ZaawIDAQABo4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5
# eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYEFJOwFn7S4Lj6QdNnH6tpm3FMFWJI
# MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzB3BgNVHR8EcDBu
# MDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVkLWNz
# LWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTItYXNz
# dXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAwEwKjAoBggrBgEF
# BQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBBAEwgYQG
# CCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tME4GCCsGAQUFBzAChkJodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25pbmdDQS5jcnQwDAYDVR0TAQH/BAIw
# ADANBgkqhkiG9w0BAQsFAAOCAQEAMVAa7mXxhBHf0dTzf6LKDncN8KnzB59KBvd0
# KvXZc6FoLHGi2Wg6XBSP+9mdDMMYOkohqstSk7RD+reT8xiptrIkSMcVcTog1Z3e
# JjYTK8B7QsSpuu2lo0RWA5rdvqMJ+lVzbbjteTq+uicP4T/EDwv2q+iPAgpXQD8y
# r084ExDWJtMfhvy0cxh555xx88rvFWOhJnXYiFtjaO9dp7f2TnZRJ44rmB98jc9E
# BR/8GLOi/BhyPiiU4nBv8JIHVP1E5zIt8/9PhfpenmiWBbuuP0YLnzrqRhswtJaq
# jJirYNLYojmINrbvdcpEKGK1AitsnuOjFadLI7bc696Y35lUATCCBY0wggR1oAMC
# AQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZIhvcNAQEMBQAwZTELMAkGA1UEBhMC
# VVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0
# LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4XDTIy
# MDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVowYjELMAkGA1UEBhMCVVMxFTATBgNV
# BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8G
# A1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjwwIjBpM+zCpyUuySE98orYWcLhKac9
# WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J58soR0uRf1gU8Ug9SH8aeFaV+vp+p
# VxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMHhOZ0O21x4i0MG+4g1ckgHWMpLc7s
# Xk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6Zu53yEioZldXn1RYjgwrt0+nMNlW
# 7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQecN4x7axxLVqGDgDEI3Y1DekLgV9
# iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4bA3VdeGbZOjFEmjNAvwjXWkmkwuap
# oGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9WV1CdoeJl2l6SPDgohIbZpp0yt5L
# HucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCUtNJhbesz2cXfSwQAzH0clcOP9yGy
# shG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvoZKYz0YkH4b235kOkGLimdwHhD5QM
# IR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/JvNNBERJb5RBQ6zHFynIWIgnffEx1
# P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCPorF+CiaZ9eRpL5gdLfXZqbId5RsC
# AwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOzX44LScV1k
# TN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA4G
# A1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6
# Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDBFBgNVHR8E
# PjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1
# cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgwBgYEVR0gADANBgkqhkiG9w0BAQwF
# AAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cHvZqsoYcs7IVeqRq7IviHGmlUIu2k
# iHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8UgPITtAq3votVs/59PesMHqai7Je
# 1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTnf+hZqPC/Lwum6fI0POz3A8eHqNJM
# QBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxUjG/voVA9/HYJaISfb8rbII01YBwC
# A8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8jLfR+cWojayL/ErhULSd+2DrZ8LaH
# lv1b0VysGMNNn3O3AamfV6peKOK5lDCCBq4wggSWoAMCAQICEAc2N7ckVHzYR6z9
# KGYqXlswDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERp
# Z2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMY
# RGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4XDTIyMDMyMzAwMDAwMFoXDTM3MDMy
# MjIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
# MTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRp
# bWVTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMaG
# NQZJs8E9cklRVcclA8TykTepl1Gh1tKD0Z5Mom2gsMyD+Vr2EaFEFUJfpIjzaPp9
# 85yJC3+dH54PMx9QEwsmc5Zt+FeoAn39Q7SE2hHxc7Gz7iuAhIoiGN/r2j3EF3+r
# GSs+QtxnjupRPfDWVtTnKC3r07G1decfBmWNlCnT2exp39mQh0YAe9tEQYncfGpX
# evA3eZ9drMvohGS0UvJ2R/dhgxndX7RUCyFobjchu0CsX7LeSn3O9TkSZ+8OpWNs
# 5KbFHc02DVzV5huowWR0QKfAcsW6Th+xtVhNef7Xj3OTrCw54qVI1vCwMROpVymW
# Jy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OAe3VuJyWQmDo4EbP29p7mO1vsgd4iFNmC
# KseSv6De4z6ic/rnH1pslPJSlRErWHRAKKtzQ87fSqEcazjFKfPKqpZzQmiftkaz
# nTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++bPf4OuGQq+nUoJEHtQr8FnGZJUlD0UfM2
# SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+OcD5sjClTNfpmEpYPtMDiP6zj9NeS3YS
# UZPJjAw7W4oiqMEmCPkUEBIDfV8ju2TjY+Cm4T72wnSyPx4JduyrXUZ14mCjWAkB
# KAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZDNIztM2xAgMBAAGjggFdMIIBWTASBgNV
# HRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBS6FtltTYUvcyl2mi91jGogj57IbzAf
# BgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qYrhwPTzAOBgNVHQ8BAf8EBAMCAYYw
# EwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYBBQUHAQEEazBpMCQGCCsGAQUFBzAB
# hhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKGNWh0dHA6Ly9j
# YWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3J0MEMG
# A1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
# dFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG
# /WwHATANBgkqhkiG9w0BAQsFAAOCAgEAfVmOwJO2b5ipRCIBfmbW2CFC4bAYLhBN
# E88wU86/GPvHUF3iSyn7cIoNqilp/GnBzx0H6T5gyNgL5Vxb122H+oQgJTQxZ822
# EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXOlWk/R3f7cnQU1/+rT4osequFzUNf7WC2
# qk+RZp4snuCKrOX9jLxkJodskr2dfNBwCnzvqLx1T7pa96kQsl3p/yhUifDVinF2
# ZdrM8HKjI/rAJ4JErpknG6skHibBt94q6/aesXmZgaNWhqsKRcnfxI2g55j7+6ad
# cq/Ex8HBanHZxhOACcS2n82HhyS7T6NJuXdmkfFynOlLAlKnN36TU6w7HQhJD5TN
# OXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEhQNC3EyTN3B14OuSereU0cZLXJmvkOHOr
# pgFPvT87eK1MrfvElXvtCl8zOYdBeHo46Zzh3SP9HSjTx/no8Zhf+yvYfvJGnXUs
# HicsJttvFXseGYs2uJPU5vIXmVnKcPA3v5gA3yAWTyf7YGcWoWa63VXAOimGsJig
# K+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHzV9m8BPqC3jLfBInwAM1dwvnQI38AC+R2
# AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZVVCsfgPrA8g4r5db7qS9EFUrnEw4d2zc4
# GqEr9u3WfPwwggbAMIIEqKADAgECAhAMTWlyS5T6PCpKPSkHgD1aMA0GCSqGSIb3
# DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7
# MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1l
# U3RhbXBpbmcgQ0EwHhcNMjIwOTIxMDAwMDAwWhcNMzMxMTIxMjM1OTU5WjBGMQsw
# CQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNlcnQxJDAiBgNVBAMTG0RpZ2lDZXJ0
# IFRpbWVzdGFtcCAyMDIyIC0gMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAM/spSY6xqnya7uNwQ2a26HoFIV0MxomrNAcVR4eNm28klUMYfSdCXc9FZYI
# L2tkpP0GgxbXkZI4HDEClvtysZc6Va8z7GGK6aYo25BjXL2JU+A6LYyHQq4mpOS7
# eHi5ehbhVsbAumRTuyoW51BIu4hpDIjG8b7gL307scpTjUCDHufLckkoHkyAHoVW
# 54Xt8mG8qjoHffarbuVm3eJc9S/tjdRNlYRo44DLannR0hCRRinrPibytIzNTLlm
# yLuqUDgN5YyUXRlav/V7QG5vFqianJVHhoV5PgxeZowaCiS+nKrSnLb3T254xCg/
# oxwPUAY3ugjZNaa1Htp4WB056PhMkRCWfk3h3cKtpX74LRsf7CtGGKMZ9jn39cFP
# cS6JAxGiS7uYv/pP5Hs27wZE5FX/NurlfDHn88JSxOYWe1p+pSVz28BqmSEtY+VZ
# 9U0vkB8nt9KrFOU4ZodRCGv7U0M50GT6Vs/g9ArmFG1keLuY/ZTDcyHzL8IuINeB
# rNPxB9ThvdldS24xlCmL5kGkZZTAWOXlLimQprdhZPrZIGwYUWC6poEPCSVT8b87
# 6asHDmoHOWIZydaFfxPZjXnPYsXs4Xu5zGcTB5rBeO3GiMiwbjJ5xwtZg43G7vUs
# fHuOy2SJ8bHEuOdTXl9V0n0ZKVkDTvpd6kVzHIR+187i1Dp3AgMBAAGjggGLMIIB
# hzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggr
# BgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwHwYDVR0j
# BBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYEFGKK3tBh/I8xFO2X
# C809KpQU31KcMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdD
# QS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz
# cC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9jYWNlcnRzLmRpZ2lj
# ZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBp
# bmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAFWqKhrzRvN4Vzcw/HXjT9aFI/H8
# +ZU5myXm93KKmMN31GT8Ffs2wklRLHiIY1UJRjkA/GnUypsp+6M/wMkAmxMdsJiJ
# 3HjyzXyFzVOdr2LiYWajFCpFh0qYQitQ/Bu1nggwCfrkLdcJiXn5CeaIzn0buGqi
# m8FTYAnoo7id160fHLjsmEHw9g6A++T/350Qp+sAul9Kjxo6UrTqvwlJFTU2WZoP
# VNKyG39+XgmtdlSKdG3K0gVnK3br/5iyJpU4GYhEFOUKWaJr5yI+RCHSPxzAm+18
# SLLYkgyRTzxmlK9dAlPrnuKe5NMfhgFknADC6Vp0dQ094XmIvxwBl8kZI4DXNlpf
# lhaxYwzGRkA7zl011Fk+Q5oYrsPJy8P7mxNfarXH4PMFw1nfJ2Ir3kHJU7n/NBBn
# 9iYymHv+XEKUgZSCnawKi8ZLFUrTmJBFYDOA4CPe+AOk9kVH5c64A0JH6EE2cXet
# /aLol3ROLtoeHYxayB6a1cLwxiKoT5u92ByaUcQvmvZfpyeXupYuhVfAYOd4Vn9q
# 78KVmksRAsiCnMkaBXy6cbVOepls9Oie1FqYyJ+/jbsYXEP10Cro4mLueATbvdH7
# WwqocH7wl4R44wgDXUcsY6glOJcB0j862uXl9uab3H4szP8XTE0AotjWAQ64i+7m
# 4HJViSwnGWH2dwGMMYIFRDCCBUACAQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNV
# BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8G
# A1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmluZyBDQQIQ
# C4jZOitkx57ksuMgsWXX0jAJBgUrDgMCGgUAoHAwEAYKKwYBBAGCNwIBDDECMAAw
# GQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisG
# AQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFE5jx7klLPlML1H3KBGJV4U6RVu2MA0G
# CSqGSIb3DQEBAQUABIIBADlDW0uRh0Jp0M1VK4S20BH8CRsIst77tZogk8x0+w/U
# Vfnhq7u4pq7hgq+cxPCuzx/QFPNuOBnjjctA3I3+86TSN/sQQxmg2DnHZBc+SjWg
# ECPL4v95G98BWS3vE4RCk/+pq/7ACdrO5R6VjTVNn39sBKnwMPjs9eQ7UQAHrBSL
# mcNSKDs2gdrFbTKI+GsrSyvDb7smGz72QNYfRwvjZMIo1yXn12HA+HlZ4w97aI6z
# atbvtBaLYEPIN6kBoEPml2pu/1lE6ZD7ECsl6axgmJXcloJs59n16YTA1dERxYXY
# Umd3f1/yWQzRwBynCZAwMRhR3zI4irvDXBrOUZ03e6ehggMgMIIDHAYJKoZIhvcN
# AQkGMYIDDTCCAwkCAQEwdzBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNl
# cnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBT
# SEEyNTYgVGltZVN0YW1waW5nIENBAhAMTWlyS5T6PCpKPSkHgD1aMA0GCWCGSAFl
# AwQCAQUAoGkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUx
# DxcNMjMwNzI2MDU0MDEzWjAvBgkqhkiG9w0BCQQxIgQgKY9u9o96rJxl2ATAe0VT
# 1FYpdGqxk+bHLBgAK6cxcUUwDQYJKoZIhvcNAQEBBQAEggIAP/edNgAAFTfTOYHd
# DODPOkXMMDkoCNiin4McsYvLrGwZct+Ex02fWUtUPoLydwOxzHq4gsfPYRbQ9IaQ
# lhliGwvJY3foRohSvdcHFCo819PlBj+M5tpFIIcSrvymE2oos+qi73CJRT5y6JcA
# N2/EMa6h0+GrFw1mqqFYRQQu/gdEflQp/dAxrEdkNOy0EgsSaUCqFqwluPakvRZs
# qR1Hv2qbwAsVrZE8vs17qlT2kTlyAdCSu+TUC70ioRkfPvYn+TnI/8mTPrIsisjF
# rJbqnd07Dycbns6WhltZcTPXZpJeambO3ADzvTRBQnJpuyOFYO0dhDabTNZOlErj
# 6P3Vr5JOPiqHGFOM+cXT/TTBJFD5XIr9YCcLvaTS7xSs0R4Ex7LUvRzbUjQOdAOx
# PR4vfUENQ9q6TfRpX7mfEBRbh695WY/CDKbaoFiL8mueWVdssodyLaovGjgNeEvI
# IGVXhIiyxFG9dXzls1jYvwdqDUHpcpXq/2hgZHduvFJgWAaEcMygRzKnkwGdxEmV
# f7/xYuFEx+WKUTiLZm3cDjyM2NCVMSLj+Ey8DDBx7loioxeoTCFc0tmvlMZFeXJI
# TVxJu6wd7RXydzsC7AXZsWhmWpVmK3JO4ShdlRjNxNrLwY8gYRkIu4lIZ/IblLP2
# 0SmxAZ1vg6YwIC7BsFksg+AwDkQ=
# SIG # End signature block