Utils.ps1

 #----------------------------------------------------------------------------------------------------------
# Utils.ps1
# A module for facilitating remote execution of commands
#----------------------------------------------------------------------------------------------------------
function GetApplicationFolder {
    if((Test-Path variable:isLinux) -and ($isLinux -or $isMacOS)) {
        #mac or linux
        $appFolder = Join-Path "~/.config" "PureStorage"
    }
    else{
        #windows
        $appFolder = Join-Path $env:programdata "PureStorage"
    }

    if (-not (Test-Path $appFolder)){
        # create app folder if needed
        New-Item -ItemType Directory -Path $appFolder | Out-Null
    }
    return (resolve-path $appFolder).Path
}

function Get-InstallationId {
    $appFolder = GetApplicationFolder
    
    # get installation.id path depending on the OS
    if((Test-Path variable:isLinux) -and ($isLinux -or $isMacOS)) {
        #mac or linux
        $installationPath = Join-Path $appFolder "$($env:USER)_PSB_installation.id"
    }
    else{
        #windows
        $installationPath = Join-Path $appFolder "$($env:username)_PSB_installation.id"
    }

    # create installation.id file if doesnt exist yet
    if(Test-Path $installationPath) {
        $value = Get-Content $installationPath
    }
    else {
        $value = ([GUID]::NewGuid()).ToString().ToUpper()
        Set-Content -Path $installationPath -Value $value
    }

    return $value
}

$MaxLengthPureName = 63

function Init-Logging() {
    $env:appFolder = GetApplicationFolder 

    $logAssemblyPath =  Join-Path $UtilsPath "log4net.dll"
    if((Test-Path variable:isLinux) -and ($isLinux -or $isMacOS)) {
        #mac or linux
        $logConfigPath = Join-Path $UtilsPath "log4netLinux.xml"
    }
    else{
        #windows
        $logConfigPath = Join-Path $UtilsPath "log4net.xml"
    }
     
    [Reflection.Assembly]::LoadFrom($logAssemblyPath) | Out-Null
    # Log4net configuration loading
    $log4netConfigFilePath = Resolve-Path $logConfigPath -ErrorAction SilentlyContinue -ErrorVariable Err
    if ($Err)
    {
        throw "Log4Net configuration file $logConfigPath cannot be found"
    }
    else
    {
        Write-Verbose "[New-Logger] Log4net configuration file is '$log4netConfigFilePath' "
        $FileInfo = New-Object System.IO.FileInfo($log4netConfigFilePath)
        [log4net.Config.XmlConfigurator]::Configure($FileInfo)
        $script:Logger = [log4net.LogManager]::GetLogger("root")
    }

    # Create installation id if doesn't exist
    Get-InstallationId | Out-Null
}

$UtilsPath = Split-Path -Parent $MyInvocation.MyCommand.Path -ErrorAction SilentlyContinue
Init-Logging
. $UtilsPath\lib.ps1

# Defines the minimum powershell version on remote sql server instance.
$script:min_ps_version = 5


function New-PureRestClient {
    Param (
        # FAEndpoint
        [Parameter(Mandatory = $true)]
        [string]
        $FAEndpoint,

        # FACredential
        [Parameter(Mandatory = $false)]
        [PSCredential]
        $FACredential,

        # rest Version
        [Parameter(Mandatory = $false)]
        [string]
        $RestVersion
    )

    $FunctionName = $MyInvocation.MyCommand
    Write-Log -Level INFO -FunctionName $FunctionName -Msg "Entering with parameter: $(Get-Parameter $MyInvocation)"

    # TODO: TMAN-18326 Set specific client for Metrics collection
    $ClientName = "PureStorage.FlashArray.Backup"
    $module = $MyInvocation.MyCommand.ScriptBlock.Module
    if ($module) {
      $Version = $module.Version
    } else {
        $Version = "0.0.0.0"
    }
    
    if(!$RestVersion){
        $MinRestVersion = [System.Version]"2.8"
        $MaxRestVersion = [System.Version]"2.16"
        $supportedVersions = Get-Pfa2ApiVersion -Endpoint $FAEndpoint -IgnoreCertificateError
        $latestSupportedVersion = $supportedVersions.ArraySupportedAPIVersion | where-object{-not ($_ -match 'dev' -or $_ -match '.X')} | Select-Object -last 1
        if([System.Version]($latestSupportedVersion) -lt $MinRestVersion){
            ThrowErrorCode -ErrorCode $ErrorCode_UnsupportedRestVersion
        }

        if([System.Version]($latestSupportedVersion) -gt $MaxRestVersion){
            $RestVersion = $MaxRestVersion.ToString()
        }
        else{
            $RestVersion = $latestSupportedVersion
        }

    }

    $RestClient = $null
    try{
        $RestClient = Connect-Pfa2Array -Endpoint $FAEndpoint -Credential $FACredential -IgnoreCertificateError -ApiVersion $RestVersion
    }
    catch{
        ThrowErrorCode -ErrorCode $ErrorCode_CantConnectToFA -innerException $_.Exception
    }

    if($RestClient)
    {
        # if was able to connect, also update the user agent to track backup sdk telemetry
        $success = $RestClient.UpdateUserAgent($ClientName, $Version );

        # this should not be a show stopper
        if(-not $success){
            Write-Log -Level WARN -FunctionName $FunctionName -Msg "Failed to set useragent"
        }
    }
    else{
        # if could not connect to FA throw error
        ThrowErrorCode -ErrorCode $ErrorCode_CantConnectToFA
    }

    return $RestClient
}


function Get-SingleVMOnly {
    [CmdletBinding()]
    Param (
        # VMName
        [Parameter(Position=0,mandatory=$true)]
        [AllowEmptyString()]
        [string]
        $Name,

        # VM object unique ID
        [Parameter(Position=1,mandatory=$false)]
        [AllowEmptyString()]
        [string]
        $VMPersistentId,

        [Parameter(Mandatory=$false)]
        [VMware.VimAutomation.ViCore.Types.V1.VIServer]
        $Server
    )

    # caller should supply VMName and/or VMPersistentId
    if ([string]::IsNullOrEmpty($Name) -and [string]::IsNullOrEmpty($VMPersistentId)) {
        ThrowErrorCode -ErrorCode $ErrorCode_MissingVMNameOrID
    }

    # NOTE: Error handling in Powershell is needlessly complex, mostly because they tried to make it simple.
    # In a simple test script, if Get-VM fails the error is recorded in $Error[]
    # This doesn't happen in a module though. In a Module, the SessionState has its own Error variable.
    # I don't know why, but "$Error" won't have the Get-Vm error, but "$script:Error" will.
    # In either case, -ErrorVariable will get the error message even if $Error[] doesn't.
    # So here we detect the error using -ErrorVariable.
    #
    $EV = $null 
    $GetVmArgs = @{}

    if ($null -ne $Server) {
        $GetVmArgs["Server"] = $Server
    }

    # Get-VM only allows either Name or ID.
    # If we have an ID, prefer it over the name
    if ([String]::IsNullOrEmpty($VMPersistentId)) {
        # Get-VM by name Only
        $VMs = @(Get-VM @GetVmArgs -Name $Name -ErrorVariable EV -ErrorAction 'SilentlyContinue')
    } elseif ([String]::IsNullOrEmpty($Name)) {
        # Get-VM by PersistentId Only
        $VMs = @(Get-VM @GetVmArgs -ErrorVariable EV -ErrorAction 'SilentlyContinue' | Where { ($_.PSObject.Properties['PersistentId']) -and ($_.PersistentId -eq $VMPersistentId) } )
    } else {
        # Get-VM by both Name and PersistentId
        $VMs = @(Get-VM @GetVmArgs -Name $Name -ErrorVariable EV -ErrorAction 'SilentlyContinue' | Where { ($_.PSObject.Properties['PersistentId']) -and ($_.PersistentId -eq $VMPersistentId) } )
        if ($VMs.Count -eq 0) {
            # No matching name, check for match by PersistentId only (VM was renamed)
            $VMs = @(Get-VM @GetVmArgs -ErrorVariable EV -ErrorAction 'SilentlyContinue' | Where { ($_.PSObject.Properties['PersistentId']) -and ($_.PersistentId -eq $VMPersistentId) } )
        }
    }

    if ($VMs.Count -eq 1) {
        return $VMs[0]
    }

    $VMNameString = Get-VmNameToString $Name $VMPersistentId

    if ($VMs.Count -eq 0) {
        if (($null -ne $EV) -and ($EV.Count -ne 0)) {
            throw $EV[0].Exception
        } else {
            ThrowErrorCode -ErrorCode $ErrorCode_VmMissingOnVcenter -params @($VMNameString, $Server.Name)
        }
    } else {
        # Too many, and no PersistentId
        ThrowErrorCode -ErrorCode $ErrorCode_TooManyVMMatchesOnVcenter -params @($VMNameString, $VMs.Count, $VMs.PersistentId)
    }
    return $null
}

function Connect-VCenter {
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $VCenterAddress,

        [Parameter(Mandatory = $true)]
        [PSCredential]
        $Credential
    )
    $FunctionName = $MyInvocation.MyCommand
    Write-Log -Level INFO -FunctionName $FunctionName -Msg "Entering with parameter: $(Get-Parameter $MyInvocation)"

    # TODO: assuming PowerCLI already installed
    if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) {
        ThrowErrorCode -ErrorCode $ErrorCode_PowerCLIMissing
    }
    # TODO: check version of PowerCLI already installed, what is the minimum version we want?
    try {
        Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -DefaultVIServerMode Multiple -Scope Session -Confirm:$false | Out-Null
        $VCenter = Connect-VIServer -Server $VCenterAddress -Credential $Credential -ErrorAction Stop
    } catch {
        Write-Log -Level ERROR -FunctionName $FunctionName -Msg $_.Exception.Message
        throw
    }
    return $VCenter
}

function Get-ESXiHostPorts {
    param (
        [Parameter(Mandatory = $true)]
        [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine]
        $VM
    )

    $FunctionName = $MyInvocation.MyCommand
    Write-Log -Level INFO -FunctionName $FunctionName -Msg "Entering with parameter: $(Get-Parameter $MyInvocation)"
  
    $ESXiHost = $VM | Get-VMHost
  
    $FCPorts = Get-VMHostHBA -VMHost $ESXiHost -Type FibreChannel | select PortWorldWideName
    Write-Log -Level INFO -FunctionName $FunctionName -Msg "Discovered FC Ports: $($FCPorts | Out-String)"
    $HexFCPorts = @($FCPorts | % { "{0:x}" -f $_.PortWorldWideName })
    Write-Log -Level INFO -FunctionName $FunctionName -Msg "FC Ports formatted: $($HexFCPorts | Out-String)"
    $iqns = @(Get-VMHostHBA -Type iSCSI -VMHost $ESXiHost | % { $_.IscsiName } )
    Write-Log -Level INFO -FunctionName $FunctionName -Msg "Discovered iSCSI Ports: $($iqns)"
    $allPorts = $HexFCPorts + $iqns
  
    return $allPorts
}

function Get-Parameter()
{
    Param(
        [Parameter(Mandatory=$False)] $caller
    )

    $params= ($caller.BoundParameters.Keys | foreach { "-$_ '$($caller.BoundParameters[$_])'" }) -join " "

    return $params
}

function Get-VmNameToString
{
    param($VMName, $VMPersistentId)

    if ([string]::IsNullOrEmpty($VMName)) {
        return "PersistentId:$VMPersistentId"
    }
    if ([string]::IsNullOrEmpty($VMPersistentId)) {
        return "$VMName"
    }
    #return "$($VMName):$($VMPersistentId)"
    #return "$($VMName) ($($VMPersistentId))"
    return "$($VMName) PersistentId:($($VMPersistentId))"
}


function ValidatePureObjectName {
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [parameter(Mandatory = $false)]
        [int]
        $MaxLength
    )

    # default max length for pure object names is 63 chars
    if(-not $maxLength) {
        $maxLength = $MaxLengthPureName
    }

    # check length
    if(($Name.Length -gt $maxLength) -or ($Name -notmatch '[a-zA-Z\-]') -or ($Name -match '[^a-zA-Z0-9\-]')){
        return $false
    }

    return $true
}

function FormatVolumeType {
    Param (
        [string]
        $VolumeType
    )

    if ("VVOL" -ieq $VolumeType) {
        return "vVol"
    }

    if ("Physical" -ieq $VolumeType) {
        return "Physical"
    }

    if ("RDM" -ieq $VolumeType) {
        return "RDM"
    }
}

function ValidateVolumeType {
    Param (
        [parameter(Mandatory = $false)]
        [string]
        $VCenterAddress,

        [parameter(Mandatory = $true)]
        [ValidateSet("Physical", "RDM", "VVOL", "")]
        [string]
        $VolumeType
    )

    # Case insensitivie for user input
    if ([String]::IsNullOrEmpty($VCenterAddress) -and ($VolumeType -ne "Physical")) {
        ThrowErrorCode -ErrorCode $ErrorCode_NoVcenterSpecified
    }
    if (-not [String]::IsNullOrEmpty($VCenterAddress) -and $VolumeType -eq "Physical") {
        ThrowErrorCode -ErrorCode $ErrorCode_vcenterForPhysical
    }

    $FormattedVolumeType = FormatVolumeType -VolumeType $VolumeType
    return $FormattedVolumeType
}

function ValidatePath {
    Param(
        [Parameter(Mandatory = $true)]
        [string[]] $Paths,

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

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.Runspaces.PSSession] $ComputerSession,

        [Parameter(Mandatory = $true)]
        [int] $DiskCount
    )

    if($Paths.Count -ne $DiskCount) {
        ThrowErrorCode -ErrorCode $ErrorCode_PathCountMismatch -params @($Paths.Count,$DiskCount)
    }

    $ValidatedPaths = @()

    # Verify $Path is a valid parameter

    foreach ($Path in $Paths) {
        if($Path -like '*#*'){
            ThrowErrorCode -ErrorCode $ErrorCode_BadDelimeter -params @("Path", $Path )
        }
        
        $Length = $Path.Length
        # If mount to a new drive letter
        if ($Length -lt 2) {
            ThrowErrorCode -ErrorCode $ErrorCode_InvalidPath -params @($Path)
        }
        elseif ($Length -eq 2) {
            if (":" -ne $Path[1] -or "A", "B", "C" -Contains $Path[0]) {
                ThrowErrorCode -ErrorCode $ErrorCode_InvalidPath -params @($Path)
            }
            $AvailableDrives = Get-PSBAvailableDrive -ComputerAddress $ComputerAddress -ComputerSession $ComputerSession
            if (-not ($AvailableDrives -Contains $Path)) {
                ThrowErrorCode -ErrorCode $ErrorCode_UnavailablePath -params @($Path)
            }
            $Path = $Path[0]
        }
        # If mount to a mount point
        else {
            # Normalize path
            $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
            SafeInvokeRemote -session $ComputerSession -ArgumentList $Path -ScriptBlock {
                Param (
                    $Path
                    )
                if ($False -eq (Test-Path -Path $Path)) {
                    ThrowErrorCode -ErrorCode $ErrorCode_MPMissing -params @($Path)
                }
                # the mount point should be empty
                else {
                    $ResolvedPath = (Resolve-Path -Path $Path).Path
                    if ($ResolvedPath[$ResolvedPath.Length -1] -ne '\') {
                        $ResolvedPath = $ResolvedPath + '\'
                    }
                    $WmiVol = Get-WmiObject win32_volume | Where-Object {$_.Name -eq $ResolvedPath}
                    if ($WmiVol) {
                        ThrowErrorCode -ErrorCode $ErrorCode_UnavailablePath -params @($Path)
                    }
                }
            }
        }

        $ValidatedPaths += $Path
    }

    return $ValidatedPaths
}

function NewVolumeName {
    Param(
        $sourceId,
        $uniqueId
    )
    # 2 is to accound for the two '-' added in the name
    $remainingLength = $MaxLengthPureName - ($uniqueId.Length + $SDKPrefix.Length + 2)
    if($remainingLength -lt $sourceId.Length){
        ThrowErrorCode -ErrorCode $ErrorCode_VolNameTooLong
    }
    return "$($SDKPrefix)-$($sourceId)-$($uniqueId)"
}

################# HELPER FUNCTIONS #################
function RetrievePgroupSnapshots{
    Param(
        [parameter(Mandatory = $true)]
        $RestClient,

        [parameter(Mandatory = $false)]
        [string]
        $VolumeSetName,

        [parameter(Mandatory=$false)]
        [switch]
        $IncludeNonSDKSnapshots,

        [parameter(Mandatory=$false)]
        [switch]
        $UseLocalTime,

        [parameter(Mandatory = $false)]
        [int]
        $Limit
    )
    $history = @()
    
    $offset = 0
    while($history.Count -lt $Limit) {

        # gets all pgroup snapshots on FA (with pagination starting from most recent)
        $pgroupSnaps = Get-Pfa2ProtectionGroupSnapshot -Array $RestClient -Sort 'created-' -Offset $offset -Limit $RestLimit -Destroyed:$false #### REST CALL ####
        if($pgroupSnaps.Count -eq 0) {
            # went through all snaps
            break
        }

        # process each pgroup snap
        foreach ($pgroupSnap in $pgroupSnaps){
            # gets volume snapshots for the pgroup snapshot
            $pgroupVolumeSnaps = Get-Pfa2VolumeSnapshot -Array $RestClient -Filter "Name=""$($pgroupSnap.Name).*""" -Destroyed:$false #### REST CALL ####

            # gets metadata for volume snapshot
            # if user passed a volume set name, get metadata for that volume set only
            if ($VolumeSetName) {
                $metadata = Get-Pfa2VolumeSnapshotTags -Array $RestClient -Namespaces $NamespaceVolSet -ResourceIds $pgroupVolumeSnaps.Id -Filter "Key=""$VolumeSetName""" #### REST CALL ####
            }
            else {
                $metadata = Get-Pfa2VolumeSnapshotTags -Array $RestClient -Namespaces $NamespaceVolSet -ResourceIds $pgroupVolumeSnaps.Id #### REST CALL ####
            }

            # no volume set matches this snapshot, skip it
            if($metadata.Count -eq 0) {
                continue
            }

            # gets history id info for snapshots
            $HistoryIds = Get-Pfa2VolumeSnapshotTags -Array $RestClient -Namespaces $NamespaceMeta -Filter "Key=""$([PureTagKeys]::HistoryId)""" -ResourceIds $pgroupVolumeSnaps.Id #### REST CALL ####

            # a snapshot can be part of multiple volume sets, group snapshots by volume set and process each group
            $groupedEnvTags = $metadata | group-object -Property {$_.Key}
            foreach ($volsetTags in $groupedEnvTags) {
                # load metadata info into a volume set obj
                try{
                    $volSet = @(ParseMetadataTags -Tags $volsetTags.Group)
                }
                catch {
                    # either could not parse environment tags, or volume set was not complete
                    continue
                }

                # get snapshot objects for volume snapshots included on the volume set
                $volumeSetSnaps = $pgroupVolumeSnaps | where-object { $_.Id -in $volset.Resources.Id }
                if($volumeSetSnaps.Count -eq 0) {
                    # no matching snaps, this shouldn't happen, but just in case..
                    continue
                }

                $isSDKSnap = $false

                # get history ID for volume set snapshots
                $VolumeSetHistoryIds = $HistoryIds | Where-Object {$_.Resource.Id -in $volumeSetSnaps.Id}
                if ($VolumeSetHistoryIds.Count -eq $volumeSetSnaps.Count){
                    # if snapshot was taken by the SDK, each snapshot will be tagged with history id
                    $HistoryId = $VolumeSetHistoryIds.Value | Get-Unique

                    # if the exact same set of volumes is part of 2 volume sets, the second set will be a match
                    # but it should not have the same history Id as it won't be possible to mount it
                    # we will handle it a non-sdk snap, as the snap was not taken for this volume set, and this way it will get a different ID
                    $isSDKSnap = $HistoryId.StartsWith("$($volsetTags.Name)#")
                }

                if(-not $isSDKSnap) {
                    # if snapshot was not taken by SDK, generate history id using pgroup snapshot name
                    $HistoryId = "$($volsetTags.Name)#$($pgroupSnap.Name)"
                }

                # only include non-sdk snapshots if user selects -IncludeNonSDKSnapshots
                if ($IncludeNonSDKSnapshots -or $isSDKSnap){
                    # generate history object
                    $history += New-HistoryObject -HistoryId $HistoryId  -VolumeSet $volset -Snapshots @($volumeSetSnaps.Name) -CreationDate $pgroupSnap.Created -PgroupName $pgroupSnap.Source.Name -localtime:$UseLocalTime -SDKSnap $isSDKSnap
                }
            }

        }
        # update offset to get remaining snapshots
        $offset += $pgroupSnaps.Count
    }
    return $history
}

function RetrieveNonPgroupSnapshots{
    Param(
        [parameter(Mandatory = $true)]
        $RestClient,

        [parameter(Mandatory = $false)]
        [string]
        $VolumeSetName,

        [parameter(Mandatory=$false)]
        [switch]
        $UseLocalTime,

        [parameter(Mandatory = $false)]
        [int]
        $Limit
    )
    $history = @()
    
    # check for volume snapshots tagged with nopgroup tag
    $noPgroupSnapTags = Get-Pfa2VolumeSnapshotTags -Array $RestClient -Namespaces $NamespaceMeta  -ResourceDestroyed:$false -Filter "Key=""$([PureTagKeys]::SnapType)"" and Value=""NoPgroup""" #### REST CALL ####

    if(-not $noPgroupSnapTags -or $noPgroupSnapTags.Count -eq 0) {
        # all volume snapshots taken by the sdk will have the "NoPgroup" tag
        # if none was found we are done
        return $history
    }
    
    # TODO: handle scale?
    # get snapshot objects for all volume snapshots tagged as nopgroup
    $volumeSnaphots = Get-Pfa2VolumeSnapshot -Array $RestClient -Ids $noPgroupSnapTags.Resource.Id -Destroyed:$false #### REST CALL ####
    
    # gets metadata for volume snapshots
    # if user passed a volume set name, get metadata for that volume set only
    if ($VolumeSetName) {
        $allMetadata = Get-Pfa2VolumeSnapshotTags -Array $RestClient -Namespaces $NamespaceVolSet -ResourceIds $volumeSnaphots.Id -Filter "Key=""$VolumeSetName""" #### REST CALL ####
    }
    else {
        $allMetadata = Get-Pfa2VolumeSnapshotTags -Array $RestClient -Namespaces $NamespaceVolSet -ResourceIds $volumeSnaphots.Id #### REST CALL ####
    }
    
    # get history ID for volume snapshots
    $HistoryIds = Get-Pfa2VolumeSnapshotTags -Array $RestClient -Namespaces $NamespaceMeta -Filter "Key=""$([PureTagKeys]::HistoryId)""" -ResourceIds $volumeSnaphots.Id | Group-Object -Property {$_.Value} #### REST CALL ####

    # all snapshots taken on the same backup should have the same history Id
    # group snapshots by history id and process each group
    foreach ($group in $HistoryIds){
        # historyId should have the format VolumeSet#HistoryId
        $parts = $group.Name -Split '#'
        if ($parts.Count -ne 2) {
            continue
        }

        # get volume set name for this history entry
        $histSet = $parts[0]
        # if volume set was selected by user, filter out other volume set snapshots
        if($VolumeSetName -and $VolumeSetName -ne $histSet){
            continue
        }

        # gets all volume snapshots Ids for this history entry
        $historySnapsIds = $group.Group.Resource.Id

        # get metadata for history entry
        $historyMetadata = $allMetadata | where-object {$_.Resource.Id -in $historySnapsIds -and $_.Key -eq $histSet}
        if($historyMetadata.Count -eq 0) {
            continue
        }

        # load metadata into volset object
        try{
            $volSet = @(ParseMetadataTags -Tags $historyMetadata)
        }
        catch {
            # either could not parse environment tags, or volume set was not complete
            continue
        }

        # gets snapshot objects for history entry
        $historySnaps = $volumeSnaphots | where-object{ $_.Id -in $historySnapsIds }

        if($historySnaps.Count -ne $volSet.Resources.Count){
            # incomplete set of snapshots
            continue
        }

        # create history entry onject
        $history += New-HistoryObject -HistoryId $group.Name -VolumeSet $volSet -Snapshots @($historySnaps.Name) -CreationDate $historySnaps[0].Created -PgroupName $null -localtime:$UseLocalTime -SDKSnap $true
    }
    return $history
}

function ProcessPgroupOption {
    Param (
        [parameter(Mandatory = $true)]
        $RestClient,
        
        [parameter(Mandatory = $true)]
        $affectedVolumes,

        [parameter(Mandatory = $false)]
        [string]
        $PgroupName,

        [parameter(Mandatory = $false)]
        [switch]
        $UseBestPgroupMatch,

        [parameter(Mandatory = $false)]
        [switch]
        $CreatePgroup,

        [parameter(Mandatory = $false)]
        [string]
        $NewPgroupSuffix
    )

    # The rest api 2.8 and bellow did not return volume id when getting
    # the volumes of a pgroup, nor let us add volumes by id to a pgroup
    $idsSupported = ([System.Version]$RestClient.ApiVersion) -gt ([System.Version]'2.8')

    ### Use existing PG
    if($PgroupName){
        # check pgroup exists
        $pg = Get-Pfa2ProtectionGroup -Array $RestClient -Name $PgroupName -ErrorAction SilentlyContinue
        if(-not $pg){
            ThrowErrorCode -ErrorCode $ErrorCode_PGMissing -params @( $PgroupName)
        }

        # Should throw exception if PG doesn't exist, but does it?
        $vols = Get-PgroupVolumes -RestClient $RestClient -PgroupName $PgroupName

        # check if all volumes are included in Pg
        $missingVols = @()
        foreach($vol in $affectedVolumes) {
            if (($idsSupported -and ($vol.Id -notin $vols.Id)) -or (-not $idsSupported -and ($vol.Name -notin $vols.Name))) {
                $missingVols += $vol.Name
            }
        }

        #reports missing volumes
        if ($missingVols.Count -gt 0) {
            ThrowErrorCode -ErrorCode $ErrorCode_PgMissingVolumes -params @( $PgroupName, ($missingVols -join ','))
        }
    }

    ### Create a new PG
    if($CreatePgroup){
        # check if pgroup name is valid
        $NewPgroupName = "$($script:SDKPrefix)-$($NewPgroupSuffix)"
        if (-not (ValidatePureObjectName -Name $NewPgroupName)) {
            ThrowErrorCode -ErrorCode $ErrorCode_PurityNameRequirements -params @("NewPgroupName",$NewPgroupName)
        }

        # get pod prefix if any
        $podPrefix = Get-PodPrefix -volumeNames @($affectedVolumes.Name)
        $NewPgroupName = "$($podPrefix)$($NewPgroupName)"

        # check if pgroup already exists
        $pg = Get-Pfa2ProtectionGroup -Array $RestClient -Name $NewPgroupName -ErrorAction SilentlyContinue
        if ($pg) {
            ThrowErrorCode -ErrorCode $ErrorCode_PGAlreadyExists -params @($NewPgroupName)
        }

        # if pgroup does not exist create it
        New-Pfa2ProtectionGroup -Array $RestClient -Name $NewPgroupName -ErrorAction Stop | Out-Null
        if($idsSupported){
            New-Pfa2ProtectionGroupVolume -Array $RestClient -GroupNames $NewPgroupName -MemberIds $affectedVolumes.id -ErrorAction Stop | Out-Null
        }
        else {
            New-Pfa2ProtectionGroupVolume -Array $RestClient -GroupNames $NewPgroupName -MemberNames $affectedVolumes.Name -ErrorAction Stop | Out-Null
        }

        # set retention policy
        Update-Pfa2ProtectionGroup -Array $RestClient -Name $NewPgroupName -SourceRetentionAllForSec $RetentionPolicySecs -SourceRetentionPerDay $RetentionPolicySnapsPerDay -SourceRetentionDays $RetentionPolicyDaysToKeep -ErrorAction Stop | Out-Null
        $PgroupName = $NewPgroupName
    }

    ### selects best match of existing PGs
    if($UseBestPgroupMatch) {
        $matchingPgroups = Get-MatchingPgroups -RestClient $RestClient -VolumesNames $affectedVolumes.Name

        if($matchingPgroups.Count -eq 0) {
            ThrowErrorCode -ErrorCode $ErrorCode_NoPGForVolset
        }
        $PgroupName = ($matchingPgroups.Name | Select-object -First 1)
    }

    return $PgroupName
}

# Parse a list of volume names and return the pod prefix if any
# If all volumes have the same pod prefix, return the pod prefix ("podname::"
# If no volumes have a pod prefix, return an empty string
# If some volumes have a pod prefix and some don't, throw an error
# If the pod prefix is not the same for all volumes, throw an error
function Get-PodPrefix {
    Param(
        $volumeNames
    )
    $FunctionName = $MyInvocation.MyCommand
    Write-Log -Level INFO -FunctionName $FunctionName -Msg "Entering with parameter: $(Get-Parameter $MyInvocation)"

    $podVolumes = @($volumeNames | where-object {$_ -Match "::"})

    if($podVolumes.Count -eq 0){
        # no volumes have a pod prefix
        $podPrefix = ""
    }
    elseif($volumeNames.Count -eq $podVolumes.Count){
        $podPrefix = ($podVolumes[0] -split '::')[0]

        # check all volumes have the same pod prefix
        foreach($vol in $podVolumes){
            if($podPrefix -ne ($vol -split '::')[0]){
                # some volumes have different pod prefixes
                ThrowErrorCode -ErrorCode $ErrorCode_PodPrefixMismatch
            }
        }

        # all volumes have the same pod prefix
        $podPrefix = "$podPrefix::"
    }
    else {
        # some volumes have a pod prefix and some don't
        ThrowErrorCode -ErrorCode $ErrorCode_PodPrefixMismatch
    }

    Write-Log -Level INFO -FunctionName $FunctionName -Msg "Exiting with result: $($podPrefix)"
    return $podPrefix
}

function New-HistoryObject {
    Param (
        $HistoryId,
        $VolumeSet,
        $Snapshots,
        $CreationDate,
        $PgroupName,
        $SDKSnap,
        [parameter(Mandatory=$false)]
        [switch]
        $localTime
    )

    # convert datetime from utc to local time
    $CreationDate = Get-Date $CreationDate
    if($localTime){
        $CreationDate = $CreationDate.ToLocalTime()
    }

    $objectProperty = [ordered]@{
        HistoryId       = $HistoryId
        VolumeSetName   = $VolumeSet.Name
        Computer        = $VolumeSet.Computer
        VolumeType      = $VolumeSet.Volumetype
        Paths           = $VolumeSet.Paths
        vCenter         = $VolumeSet.vCenter
        VMId            = $VolumeSet.VMID
        CreationDate    = $CreationDate
        ProtectionGroup = $PgroupName
        SDKSnap         = $SDKSnap
        Snapshots       = $Snapshots
    }

    $pgObject = New-Object -TypeName psobject -Property $objectProperty
    return $pgObject
}

function Get-HistoryObject {
    Param (
        $RestClient,
        $HistoryId
    )

    # historyId has the format "volsetName#guid" and we want to extract the volsetName
    $parts = $HistoryId -Split '#'
    if($parts.Count -ne 2) {
        ThrowErrorCode -ErrorCode $ErrorCode_InvalidHistory
    }
    $volsetName = $parts[0]

    #### REST CALL ####
    $HistoryEntries = Get-Pfa2VolumeSnapshotTags -Array $RestClient -Namespaces $NamespaceMeta -Filter "Key=""$([PureTagKeys]::HistoryId)"" and Value=""$($HistoryId)"""
    if ($HistoryEntries.Count -gt 0){
        # it's a SDK Snap
        #### REST CALL ####
        $EnvTags = Get-Pfa2VolumeSnapshotTags -Array $RestClient -Namespaces $NamespaceVolSet -Filter "Key=""$($volsetName)""" -ResourceIds $HistoryEntries.Resource.Id
        $volSet = ParseMetadataTags -Tags $EnvTags
        #### REST CALL ####
        $snaps = @(Get-Pfa2VolumeSnapshot -Array $RestClient -Ids $HistoryEntries.Resource.Id -Destroyed:$false) 
        $parts = $snaps[0].Name.Split(".")
        if($parts.Count -eq 3){
            $pgroupName = $parts[0]
        }
        else{
            $pgroupName = $null
        }

        return New-HistoryObject -HistoryId $HistoryId -VolumeSet $volSet -Snapshots @($snaps.Name) -CreationDate $snaps[0].Created -PgroupName $pgroupName -SDKSnap $true
    }
    else {
        $pgSnapName = $parts[1]
        $pgSnap = Get-Pfa2ProtectionGroupSnapshot -Array $RestClient -Filter "Name=""$($pgSnapName)""" -Destroyed:$false
        if ($pgSnap) {
            #### REST CALL ####
            $volSnaps = Get-Pfa2VolumeSnapshot -Array $RestClient -Filter "Name=""$($pgSnap.Name).*""" -Destroyed:$false 

            #### REST CALL ####
            $pgSnapEnv = Get-Pfa2VolumeSnapshotTags -Array $RestClient -Namespaces $NamespaceVolSet -ResourceIds $volSnaps.Id -Filter "Key=""$volsetName"""

            $volSet = @(ParseMetadataTags -Tags $pgSnapEnv)
            $snaps = $volSnaps | where-object { $_.Id -in $volSet.Resources.Id }

            return New-HistoryObject -HistoryId $HistoryId  -VolumeSet $volSet -Snapshots @($snaps.Name) -CreationDate $pgSnap.Created -PgroupName $pgSnap.Source.Name -SDKSnap $false
        }
    }

}

function New-MountObject {
    Param (
        $MountId,
        $HistoryId,
        $Volumetype,
        $Computer,
        $Paths, 
        $vCenter,
        $VMId,
        $Resources
    )
    $objectProperty = [ordered]@{
        MountId     = $MountId
        HistoryId   = $HistoryId
        Computer    = $Computer
        Volumetype  = $Volumetype
        Paths       = $Paths
        vCenter     = $vCenter
        VMId        = $VMId
        Resources     = $Resources
    }

    $pgObject = New-Object -TypeName psobject -Property $objectProperty
    return $pgObject
}

function Get-MountObject {
    Param (
        $RestClient,
        $MountId
    )

    # get mount id tags by mount ID
    $MountIds = Get-Pfa2VolumeTag -Array $RestClient -Namespaces $NamespaceMount -Filter "key='$([PureTagKeys]::MountId)'" -ResourceDestroyed:$false | Where-Object {$_.Value.StartsWith("$($MountId)#")} #### REST CALL ####
    if(-not $MountIds -or $MountIds.Count -eq 0) {
        ThrowErrorCode -ErrorCode $ErrorCode_InvalidMount -params @($MountId)
    }

    # id format: mountId#volset#hitoryId
    $idParts = ($MountIds.Value | Sort-Object | Get-Unique ) -Split '#'
    $HistoryId = "$($idParts[1])#$($idParts[2])"

    # gets metadata for mounted volumes
    $metadata = Get-Pfa2VolumeTag -Array $RestClient -Namespaces $NamespaceMount -Filter "key='$([PureTagKeys]::Metadata)'" -ResourceIds $MountIds.Resource.Id #### REST CALL ####

    # load metadata into volset object
    try{
        $volSet = @(ParseMetadataTags -Tags $metadata)
    }
    catch {
        # either could not parse environment tags, or volume set was not complete
        ThrowErrorCode -ErrorCode $ErrorCode_CantParseMountTag -params @($MountId,$_)
    }

    # create mount history object
    return New-MountObject -MountId $MountId -HistoryId $HistoryId -Volumetype $volSet.VolumeType -Computer $volSet.Computer -Paths $volSet.Paths -vCenter $volSet.vCenter -VMId $volSet.VMId -Resources $volSet.Resources
}

function Get-VirtualizationInfo {
    Param(
        $HistoryItem,
        $VCenterAddress,
        [PSCredential] $VCenterCredential,
        $Computer,
        $VMName,
        $VMPersistentId
    )

    # If $VCenterAddress is not provided, assume the same VCenter as the backup history.
    if ([string]::IsNullOrWhiteSpace($VCenterAddress)) {
        $VCenterAddress = $HistoryItem.VCenter
    }

    if ([string]::IsNullOrWhiteSpace($Computer)) {
        $Computer = $HistoryItem.Computer
    }

    if ([string]::IsNullOrWhiteSpace($VMName) -and [string]::IsNullOrWhiteSpace($VMPersistentId)) {
        if ($VCenterAddress -and [string]::IsNullOrWhiteSpace($HistoryItem.VMId)) {
            ThrowErrorCode -ErrorCode $ErrorCode_MissingVMNameOrID
        }
        $VMPersistentId = $HistoryItem.VMId
    }

    $MountVM = $null
    $hostports = $null
    if ($VCenterAddress) {
        $EsxiDetails = Get-ESXiDetails -VCenterAddress $VCenterAddress -VCenterCredential $VCenterCredential -VMName $VMName -VMPersistentId $VMPersistentId
        $hostports = $EsxiDetails.EsxiPorts -Split ','
        $MountVM = $EsxiDetails.VM
    }

    if ("VVOL" -ieq $HistoryItem.VolumeType -and -not $MountVM) {
        $Server = Connect-VCenter -VCenterAddress $VCenterInfo.Address -Credential $VCenterCredential
        $MountVM = Get-SingleVMOnly -Name $VMName -VMPersistentId $VMPersistentId -Server $Server
    }

    return $VCenterAddress, $Computer, $MountVM, $hostports
}

function CheckMountedVolumeTags {
    Param (
        [string] $ComputerAddress,
        [string[]] $Paths,
        [string] $VCenterAddress,
        [string] $VMPersistentId,
        [string] $VolumeType
    )

    # verify that values do not contain delimiter
    $Path = $Paths -Join ','
    if($Path                -like '*#*') { ThrowErrorCode -ErrorCode $ErrorCode_BadDelimeter -params @( "Path",$Path )}
    if($ComputerAddress       -like '*#*') { ThrowErrorCode -ErrorCode $ErrorCode_BadDelimeter -params @( "Computer Address",$ComputerAddress )}
    if($VCenterAddress    -like '*#*') { ThrowErrorCode -ErrorCode $ErrorCode_BadDelimeter -params @( "VCenter Address",$VCenterAddress )}
    if($VMPersistentId -like '*#*') { ThrowErrorCode -ErrorCode $ErrorCode_BadDelimeter -params @( "VM Persistent Id",$VMPersistentId )}

    for( $i =0; $i -lt $Paths.Count; $i++) {
        # get drive letter and serial of volume to be tagged
        $diskPath = $Paths[$i]
        # creates metadata string
        $metadata = "$($Paths.Count)#$($VolumeType)#$($ComputerAddress)#$($diskPath)#$($VCenterAddress)#$($VMPersistentId)"
        if ($metadata.Length -gt 256) {
            # note: tag values can have up to 256 character
            # VolCount: 1 to 3 chars
            # VolType: 3 or 8 chars
            # VMPersistentId: 36
            # Delimiters: 5 chars
            # Remaining: 204Chars / 3 = ~68 chars per field (ComputerAddress, diskPath, VCenterAddress)
            ThrowErrorCode -ErrorCode $ErrorCode_EnvTagTooLong -params @($metadata)
        }
    }
}

function New-MountedVolumesTags {
    Param (
        [string] $MountId, 
        [string] $ComputerAddress,
        [string[]] $Paths,
        [string] $VCenterAddress,
        [string] $VMPersistentId,
        [string[]] $MountVolumesSerials,
        [string] $VolumeType,
        $RestClient
    )

    # verify that values do not contain delimiter
    CheckMountedVolumeTags -ComputerAddress $ComputerAddress -Paths $Paths -VCenterAddress $VCenterAddress -VMPersistentId $VMPersistentId -VolumeType $VolumeType 

    $volumes = @()
    for( $i =0; $i -lt $MountVolumesSerials.Count; $i++) {
        # get drive letter and serial of volume to be tagged
        $diskPath = $Paths[$i]

        # get volume object to be tagged
        $diskSerial = $MountVolumesSerials[$i]
        $volume = Get-Pfa2Volume -Array $RestClient -Filter "Serial=""$diskSerial"""
        
        # update volumes list
        $volumes += @{
            Id = $volume.Id
            Name = $volume.Name
        }

        # tag volume with Mount Id [workflowid#historyId]
        $idTag = Set-Pfa2VolumeTagBatch -Array $RestClient -ResourceIds $volume.Id -TagCopyable $false -TagNamespace $NamespaceMount -TagKey "$([PureTagKeys]::MountId)" -TagValue $MountId 
        if (-not $idTag) { ThrowErrorCode -ErrorCode $ErrorCode_FailToSetMountTag }

        # creates metadata string
        $metadata = "$($MountVolumesSerials.Count)#$($VolumeType)#$($ComputerAddress)#$($diskPath)#$($VCenterAddress)#$($VMPersistentId)"
        if ($metadata.Length -gt 256) {
            # note: tag values can have up to 256 character
            # VolCount: 1 to 3 chars
            # VolType: 3 or 8 chars
            # VMPersistentId: 36
            # Delimiters: 5 chars
            # Remaining: 204Chars / 3 = ~68 chars per field (ComputerAddress, diskPath, VCenterAddress)
            ThrowErrorCode -ErrorCode $ErrorCode_EnvTagTooLong -params @($metadata)
        }

        # tags volume with metadata
        $metaTag = Set-Pfa2VolumeTagBatch -Array $RestClient -ResourceIds $volume.Id -TagCopyable $false -TagNamespace $NamespaceMount -TagKey "$([PureTagKeys]::Metadata)" -TagValue $metadata 
        if (-not $metaTag) { ThrowErrorCode -ErrorCode $ErrorCode_FailToSetVolumeMetaTag -params @($metadata,$volume.Name) }
    }

    return $volumes
}

# SIG # Begin signature block
# MIIn+AYJKoZIhvcNAQcCoIIn6TCCJ+UCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUbPRFYSdXAB8Fb+06rW7dXBxq
# 3fqggiEoMIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG9w0B
# AQwFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk
# IElEIFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBiMQsw
# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
# ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw
# ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz
# 7MKnJS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS
# 5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7
# bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfI
# SKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jH
# trHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14
# Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2
# h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt
# 6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPR
# iQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ER
# ElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4K
# Jpn15GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB/zAd
# BgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReuir/SS
# y4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0wazAk
# BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAC
# hjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURS
# b290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAGBgRV
# HSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9mqyh
# hyzshV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxSA8hO
# 0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/6Fmo
# 8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSMb++h
# UD38dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt9H5x
# aiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMIIGrjCCBJag
# AwIBAgIQBzY3tyRUfNhHrP0oZipeWzANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQG
# EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
# cnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjIw
# MzIzMDAwMDAwWhcNMzcwMzIyMjM1OTU5WjBjMQswCQYDVQQGEwJVUzEXMBUGA1UE
# ChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQg
# UlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAxoY1BkmzwT1ySVFVxyUDxPKRN6mXUaHW0oPRnkyibaCw
# zIP5WvYRoUQVQl+kiPNo+n3znIkLf50fng8zH1ATCyZzlm34V6gCff1DtITaEfFz
# sbPuK4CEiiIY3+vaPcQXf6sZKz5C3GeO6lE98NZW1OcoLevTsbV15x8GZY2UKdPZ
# 7Gnf2ZCHRgB720RBidx8ald68Dd5n12sy+iEZLRS8nZH92GDGd1ftFQLIWhuNyG7
# QKxfst5Kfc71ORJn7w6lY2zkpsUdzTYNXNXmG6jBZHRAp8ByxbpOH7G1WE15/teP
# c5OsLDnipUjW8LAxE6lXKZYnLvWHpo9OdhVVJnCYJn+gGkcgQ+NDY4B7dW4nJZCY
# OjgRs/b2nuY7W+yB3iIU2YIqx5K/oN7jPqJz+ucfWmyU8lKVEStYdEAoq3NDzt9K
# oRxrOMUp88qqlnNCaJ+2RrOdOqPVA+C/8KI8ykLcGEh/FDTP0kyr75s9/g64ZCr6
# dSgkQe1CvwWcZklSUPRR8zZJTYsg0ixXNXkrqPNFYLwjjVj33GHek/45wPmyMKVM
# 1+mYSlg+0wOI/rOP015LdhJRk8mMDDtbiiKowSYI+RQQEgN9XyO7ZONj4KbhPvbC
# dLI/Hgl27KtdRnXiYKNYCQEoAA6EVO7O6V3IXjASvUaetdN2udIOa5kM0jO0zbEC
# AwEAAaOCAV0wggFZMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLoW2W1N
# hS9zKXaaL3WMaiCPnshvMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P
# MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcB
# AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr
# BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAI
# BgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQB9WY7Ak7Zv
# mKlEIgF+ZtbYIULhsBguEE0TzzBTzr8Y+8dQXeJLKftwig2qKWn8acHPHQfpPmDI
# 2AvlXFvXbYf6hCAlNDFnzbYSlm/EUExiHQwIgqgWvalWzxVzjQEiJc6VaT9Hd/ty
# dBTX/6tPiix6q4XNQ1/tYLaqT5Fmniye4Iqs5f2MvGQmh2ySvZ180HAKfO+ovHVP
# ulr3qRCyXen/KFSJ8NWKcXZl2szwcqMj+sAngkSumScbqyQeJsG33irr9p6xeZmB
# o1aGqwpFyd/EjaDnmPv7pp1yr8THwcFqcdnGE4AJxLafzYeHJLtPo0m5d2aR8XKc
# 6UsCUqc3fpNTrDsdCEkPlM05et3/JWOZJyw9P2un8WbDQc1PtkCbISFA0LcTJM3c
# HXg65J6t5TRxktcma+Q4c6umAU+9Pzt4rUyt+8SVe+0KXzM5h0F4ejjpnOHdI/0d
# KNPH+ejxmF/7K9h+8kaddSweJywm228Vex4Ziza4k9Tm8heZWcpw8De/mADfIBZP
# J/tgZxahZrrdVcA6KYawmKAr7ZVBtzrVFZgxtGIJDwq9gdkT/r+k0fNX2bwE+oLe
# Mt8EifAAzV3C+dAjfwAL5HYCJtnwZXZCpimHCUcr5n8apIUP/JiW9lVUKx+A+sDy
# Divl1vupL0QVSucTDh3bNzgaoSv27dZ8/DCCBrAwggSYoAMCAQICEAitQLJg0pxM
# n17Nqb2TrtkwDQYJKoZIhvcNAQEMBQAwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoT
# DERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UE
# AxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4XDTIxMDQyOTAwMDAwMFoXDTM2
# MDQyODIzNTk1OVowaTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
# bmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBS
# U0E0MDk2IFNIQTM4NCAyMDIxIENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
# AgoCggIBANW0L0LQKK14t13VOVkbsYhC9TOM6z2Bl3DFu8SFJjCfpI5o2Fz16zQk
# B+FLT9N4Q/QX1x7a+dLVZxpSTw6hV/yImcGRzIEDPk1wJGSzjeIIfTR9TIBXEmtD
# mpnyxTsf8u/LR1oTpkyzASAl8xDTi7L7CPCK4J0JwGWn+piASTWHPVEZ6JAheEUu
# oZ8s4RjCGszF7pNJcEIyj/vG6hzzZWiRok1MghFIUmjeEL0UV13oGBNlxX+yT4Us
# SKRWhDXW+S6cqgAV0Tf+GgaUwnzI6hsy5srC9KejAw50pa85tqtgEuPo1rn3MeHc
# reQYoNjBI0dHs6EPbqOrbZgGgxu3amct0r1EGpIQgY+wOwnXx5syWsL/amBUi0nB
# k+3htFzgb+sm+YzVsvk4EObqzpH1vtP7b5NhNFy8k0UogzYqZihfsHPOiyYlBrKD
# 1Fz2FRlM7WLgXjPy6OjsCqewAyuRsjZ5vvetCB51pmXMu+NIUPN3kRr+21CiRshh
# WJj1fAIWPIMorTmG7NS3DVPQ+EfmdTCN7DCTdhSmW0tddGFNPxKRdt6/WMtyEClB
# 8NXFbSZ2aBFBE1ia3CYrAfSJTVnbeM+BSj5AR1/JgVBzhRAjIVlgimRUwcwhGug4
# GXxmHM14OEUwmU//Y09Mu6oNCFNBfFg9R7P6tuyMMgkCzGw8DFYRAgMBAAGjggFZ
# MIIBVTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRoN+Drtjv4XxGG+/5h
# ewiIZfROQjAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qYrhwPTzAOBgNVHQ8B
# Af8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwdwYIKwYBBQUHAQEEazBpMCQG
# CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKG
# NWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290
# RzQuY3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMBwGA1UdIAQVMBMwBwYFZ4EMAQMw
# CAYGZ4EMAQQBMA0GCSqGSIb3DQEBDAUAA4ICAQA6I0Q9jQh27o+8OpnTVuACGqX4
# SDTzLLbmdGb3lHKxAMqvbDAnExKekESfS/2eo3wm1Te8Ol1IbZXVP0n0J7sWgUVQ
# /Zy9toXgdn43ccsi91qqkM/1k2rj6yDR1VB5iJqKisG2vaFIGH7c2IAaERkYzWGZ
# gVb2yeN258TkG19D+D6U/3Y5PZ7Umc9K3SjrXyahlVhI1Rr+1yc//ZDRdobdHLBg
# XPMNqO7giaG9OeE4Ttpuuzad++UhU1rDyulq8aI+20O4M8hPOBSSmfXdzlRt2V0C
# FB9AM3wD4pWywiF1c1LLRtjENByipUuNzW92NyyFPxrOJukYvpAHsEN/lYgggnDw
# zMrv/Sk1XB+JOFX3N4qLCaHLC+kxGv8uGVw5ceG+nKcKBtYmZ7eS5k5f3nqsSc8u
# pHSSrds8pJyGH+PBVhsrI/+PteqIe3Br5qC6/To/RabE6BaRUotBwEiES5ZNq0RA
# 443wFSjO7fEYVgcqLxDEDAhkPDOPriiMPMuPiAsNvzv0zh57ju+168u38HcT5uco
# P6wSrqUvImxB+YJcFWbMbA7KxYbD9iYzDAdLoNMHAmpqQDBISzSoUSC7rRuFCOJZ
# DW3KBVAr6kocnqX9oKcfBnTn8tZSkP2vhUgh+Vc7tJwD7YZF9LRhbr9o4iZghurI
# r6n+lB3nYxs6hlZ4TjCCBsIwggSqoAMCAQICEAVEr/OUnQg5pr/bP1/lYRYwDQYJ
# KoZIhvcNAQELBQAwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
# bmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2
# IFRpbWVTdGFtcGluZyBDQTAeFw0yMzA3MTQwMDAwMDBaFw0zNDEwMTMyMzU5NTla
# MEgxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjEgMB4GA1UE
# AxMXRGlnaUNlcnQgVGltZXN0YW1wIDIwMjMwggIiMA0GCSqGSIb3DQEBAQUAA4IC
# DwAwggIKAoICAQCjU0WHHYOOW6w+VLMj4M+f1+XS512hDgncL0ijl3o7Kpxn3GIV
# WMGpkxGnzaqyat0QKYoeYmNp01icNXG/OpfrlFCPHCDqx5o7L5Zm42nnaf5bw9Yr
# IBzBl5S0pVCB8s/LB6YwaMqDQtr8fwkklKSCGtpqutg7yl3eGRiF+0XqDWFsnf5x
# XsQGmjzwxS55DxtmUuPI1j5f2kPThPXQx/ZILV5FdZZ1/t0QoRuDwbjmUpW1R9d4
# KTlr4HhZl+NEK0rVlc7vCBfqgmRN/yPjyobutKQhZHDr1eWg2mOzLukF7qr2JPUd
# vJscsrdf3/Dudn0xmWVHVZ1KJC+sK5e+n+T9e3M+Mu5SNPvUu+vUoCw0m+PebmQZ
# BzcBkQ8ctVHNqkxmg4hoYru8QRt4GW3k2Q/gWEH72LEs4VGvtK0VBhTqYggT02ke
# fGRNnQ/fztFejKqrUBXJs8q818Q7aESjpTtC/XN97t0K/3k0EH6mXApYTAA+hWl1
# x4Nk1nXNjxJ2VqUk+tfEayG66B80mC866msBsPf7Kobse1I4qZgJoXGybHGvPrhv
# ltXhEBP+YUcKjP7wtsfVx95sJPC/QoLKoHE9nJKTBLRpcCcNT7e1NtHJXwikcKPs
# CvERLmTgyyIryvEoEyFJUX4GZtM7vvrrkTjYUQfKlLfiUKHzOtOKg8tAewIDAQAB
# o4IBizCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/
# BAwwCgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcB
# MB8GA1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCPnshvMB0GA1UdDgQWBBSltu8T
# 5+/N0GSh1VapZTGj3tXjSTBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsMy5k
# aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hBMjU2VGltZVN0
# YW1waW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCBgDAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUFBzAChkxodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hBMjU2VGlt
# ZVN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCBGtbeoKm1mBe8cI1P
# ijxonNgl/8ss5M3qXSKS7IwiAqm4z4Co2efjxe0mgopxLxjdTrbebNfhYJwr7e09
# SI64a7p8Xb3CYTdoSXej65CqEtcnhfOOHpLawkA4n13IoC4leCWdKgV6hCmYtld5
# j9smViuw86e9NwzYmHZPVrlSwradOKmB521BXIxp0bkrxMZ7z5z6eOKTGnaiaXXT
# UOREEr4gDZ6pRND45Ul3CFohxbTPmJUaVLq5vMFpGbrPFvKDNzRusEEm3d5al08z
# jdSNd311RaGlWCZqA0Xe2VC1UIyvVr1MxeFGxSjTredDAHDezJieGYkD6tSRN+9N
# UvPJYCHEVkft2hFLjDLDiOZY4rbbPvlfsELWj+MXkdGqwFXjhr+sJyxB0JozSqg2
# 1Llyln6XeThIX8rC3D0y33XWNmdaifj2p8flTzU8AL2+nCpseQHc2kTmOt44Owde
# OVj0fHMxVaCAEcsUDH6uvP6k63llqmjWIso765qCNVcoFstp8jKastLYOrixRoZr
# uhf9xHdsFWyuq69zOuhJRrfVf8y2OMDY7Bz1tqG4QyzfTkx9HmhwwHcK1ALgXGC7
# KP845VJa1qwXIiNO9OzTF/tQa/8Hdx9xl0RBybhG02wyfFgvZ0dl5Rtztpn5aywG
# Ru9BHvDwX+Db2a2QgESvgBBBijCCB2cwggVPoAMCAQICEATd+82EVAN2YngfhA+f
# z/UwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lD
# ZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2ln
# bmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMTAeFw0yMzEwMDQwMDAwMDBaFw0y
# NjExMTUyMzU5NTlaMG8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MREwDwYDVQQHEwhCZWxsZXZ1ZTEbMBkGA1UEChMSUHVyZSBTdG9yYWdlLCBJbmMu
# MRswGQYDVQQDExJQdXJlIFN0b3JhZ2UsIEluYy4wggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQCdhXqOLFS3HR5KD2RtAOzGdwKU0mMGGHfU7qUo1YFvDCN8
# vF/X8LDhouGtsZPdIfd298orsXHfXYElTgBo91gba7SqKBWi9xdXTqMR5vpt41K/
# a554AgiQp02nfYwuspZoAGnt//mDJ6ErP1jUFiWuwHsYsxk0gFEayp5xIKzmj3q4
# 9g+AenKpktbDn6HPpXZPdvg+g+GR9lPpiJo7Z40SIqzaacJsVcl5MhPfbFdLeP1s
# n0MBW3BiYLyz4CEUq8IA2vJ2557N0uB0UzWERE31brL0mBn5gB1g8Zij9VsI9J5+
# Q+THKYIgwknlnXFiSwQhQbJ3Cn7IVotei1M/D011XjUR66kNHm02VVDsbxX92xLf
# qIX7BZ0e6shMsOFVakkdM00nXhfRscDkRqEQ+IwgC3vcyJgp/QRX0SfWaaD5G0fi
# ECMBZtmq5hijTJ18MAW2KaFePW0PIn9IRnoXS3tx9coXVJMTFwnLYdIukelF4jIW
# 779IP5lQH7IBNHS01BgysjWVaQhPYxWZYtsxyRUX3gVRjFChhOtBNCAy2S+YYjUS
# TOM7CdUNTtCARX/HgcRYxxU7UTOYXPYyabdQu3mFF8yD5YNkarlgc4TQ+H1PWnIU
# l7pq3P0ZSaE5Est24ApVi6wlZC/Q3jQRKPziRg8x7Zv1TZX8TfxPDmE0Nsd+BwID
# AQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYD
# VR0OBBYEFCvH/lBQxrVtiuuihv+e6+2VgDPXMD4GA1UdIAQ3MDUwMwYGZ4EMAQQB
# MCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNV
# HQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBT
# oFGgT4ZNaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0
# Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6
# Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5n
# UlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggr
# BgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBo
# dHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2Rl
# U2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqG
# SIb3DQEBCwUAA4ICAQCrjkZxw1B2w/pYu4siB36x5J9Xate13IDt57bxZvc7OGgz
# limeUq1/HObzW4Vej9tESBpT6a5FBuVSXQXvYQntQczEFBBksRXyad3tx5/xElHA
# LaE6BCZUtPKu3/CSrgsvVi7OgWNybOFWF2Xk9K1djImG55J7jOY+8ZKegUSlHPjB
# 8HF9G4VdS85L2SuFaRzOMTEIW+h0Ihkp6Js1rbe0YLZu/ad6VWFKoX++FDg3cbM8
# FLf482p+XCmuX/qZuFmySYDQQ4jvNiecEiyZ4m6HUryx9Fagc0NBADiOJc1R2p2I
# QbBasyndhn8KWlGSudJ+uCfuzD6ukGVe4kOpYlqkzVeOscetaY0/5v+896yP4FA8
# NS68I2eMuKbis2ouOIrAVkNPdymBjaEW1U6q979upeEG22UjjrRkq5qSdO+nk2tK
# NL1ZIc92bqIs132yuwVZ6A7Dvez03VSitT2UVBMz0BKNy1EnZ4hjqBrApU+Bbcwc
# 7nPV9hKKbEFKCcCNLpkAP8SCVX6r7qMyqYhAl+XKSfCkMpxRD2LykRup5mz54cQP
# RPoy86iVhFhWUez1O3t371sgYulMuxaff5mXK3xlzYZUHpJGkOYntQ2VlqUpl/VO
# KcNTXWnuPOyuUZY0b9tWU0Ofs8Imp7+lULJ7XUbrJoY1bUa22ce912PVBsWOojGC
# BjowggY2AgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
# bmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBS
# U0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQBN37zYRUA3ZieB+ED5/P9TAJBgUrDgMC
# GgUAoHAwEAYKKwYBBAGCNwIBDDECMAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcC
# AQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYE
# FBhgicSufgmFKQ8NzkptCOh34dk9MA0GCSqGSIb3DQEBAQUABIICACU7Xr6RXlSI
# RUgCRdgInT5idCUZLh3q4f+vMMa/erdn46P2O5ewX53L4We/f1/67ryyG07tgSK5
# H9zd/Hjl52MP72UZhaSnqcOkfq3sbWeuB9CH6pxeAwKp+ZZiFbR4qLROab6iZ9/2
# e4G81fwswkjMhaGbGLp3EUtFihhb2I7m4SFgE67cNVW2AQm5606aQYQMamUlAWnH
# B1SaU2bnTo9l0ihOllqzzHyFf4vb6KvZWstrgvrGQPzMAylKAbd+5pVe8xB28yvp
# 4HXHCDDJsSzfs1gixDUp1PL1D/YiNbnUZeI6ouuJVlL4kW6+6DVLMkej0oKBfLNs
# EK5EjMjPo+fvZUfA3+TCvoq4YrShL4idwDNZxUG7hGQA6crEHHiguP90M7LO5CKq
# UdkspnS5b24bRjMLIB0hp4GMxe/ciChFB+lsxDl4qQ2946xB+DiH9sE4yZCFCePd
# //qu22SpRqYo9vTqa46W20T2JZIo1Oebpq8Oq2Le46iEhkNxlAvmu94GKRQofvJa
# LEpLb7QYG4NVAUEjJgw3Nhpe1fpYU6ec7mTyI8AO/GI/aevCwztMR/ivi4Md83AS
# 6EICIEzeSMrByKK2uiETiKCxniHuWgeduv/4BNp4xET5v4ZcKnSsHcpWJrlIeBBB
# SfAL/4dEE4P1promNYU10Hedmr5MfAQPoYIDIDCCAxwGCSqGSIb3DQEJBjGCAw0w
# ggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
# MTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRp
# bWVTdGFtcGluZyBDQQIQBUSv85SdCDmmv9s/X+VhFjANBglghkgBZQMEAgEFAKBp
# MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEx
# MDAwMjY1NFowLwYJKoZIhvcNAQkEMSIEIK2k4kWSjSuCKAnSrJJEEv08chNB8v3m
# Xjz3WF6owUaDMA0GCSqGSIb3DQEBAQUABIICAIInd8b7xAoU6ePnQdis9+Qbcw3Y
# 7m+MG8cnXa4gDf6g942XiFb33be9cfJtOeXFrDHQSAJ0W/3I2YNi8m6kAYx+t/o6
# b5Z5UfFZEkyhleH6DIDKmzYF9mX9cxkG74uq0229d8qshWzVL5hQZBKZRz4eRZqh
# llrjdFm6a6GQcXpDyUWXhWvr+JaxgNdMpY3RGjlxBIywRbEyzLF2n/CbfWB1V01u
# SXTHB9NqdHUAc/T8YoRTEId9jD2Nkc9nFP4s53MhLwuREKfzhcdb69S00XfLM655
# i3/ChAqBs9y7bV5pOZmRg0otHP07ZfGQRVjhwuET9D4gIFJODbDL7OnONvaTGCDt
# e3WkQ7KsZWipMACG7XoryyR4fU7sLLD0J+tI1k1IEcgXyFHQRBxn0Hd/nzcACgKT
# 5OymGntr7KiQJijghKTaYIei76cHRVyZH6VMfULNzy/k1fBJRbmA+V0IPt+H/3C4
# mIYpGg/D9MJJ9LmMu5E4JItz1YM/4YPpAVg700Yk3niHr+SkJg7TjONnEbHVWGjs
# eI7IT3qkzS9k1AU3m486vQGF1XAteEpUkImxRQjnVN/cWIqAqEqbzQv0L8rwozEz
# 4iVh2i/1dj2OObDT+D+5zUBjZUGy/P6lLYmK/je2fsOKiH48buBJmfQHiNqh8jQp
# GPsMtHqRXIagiBM4
# SIG # End signature block