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 |