PureStorage.FlashArray.Backup.psm1

Set-StrictMode -Version 1.0
$here = Split-Path -Parent $MyInvocation.MyCommand.Path -ErrorAction Stop

Import-Module PureStoragePowerShellSDK2

Add-Type -AssemblyName System.Core
Add-Type -AssemblyName System.Security

$script:StatusSucceeded = "Succeeded"
$script:SDKPrefix = "PSB"

$RestLimit = 100
$RetentionPolicySecs = 86400
$RetentionPolicySnapsPerDay = 4
$RetentionPolicyDaysToKeep = 7

$PSBSDK_Remoting_Dir = "$here"
. "$PSBSDK_Remoting_Dir\Utils.ps1"
. "$PSBSDK_Remoting_Dir\Utils.Log.ps1"
. "$PSBSDK_Remoting_Dir\Utils.Exported.ps1"
. "$PSBSDK_Remoting_Dir\Utils.VMWare.ps1"
. "$PSBSDK_Remoting_Dir\Utils.VolumeSet.ps1"
. "$PSBSDK_Remoting_Dir\Utils.VVOLS.ps1"
. "$PSBSDK_Remoting_Dir\Utils.CrashCons.ps1"

# Tag Keys
enum PureTagKeys {
    SnapType
    HistoryId
    MountId
    Metadata
}

# Tag Namespaces
$NamespaceVolSet = "$($script:SDKPrefix)_volset"
$NamespaceMount = "$($script:SDKPrefix)_mount"
$NamespaceMeta = "$($script:SDKPrefix)_meta"

# Status msgs
$validateVolSetMsg = "Validating Volume set."
$validatePgroupMsg = "Validating protection group choice."
$takingSnapshotMsg = "Taking snapshot"
$replicatingSnapshotMsg = "Replicating snapshot"
$noPgroupWarnMsg = "The volume set specified contains multiple volumes. Taking snapshots of multiple volumes outside of a protection group will not guarantee crash consistency. Are you sure you want to proceed?"
$noPgroupDescMsg = "Taking snapshots of multiple volumes without Protection Groups. Crash consistency cannot be guaranteed."
$noPgroupTitleMsg = "NoPgroup Option Selected"
$userAbortedMsg = "Operation cancelled by the user"
$GetCorVolMsg = "Getting corresponding volumes on FlashArray"
$TagVolsMsg = "Tagging matching volumes on FlashArray"
$LoadTagsMsg = "Loading tags from FlashArray"
$BldVolSetMsg = "Building volume set objects"
$RemMetaMsg = "Removing metadata"
$FindMatPgMsg = "Finding matching pgroups"
$vSetExistsDescMsg = "Overwriting existing volume set. This might affect existing Backups"
$vSetExistsWarnMsg = "A volume set with this name already exists, do you want to overwrite it? This will affect other future Backups."
$vSetExistsTitleMsg = "Volume Set Name already exists"



$ModulePath = "$env:ProgramW6432\WindowsPowerShell\Modules"
if (-not $env:PSModulePath.Contains($ModulePath)) {
    $env:PSModulePath += ";$ModulePath"
}

################# EXPORTED CMDLETS #################

<#
 .Synopsis
    Creates a snapshot on all of the Pure Volumes in the Volume Set.
 .Description
    Create a snapshot on all of the Pure Volumes in the Volume Set. Decide on the Protection Group behavior by utilizing the appropriate parameters.
    If no Protection Groups exist, volume snapshots can be taken using the -NoPgroup parameter with the caveat that if there is more than 1 volume it is not guaranteed to be consistent without using a Protection Group snapshot. A Protection Group can be created using the -createpgroup parameter.
    If a single Protection Group exists you can declare it in the Invoke-PsbSnapshotJob cmdlet with the -PgroupName parameter.
    If more than one Protection Groups exist, you can enumerate their names with this cmdlet and choose the one you want, then declare it with the Invoke-PsbSnapshotJob cmdlet with the -PgroupName parameter.
    You can also let the Invoke-PsbSnapshotJob choose the Protection Group that has all volumes in the volume set, with the fewest number of other volumes with the -UseBestPgroupMatch parameter, as denoted in the "ExtraVolumesCount" column from this cmdlet.
 .Parameter VolumeSetName
    Volume Set Name that identifies an existing Volume Set on the Flash Array.
 .Parameter FlashArrayAddress
    The FlashArray address
 .Parameter FlashArrayCredential
    FlashArray credentials
 .Parameter ComputerAddress
    The name of the Computer
 .Parameter ComputerCredential
    Computer credentials
 .Parameter Path
    The Path to the disks to be backed up. Must be Drive Letters or Mount Points.
 .Parameter VCenterAddress
    The name of the controlling VCenter, if the computer is a VM
 .Parameter VCenterCredential
    VCenter credentials
 .Parameter VMName
    The name of the VM, if the computer is a VM. This may be different from the ComputerAddress.
 .Parameter VMPersistentId
    The PersistentId (also referred to as the InstanceUuid or MoRef) of the VM.
 .Parameter SkipValidation
    Do not validate configuration before saving.
 .Parameter VolumeType
    The type of the volumes being added to the set. Valid values are RDM, VVOL or Physical
 .Parameter PgroupName
    The name of an existing Protection Group to be used for taking the snapshot.
 .Parameter UseBestPgroupMatch
    The best match among the Protection Groups that include all volumes in the volume set will be used for taking the snapshot.
 .Parameter CreatePgroup
    A protection group with the name specified by 'NewPgroupName' will be created, including all volumes in the volume set.
 .Parameter NewPgroupSuffix
    The suffix that will be used to create the new Protection Group. The protection group will be named PSB-NewPgroupSuffix
 .Parameter NoPgroup
    When selected, the function will use volume snapshots rather than Protection Group Snapshots.
    Volume snapshots are taken individually and cannot guarantee consistency among multiple volumes in the set.
    Volume snapshots are not subject to retention policies and need to be removed manually.
 .Example
    Invoke-PsbSnapshotJob -FlashArrayAddress $FlashArrayEndpoint -FlashArrayCredential $FlashArrayCredential -VolumeSetName qsqlvm4g -VolumeType VVOL -VCenterAddress $vCenterEndpoint -VCenterCredential $vCenterCredential -ComputerAddress $vmAddress -ComputerCredential $vmCredential -Path 'g:\' -VMName $vmName -VMPersistentId $vmpid -usebestpgroupmatch
    Creates Protection Group snapshots on all volumes in the Volume Set and uses the best Protection Group.
 .Example
    Invoke-PsbSnapshotJob -FlashArrayAddress $FlashArrayEndpoint -FlashArrayCredential $FlashArrayCredential -VolumeSetName qsqlvm4g -VolumeType VVOL -VCenterAddress $vCenterEndpoint -VCenterCredential $vCenterCredential -ComputerAddress $vmAddress -ComputerCredential $vmCredential -Path 'g:\' -VMName $vmName -VMPersistentId $vmpid -createpgroup -newpgroupsuffix temp-pgroup1
    Creates Protection Group snapshots on all volumes in the Volume Set and creates a Protection Group with the declared suffix.
 .Example
    Invoke-PsbSnapshotJob -FlashArrayAddress $FlashArrayEndpoint -FlashArrayCredential $FlashArrayCredential -VolumeSetName qsqlvm4-g -VolumeType VVOL -VCenterAddress $vCenterEndpoint -VCenterCredential $vCenterCredential -ComputerAddress $vmAddress -ComputerCredential $vmCredential -Path 'g:\' -VMName $vmName -VMPersistentId $vmpid -pgroupname asyncdemo
    Creates Protection Group snapshots on all volumes in the Volume Set and creates a uses the declared Protection Group asyncdemo.
 .Example
    Invoke-PsbSnapshotJob -FlashArrayAddress $FlashArrayEndpoint -FlashArrayCredential $FlashArrayCredential -VolumeSetName qsqlvm4-g -VolumeType VVOL -VCenterAddress $vCenterEndpoint -VCenterCredential $vCenterCredential -ComputerAddress $vmAddress -ComputerCredential $vmCredential -Path 'g:\' -VMName $vmName -VMPersistentId $vmpid -nopgroup
    Creates volume snapshots on all volumes in the Volume Set.
#>

function Invoke-PsbSnapshotJob {
    [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')]
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $FlashArrayAddress,

        [parameter(Mandatory = $true)]
        [PSCredential]
        $FlashArrayCredential,

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

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

        [parameter(ParameterSetName='Credential_Virtual_UseExistingPgroup' , Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Virtual_UseBestPgroupMatch'  , Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Virtual_CreatePgroup'   , Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Virtual_NoPgroup'   , Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Physical_UseExistingPgroup'        , Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Physical_UseBestPgroupMatch'         , Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Physical_CreatePgroup'          , Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Physical_NoPgroup'          , Mandatory = $true)]
        [PSCredential]
        $ComputerCredential,

        [parameter(ParameterSetName='PSSession_Virtual_UseExistingPgroup' , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_UseBestPgroupMatch'  , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_CreatePgroup'   , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_NoPgroup'   , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Physical_UseExistingPgroup'        , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Physical_UseBestPgroupMatch'         , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Physical_CreatePgroup'          , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Physical_NoPgroup'          , Mandatory = $true)]
        [System.Management.Automation.Runspaces.PSSession]
        $ComputerSession,

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

        [parameter(ParameterSetName='Credential_Physical_UseExistingPgroup', Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Virtual_UseExistingPgroup', Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Physical_UseExistingPgroup', Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_UseExistingPgroup', Mandatory = $true)]
        [string]
        $PgroupName,

        [parameter(ParameterSetName='Credential_Physical_UseBestPgroupMatch', Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Virtual_UseBestPgroupMatch', Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Physical_UseBestPgroupMatch', Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_UseBestPgroupMatch', Mandatory = $true)]
        [switch]
        $UseBestPgroupMatch,

        [parameter(ParameterSetName='Credential_Physical_CreatePgroup', Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Virtual_CreatePgroup', Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Physical_CreatePgroup', Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_CreatePgroup', Mandatory = $true)]
        [switch]
        $CreatePgroup,

        [parameter(ParameterSetName='Credential_Physical_CreatePgroup', Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Virtual_CreatePgroup', Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Physical_CreatePgroup', Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_CreatePgroup', Mandatory = $true)]
        [string]
        $NewPgroupSuffix,

        [parameter(ParameterSetName='Credential_Physical_NoPgroup', Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Virtual_NoPgroup', Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Physical_NoPgroup', Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_NoPgroup', Mandatory = $true)]
        [switch]
        $NoPgroup,

        [parameter(ParameterSetName='Credential_Physical_UseBestPgroupMatch', Mandatory = $false)]
        [parameter(ParameterSetName='Credential_Virtual_UseBestPgroupMatch', Mandatory = $false)]
        [parameter(ParameterSetName='PSSession_Physical_UseBestPgroupMatch', Mandatory = $false)]
        [parameter(ParameterSetName='PSSession_Virtual_UseBestPgroupMatch', Mandatory = $false)]
        [parameter(ParameterSetName='Credential_Physical_UseExistingPgroup', Mandatory = $false)]
        [parameter(ParameterSetName='Credential_Virtual_UseExistingPgroup', Mandatory = $false)]
        [parameter(ParameterSetName='PSSession_Physical_UseExistingPgroup', Mandatory = $false)]
        [parameter(ParameterSetName='PSSession_Virtual_UseExistingPgroup', Mandatory = $false)]
        [switch]
        $ReplicateNow,

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

        [parameter(ParameterSetName='Credential_Virtual_UseExistingPgroup' , Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Virtual_UseBestPgroupMatch'  , Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Virtual_CreatePgroup'   , Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Virtual_NoPgroup'   , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_UseExistingPgroup' , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_UseBestPgroupMatch'  , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_CreatePgroup'   , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_NoPgroup'   , Mandatory = $true)]
        [string]
        $VCenterAddress,

        [parameter(ParameterSetName='Credential_Virtual_UseExistingPgroup' , Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Virtual_UseBestPgroupMatch'  , Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Virtual_CreatePgroup'   , Mandatory = $true)]
        [parameter(ParameterSetName='Credential_Virtual_NoPgroup'   , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_UseExistingPgroup' , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_UseBestPgroupMatch'  , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_CreatePgroup'   , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_NoPgroup'   , Mandatory = $true)]
        [PSCredential]
        $VCenterCredential,

        [parameter(ParameterSetName='Credential_Virtual_UseExistingPgroup' , Mandatory = $false)]
        [parameter(ParameterSetName='Credential_Virtual_UseBestPgroupMatch'  , Mandatory = $false)]
        [parameter(ParameterSetName='Credential_Virtual_CreatePgroup'   , Mandatory = $false)]
        [parameter(ParameterSetName='Credential_Virtual_NoPgroup'   , Mandatory = $false)]
        [parameter(ParameterSetName='PSSession_Virtual_UseExistingPgroup' , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_UseBestPgroupMatch'  , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_CreatePgroup'   , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_NoPgroup'   , Mandatory = $true)]
        [string]
        $VMName,

        [parameter(ParameterSetName='Credential_Virtual_UseExistingPgroup' , Mandatory = $false)]
        [parameter(ParameterSetName='Credential_Virtual_UseBestPgroupMatch'  , Mandatory = $false)]
        [parameter(ParameterSetName='Credential_Virtual_CreatePgroup'   , Mandatory = $false)]
        [parameter(ParameterSetName='Credential_Virtual_NoPgroup'   , Mandatory = $false)]
        [parameter(ParameterSetName='PSSession_Virtual_UseExistingPgroup' , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_UseBestPgroupMatch'  , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_CreatePgroup'   , Mandatory = $true)]
        [parameter(ParameterSetName='PSSession_Virtual_NoPgroup'   , Mandatory = $true)]
        [string]
        $VMPersistentId,

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

        [parameter(Mandatory = $false)]
        [switch]
        $Force
    )
    return Invoke-Pfa2CmdletWrapper -FunctionName $MyInvocation.MyCommand -Parameters $MyInvocation.BoundParameters
}

<#
 .Synopsis
    Retrieves history of backup jobs.
 .Description
    Returns list of snapshots for Volume Set specified by VolumeSetName.
 .Parameter FlashArrayAddress
    The FlashArray address
 .Parameter FlashArrayCredential
    FlashArray credentials
 .Parameter VolumeSetName
    Volume Set Name that identifies an existing Volume Set on the Flash Array.
 .Parameter All
    When specified, snapshots for all volume sets will be returned.
 .Parameter IncludeNonSDKSnapshots
    Switch. When used will include snapshots created outside the SDK.
 .Parameter UseLocalTime
    Switch. When used, creation time will be returned in local time format.
 .Parameter Limit
    Limit of history items returned. Default is 10.
 .Example
    Get-PsbSnapshotJobHistory -FlashArrayAddress $FlashArrayEndpoint -FlashArrayCredential $FlashArrayCredential -all
    Retrieves a list of all snapshots from all Volume Sets.
 .Example
    Get-PsbSnapshotJobHistory -flasharrayaddress $FlashArrayEndpoint -flasharraycredential $FlashArrayCredential -VolumeSetName sqlvm4-g
    Retrieves a list of all snapshots from the specified Volume Set.
 .Example
    Get-PsbSnapshotJobHistory -flasharrayaddress $FlashArrayEndpoint -flasharraycredential $FlashArrayCredential -VolumeSetName sqlvm4-g -IncludeNonSDKSnapshots
    Retrieves a list of all snapshots from the specified Volume Set including snapshots that were not taken using Invoke-PsbSnapshotJob.
#>

function Get-PsbSnapshotJobHistory {
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $FlashArrayAddress,

        [parameter(Mandatory = $true)]
        [PSCredential]
        $FlashArrayCredential,

        [parameter(ParameterSetName='ByVolumeSet', Mandatory = $true)]
        [string]
        $VolumeSetName,

        [parameter(ParameterSetName="All", Mandatory=$true)]
        [switch]
        $All,

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

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

        [parameter(Mandatory = $false)]
        [int]
        $Limit=10
    )
    return Invoke-Pfa2CmdletWrapper -FunctionName $MyInvocation.MyCommand -Parameters $MyInvocation.BoundParameters
}

<#
 .Synopsis
    Removes a snapshot from the backup history.
 .Description
    Removes a specified snapshot, or all snapshots from the backup history. Snapshots cannot be removed if they are mounted.
 .Parameter FlashArrayAddress
    The FlashArray address
 .Parameter FlashArrayCredential
    FlashArray credentials
 .Parameter HistoryId
    HistoryId of a previously invoked backup job.
 .Example
    $snaps = Get-PsbSnapshotJobHistory -FlashArrayAddress $FlashArrayEndpoint -FlashArrayCredential $FlashArrayCredential -VolumeSetName qsqlvm4g
    Remove-PsbSnapshotSet -FlashArrayAddress $FlashArrayEndpoint -FlashArrayCredential $FlashArrayCredential -HistoryId $snaps[0].historyid
    Enumerate snapshots for a specified Volume Set and then remove the newest snapshot from the FlashArray.
#>

function Remove-PsbSnapshotSet {
    [cmdletbinding(SupportsShouldProcess=$true, ConfirmImpact='High')]
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $FlashArrayAddress,

        [parameter(Mandatory = $true)]
        [PSCredential]
        $FlashArrayCredential,

        [parameter(Mandatory = $true)]
        [string]
        $HistoryId,

        [parameter(Mandatory = $false)]
        [switch]
        $Force
    )
    return Invoke-Pfa2CmdletWrapper -FunctionName $MyInvocation.MyCommand -Parameters $MyInvocation.BoundParameters
}

<#
 .Synopsis
    Mounts a snapshot from the Backup History to the specified machine.
    The disk type will be preserved in the mount. For example a Volume Set using pRDM will mount a copy of its snapshots as a pRDM.
    A Volume Set using a vVol will mount a copy of its snapshots as vVol.
    A Volume Set using physical cannot be mounted to a VM, unless the VM is using in-guest iSCSI where it will be treated as physical.
 .Description
    Creates a volume copy of a snapshot and exposes it as a disk on the specified machine.
 .Parameter HistoryId
    HistoryId of a previously invoked backup job.
 .Parameter FlashArrayAddress
    Flash Array Name where the snapshots is located.
 .Parameter FlashArrayCredential
    FlashArray credentials
 .Parameter Path
    Drive Letters or Mount POints to be used to expose volume copies.
 .Parameter ComputerAddress
    Credential Name for computer to be used on the mount. If not specified, it will use the same as the one specified on history.
 .Parameter ComputerCredential
    Mount computer credentials
 .Parameter VMName
    VM Name for computer to be used on the mount if volume type is RDM or vVols. If not specified, it will use the same as the one specified on history.
 .Parameter VMPersistentId
    VM Persistent ID for computer to be used on the mount if volume type is RDM or vVols. If not specified, it will use the same as the one specified on history.
 .Parameter VCenterAddress
    VCenter Credential Name for environment to be used on the mount if volume type is RDM or vVols. If not specified, it will use the same as the one specified on history.
 .Parameter VCenterCredential
    vCenter credentials
 .Parameter DiskTimeout
    Timeout in seconds to wait for mounted disk partitions to load. Default is 5 seconds.
 .Parameter DiskLoadRetries
    Number of retries to load mounted disk partitions. Default is 5 retries.

 .Example
    $history = Get-PsbSnapshotJobHistory -FlashArrayAddress $FlashArrayEndpoint -VolumeSetName $myVolSet -Limit 1
    Mount-PsbSnapshotSet -HistoryId $history.HistoryId -FlashArrayAddress $FlashArrayEndpoint -Path 'e:,f:'
    First enumerate the backup history for $myVolSet retaining the most recent snapshot. Then mount the returned history to the original computer on drive letters E: and F:
 .Example
    $history = Get-PsbSnapshotJobHistory -FlashArrayAddress $FlashArrayEndpoint -FlashArrayCredential $fa30 -volumesetname e0801
    Mount-PsbSnapshotSet -HistoryId $history[0].HistoryId -FlashArrayAddress $FlashArrayEndpoint -flasharraycredential $fa30 -ComputerAddress sqlvm4 -computercredential $sqlvm4 -Path 'c:\mp61'
    First enumerate the backup history for volume set ‘e0801’ retaining the most recent snapshot. Then mount the returned history to a new server using mount point c:\mp61.
#>

function Mount-PsbSnapshotSet {
    [CmdletBinding()]
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $HistoryId,

        [parameter(Mandatory = $true)]
        [string]
        $FlashArrayAddress,

        [parameter(Mandatory = $true)]
        [PSCredential]
        $FlashArrayCredential,

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

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

        [parameter(ParameterSetName= 'Credential_Physical', Mandatory = $true)]
        [parameter(ParameterSetName= 'Credential_Virtual', Mandatory = $true)]
        [PSCredential]
        $ComputerCredential,

        [parameter(ParameterSetName= 'PSSession_Physical', Mandatory = $true)]
        [parameter(ParameterSetName= 'PSSession_Virtual', Mandatory = $true)]
        [System.Management.Automation.Runspaces.PSSession]
        $ComputerSession,

        [parameter(ParameterSetName= 'PSSession_Virtual', Mandatory = $false)]
        [parameter(ParameterSetName= 'Credential_Virtual', Mandatory = $false)]
        [string]
        $VMName,

        [parameter(ParameterSetName= 'PSSession_Virtual', Mandatory = $false)]
        [parameter(ParameterSetName= 'Credential_Virtual', Mandatory = $false)]
        [string]
        $VMPersistentId,

        [parameter(ParameterSetName= 'PSSession_Virtual', Mandatory = $true)]
        [parameter(ParameterSetName= 'Credential_Virtual', Mandatory = $true)]
        [string]
        $VCenterAddress,

        [parameter(ParameterSetName= 'PSSession_Virtual', Mandatory = $true)]
        [parameter(ParameterSetName= 'Credential_Virtual', Mandatory = $true)]
        [PSCredential]
        $VCenterCredential,

        [parameter(ParameterSetName= 'PSSession_Virtual', Mandatory = $false)]
        [parameter(ParameterSetName= 'Credential_Virtual', Mandatory = $false)]
        [string]
        $DatastoreName,

        [parameter(Mandatory = $false)]
        [int]
        $DiskTimeout = 5,

        [parameter(Mandatory = $false)]
        [int]
        $DiskLoadRetries = 5
    )
    return Invoke-Pfa2CmdletWrapper -FunctionName $MyInvocation.MyCommand -Parameters $MyInvocation.BoundParameters
}

<#
 .Synopsis
    Dismount a Volume Set snapshot from a host, remove the entry from the Mount History, and delete it from the FlashArray.
 .Description
    Unexposes disk from the mount machine, and then removes the volume copy.
 .Parameter MountId
    MountId of the Mount history to be dismounted.
 .Parameter FlashArrayAddress
    The FlashArray address
 .Parameter FlashArrayCredential
    FlashArray credentials
 .Parameter Paths
    Drive Letters or Mount POints to be used to expose volume copies.
 .Parameter ComputerAddress
    Credential Name for computer to be used on the mount. If not specified, it will use the same as the one specified on history.
 .Parameter ComputerCredential
    Mount computer credentials
 .Parameter VMName
    VM Name for computer to be used on the mount if volume type is RDM or vVols. If not specified, it will use the same as the one specified on history.
 .Parameter VCenterAddress
    VCenter Address for environment to be used on the mount if volume type is RDM or vVols.
 .Parameter VCenterCredential
    vCenter credentials
 .Parameter VCenterCredential
    vCenter credentials
 .Example
    $mountHistory = Get-PsbSnapshotSetMountHistory -FlashArrayAddress $FlashArrayEndpoint -FlashArrayCredential (Get-Credential)
    Dismount-PsbSnapshotSet -FlashArrayAddress $FlashArrayEndpoint -MountId $mountHistory[0].MountId -ComputerAddress $sqlvm4 -ComputerCredential $sqlvm4
    First enumerate the Mount History. Then dismount the first mount history entry.
#>

function Dismount-PsbSnapshotSet {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $MountId,

        [parameter(Mandatory = $true)]
        [string]
        $FlashArrayAddress,

        [parameter(Mandatory = $true)]
        [PSCredential]
        $FlashArrayCredential,

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

        [parameter(ParameterSetName= 'Credential_Physical', Mandatory = $true)]
        [parameter(ParameterSetName= 'Credential_Virtual', Mandatory = $true)]
        [PSCredential]
        $ComputerCredential,

        [parameter(ParameterSetName= 'PSSession_Physical', Mandatory = $true)]
        [parameter(ParameterSetName= 'PSSession_Virtual', Mandatory = $true)]
        [System.Management.Automation.Runspaces.PSSession]
        $ComputerSession,

        [parameter(ParameterSetName= 'PSSession_Virtual', Mandatory = $true)]
        [parameter(ParameterSetName= 'Credential_Virtual', Mandatory = $true)][string]
        $VCenterAddress,

        [parameter(ParameterSetName= 'PSSession_Virtual', Mandatory = $true)]
        [parameter(ParameterSetName= 'Credential_Virtual', Mandatory = $true)][PSCredential]
        $VCenterCredential
    )
    return Invoke-Pfa2CmdletWrapper -FunctionName $MyInvocation.MyCommand -Parameters $MyInvocation.BoundParameters
}

<#
 .Synopsis
    Retrieves history of mounted backup jobs.
 .Description
    Returns Mount details for all mounted jobs in a Flash Array.
 .Parameter FlashArrayAddress
    The FlashArray address where volume copies were created.
 .Parameter FlashArrayCredential
    FlashArray credentials
 .Parameter HistoryId
    When specified, only mounted jobs for this History Id will be returned.
 .Example
    Get-PsbSnapshotJobHistory -FlashArrayAddress FlashArrayEndpoint -FlashArrayCredential (Get-Credential)
#>

function Get-PsbSnapshotSetMountHistory {
    [CmdletBinding()]
    Param (
        [parameter(Mandatory = $false)]
        [string]
        $HistoryId,

        [parameter(Mandatory = $true)]
        [string]
        $FlashArrayAddress,

        [parameter(Mandatory = $true)]
        [PSCredential]
        $FlashArrayCredential
    )
    return Invoke-Pfa2CmdletWrapper -FunctionName $MyInvocation.MyCommand -Parameters $MyInvocation.BoundParameters
}

<#
 .Synopsis
    Create a volume set from the specified disk paths that can include drive letters and mount points. A recommended prerequisite is to get the VM Persistent ID so automation does not fail in the event the friendly name for the VM is changed in vCenter. The disk type can be physical where a HOST is configured on the FlashArray, a VMware pRDM, or a VMware vVol (Virtual Volume).
 .Description
    Receives a list of disk paths, finds the corresponding volumes on the FlashArray, and then tags the corresponding volumes with the declared VolumeSet tag.
 .Parameter VolumeSetName
    Volume Set Name to identify set of volumes
 .Parameter ComputerAddress
    The name of the host machine.
 .Parameter ComputerCredential
    Computer credentials
 .Parameter FlashArrayAddress
    The FlashArray address
 .Parameter FlashArrayCredential
    FlashArray credentials
 .Parameter VCenterAddress
    The name of the controlling VCenter, if the host is a VM
 .Parameter VMName
    The name of the VM, if the host is a VM. This may be different from the ComputerAddress.
 .Parameter VMPersistentId
    The PersistentId (also referred to as the InstanceUuid or MoRef) of the VM.
 .Parameter Path
    Paths that points to the disks to be included on the volumeset. Should be drive letters or a mount point paths.
 .Parameter SkipValidation
    Do not validate configuration before saving.
 .Parameter VolumeType
    The type of the volumes being added to the set. Valid values are RDM, VVOL or Physical
 .Example
    #First query the VMPersistentID for SQLVM2 and assign it to variable $VMPID.
    $VMPID = Get-PSBVMPersistentId -VCenterAddress $vCenterEndpoint -VCenterCredential $vCenterCredential -VMName sqlvm2

    #Then create a volume set named myvolSet for disks E and F on server sqlvm2 using FlashArray fa30.
    New-PsbVolumeSet -VolumeSetName myvolSet -ComputerAddress sqlvm2 -ComputerCredential $vmCredential -FlashArrayAddress fa30 -FlashArrayCredential $FlashArrayCredential -Path 'E:\,F:\' -VCenterAddress $vCenterEndpoint -vCenterCredential $vCenterCredential -VMName sqlvm2 -VMPersistentID $VMPID -VolumeType vvol

#>

function New-PsbVolumeSet {
    [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')]
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $VolumeSetName,

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

        [parameter(ParameterSetName= 'Credential_Physical', Mandatory = $true)]
        [parameter(ParameterSetName= 'Credential_Virtual', Mandatory = $true)]
        [pscredential]
        $ComputerCredential,

        [parameter(ParameterSetName= 'PSSession_Physical', Mandatory = $true)]
        [parameter(ParameterSetName= 'PSSession_Virtual', Mandatory = $true)]
        [System.Management.Automation.Runspaces.PSSession]
        $ComputerSession,

        [parameter(Mandatory = $true)]
        [string]
        $Path,

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

        [parameter(Mandatory = $true)]
        [string]
        $FlashArrayAddress,

        [parameter(Mandatory = $true)]
        [pscredential]
        $FlashArrayCredential,

        [parameter(ParameterSetName= 'Credential_Virtual', Mandatory = $true)]
        [parameter(ParameterSetName= 'Session_Virtual', Mandatory = $true)]
        [string]
        $VCenterAddress,

        [parameter(ParameterSetName= 'Credential_Virtual', Mandatory = $true)]
        [parameter(ParameterSetName= 'Session_Virtual', Mandatory = $true)]
        [pscredential]
        $VCenterCredential,

        [parameter(ParameterSetName= 'Credential_Virtual', Mandatory = $false)]
        [parameter(ParameterSetName= 'Session_Virtual', Mandatory = $false)]
        [string]
        $VMName,

        [parameter(ParameterSetName= 'Credential_Virtual', Mandatory = $false)]
        [parameter(ParameterSetName= 'Session_Virtual', Mandatory = $false)]
        [parameter(Mandatory = $false)]
        [string]
        $VMPersistentId,

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

        [parameter(Mandatory = $false)]
        [switch]
        $Force
    )
    return Invoke-Pfa2CmdletWrapper -FunctionName $MyInvocation.MyCommand -Parameters $MyInvocation.BoundParameters
}

<#
 .Synopsis
    Get volume set details.
 .Description
    Retrieves one or all volume sets on a FlashArray and returns volume set details
 .Parameter VolumeSetName
    Optional. Volume Set Name that identifies an existing Volume Set. If not specified, function will return details of all existing Volume Sets on the Flash Array.
 .Parameter FlashArrayAddress
    The FlashArray address
 .Parameter FlashArrayCredential
    FlashArray credentials
 .Example
    # Returns a list of all existing volume sets on flasharray fa30.
    Get-PsbVolumeSet -FlashArrayAddress fa30 -FlashArrayCredential (Get-Credential)
 .Example
    # Returns details of volume set myvolSet from flasharray fa30.
    Get-PsbVolumeSet -FlashArrayAddress fa30 -FlashArrayCredential $fa30 -VolumeSetName myvolSet
#>

function Get-PsbVolumeSet {
    [CmdletBinding()]
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $FlashArrayAddress,

        [parameter(Mandatory = $true)]
        [PSCredential]
        $FlashArrayCredential,

        [parameter(Mandatory = $false)]
        [string]
        $VolumeSetName
    )
    return Invoke-Pfa2CmdletWrapper -FunctionName $MyInvocation.MyCommand -Parameters $MyInvocation.BoundParameters
}

<#
 .Synopsis
    Removes a volume set from FlashArray.
 .Parameter VolumeSetName
    Volume Set Name that identifies an existing Volume Set to be removed from Flash Array.
 .Parameter FlashArrayAddress
    The FlashArray address
 .Parameter FlashArrayCredential
    FlashArray credentials
 .Example
    # Remove myvolSet metadata from flasharray fa30.
    Remove-PsbVolumeSet -FlashArrayAddress fa30 -FlashArrayCredential (Get-Credential) -VolumeSetName myvolSet
 .Example
    # Remove myvolSet metadata from flasharray fa30.
    Remove-PsbVolumeSet -FlashArrayAddress fa30 -FlashArrayCredential $FlashArrayCredential -VolumeSetName myvolSet
    # If desired, remove the Protection Group leveraging the dependent PureStoragePowerShellSDK2
    # First connect to the FlashArray
    $pfa30 = connect-pfa2array -endpoint fa30 -credential $FlashArrayCredential -IgnoreCertificateError
    # Get properties on the protection group (optional)
    Get-Pfa2ProtectionGroup -Array $pfa30 -Name psb-qsqlvm4
    # Destroy the protection group. Volumes do not need to be removed before destroying.
    Update-Pfa2ProtectionGroup -Array $pfa30 -Destroyed $true -Name psb-qsqlvm4
    # If you destroy the protection group by accident and want to recover, rerun the command with ‘-Destroyed’ set to false false. (optional)
    Update-Pfa2ProtectionGroup -Array $pfa30 -Destroyed $false -Name psb-qsqlvm4

#>

function Remove-PsbVolumeSet {
    [CmdletBinding()]
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $FlashArrayAddress,

        [parameter(Mandatory = $true)]
        [PSCredential]
        $FlashArrayCredential,

        [parameter(Mandatory = $true)]
        [string]
        $VolumeSetName
    )
    return Invoke-Pfa2CmdletWrapper -FunctionName $MyInvocation.MyCommand -Parameters $MyInvocation.BoundParameters
}

<#
 .Synopsis
    Retrieves a list of matching Protection Groups for a volume set
 .Description
    Search FlashArray for all of the Protection Groups that include all of the volumes that are a part of the specified volume set. It can be used to help determine which pgroup should be used when invoking a backup.
    If no Protection Groups exist, volume snapshots can be taken with the caveat that if there is more than 1 volume it is not guaranteed to be consistent without using a Protection Group snapshot. A Protection Group can be created using the -createpgroup parameter of Invoke-PsbSnapshotJob.
    If a single Protection Group exists you can declare it in the Invoke-PsbSnapshotJob cmdlet with the -PgroupName parameter.
    If more than one Protection Groups exist, you can enumerate their names with this cmdlet and choose the one you want, then declare it with the Invoke-PsbSnapshotJob cmdlet with the -PgroupName parameter.
    You can also let the Invoke-PsbSnapshotJob choose the Protection Group that has all volumes in the volume set, with the fewest number of other volumes with the -UseBestPgroupMatch parameter, as denoted in the “ExtraVolumesCount” column from this cmdlet.
 .Parameter VolumeSetName
    Volume Set Name that identifies an existing Volume Set on the Flash Array.
 .Parameter FlashArrayAddress
    The FlashArray address
  .Parameter FlashArrayCredential
    FlashArray credentials
 .Example
    Get-PsbVolumeSetProtectionGroup -FlashArrayAddress fa30 -FlashArrayCredential (Get-Credential) -VolumeSetName myvolSet
    Returns a list of matching pgroups for myvolSet on flasharray fa30.
#>

function Get-PsbVolumeSetProtectionGroup {
    [CmdletBinding()]
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $FlashArrayAddress,

        [parameter(Mandatory = $true)]
        [PSCredential]
        $FlashArrayCredential,

        [parameter(Mandatory = $true)]
        [string]
        $VolumeSetName
    )
    return Invoke-Pfa2CmdletWrapper -FunctionName $MyInvocation.MyCommand -Parameters $MyInvocation.BoundParameters
}

<#
 .Synopsis
    Get the list of drive letters available for mounting a snapshot.
 .Description
    Query the specified ComputerAddress to get a list of available drive letters.
    This information can be used in the Mount-PsbSnapshotSet cmdlet to mount a snapshot set to those drive letters using the Path parameter.
    If an available drive letter is in use as a mapped network drive, the drive letter will have to be unmapped from the share before the mounted disk is accessible.
 .Parameter ComputerAddress
    The name of the VM.
 .Parameter ComputerCredential
    The Credential for the VM.
 .Parameter ComputerSession
    The PSSession for the VM.
 .Example
    $session = new-pssession
    Get-PSBAvailableDrive -ComputerAddress $vmName -ComputerSession $session
#>

function Get-PSBAvailableDrive {
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $ComputerAddress,

        [parameter(ParameterSetName= 'Credential', Mandatory = $true)]
        [PSCredential]
        $ComputerCredential,

        [parameter(ParameterSetName= 'PSSession', Mandatory = $true)]
        [System.Management.Automation.Runspaces.PSSession]
        $ComputerSession
    )
    return Invoke-Pfa2CmdletWrapper -FunctionName $MyInvocation.MyCommand -Parameters $MyInvocation.BoundParameters
}

<#
 .Synopsis
    Get VM Persistent ID (or IDs if more than one VM has the same name) from vCenter.
    The VM friendly name in vCenter, the vCenter hostname, FQDN, or IP Address, and the vCenter credential are required parameters.
 .Description
    Get VM Persistent ID (or IDs if more than one VM has the same name) from vCenter.
    The VM Persistent ID is needed when creating a new volume set (New-PsbVolumeSet) and when invoking a snapshot (Invoke-PsbSnapshotJob) so that operations can succeed even if the VM is renamed in vCenter.
 .Parameter VCenterAddress
    The friendly name of the controlling VCenter previously configured with Add-PfaBackupCred
 .Parameter VCenterCredential
    The credential for the controlling VCenter previously configured with Add-PfaBackupCred
 .Parameter VMName
    VM name in vCenter
 .Example
    Get-PSBVMPersistentId -VMName $vmName -VCenterAddress $vCenterEndpoint -VCenterCredential $vCenterCredential
#>

function Get-PSBVMPersistentId {
    Param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $VCenterAddress,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [PSCredential] $VCenterCredential,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $VMName
    )
    return Invoke-Pfa2CmdletWrapper -FunctionName $MyInvocation.MyCommand -Parameters $MyInvocation.BoundParameters
}

################# WRAPPER #################
function Invoke-Pfa2CmdletWrapper {
    Param (
        $FunctionName,
        $Parameters
    )

    $params = ($Parameters.Keys | ForEach-Object { "-$_ '$($Parameters[$_])'" }) -join " "
    $FlashArrayAddress = $Parameters.FlashArrayAddress
    $FlashArrayCredential = $Parameters.FlashArrayCredential

    # log start and create basic objects
    Write-Log -Level INFO -FunctionName $FunctionName -Msg ($script:EnteringMsg + $params)

    Write-Progress -Activity $FunctionName -Status $script:VerCredMsg

    $workflowId = New-WorkflowID

    if ($Parameters['thread_id']){
        $thread_id = $Parameters['thread_id']

        # most operations do not expect a thread_id, we will add the parameter back if needed
        $Parameters.Remove('thread_id') | Out-Null
    }
    else {
        $thread_id = $FunctionName
    }

    # Create REST Client
    $RestClient = $null

    #"Get-PSBAvailableDrive", "Get-PSBVMPersistentId" are helper functions that do not talk to the FA
    if(-not (@("Get-PSBAvailableDrive", "Get-PSBVMPersistentId") -contains $FunctionName)){
        try{
            if ($null -eq $FlashArrayCredential) {
                ThrowErrorCode -ErrorCode $ErrorCode_MissingFACred
            }
            # throws exception if fails to connect
            $RestClient = New-PureRestClient -FAEndpoint $FlashArrayAddress -FACredential $FlashArrayCredential
        }
        catch{
            # catch issues with credentials and connection to the FA
            New-LogFatalErrorAndThrow  -FunctionName $FunctionName -ErrorMsg $_.Exception.Message -Exception $_
        }
        New-PhoneHomeWorkflowLogEntry -RestClient $RestClient -Event $script:BeginWorkflow -ID $workflowId -Name $FunctionName -thread_id $thread_id
        $Parameters['RestClient'] = $RestClient
        $Parameters.Remove("FlashArrayAddress") | Out-Null
        $Parameters.Remove("FlashArrayCredential") | Out-Null
    }

    $Parameters['FunctionName'] = $FunctionName
    $removeSessions = $false


    $result = $null
    try {
        # validate windows credentials
        if($parameters['ComputerAddress']){
            if($Parameters['ComputerSession']){
                if($parameters['ComputerAddress'] -ne $Parameters['ComputerSession'].ComputerName){
                    ThrowErrorCode -ErrorCode $ErrorCode_InvalidComputerSession
                }
                if ($Parameters['ComputerCredential']){
                    $Parameters.Remove("ComputerCredential") | Out-Null
                }
            }
            elseif ($Parameters['ComputerCredential']){
                $ComputerSession = New-RemoteSession -ComputerAddress $parameters['ComputerAddress'] -Credential $parameters['ComputerCredential'] -ErrorAction SilentlyContinue -ErrorVariable sdkError
                if(-not $ComputerSession){
                    ThrowErrorCode -ErrorCode $ErrorCode_InvalidComputerCred -innerException $sdkError[0].Exception
                }

                $Parameters.Remove("ComputerCredential") | Out-Null
                $Parameters['ComputerSession'] = $ComputerSession
                $removeSessions = $true
            }
            else{
                # TODO: review error
                ThrowErrorCode -ErrorCode $ErrorCode_InvalidComputerCred
            }
        }

        # validate vcenter credentials
        if($parameters['VCenterAddress']){
            # check if powerCLI is installed
            if(-not (Get-Module -ListAvailable VMware.PowerCLI)){
                ThrowErrorCode -ErrorCode $ErrorCode_MissingPowerCLI
            }

            if(-not $parameters['VCenterCredential']){
                ThrowErrorCode -ErrorCode $ErrorCode_MissingVcenterCreds
            }
            try{
                Connect-VCenter -VCenterAddress $parameters['VCenterAddress'] -Credential $parameters['VCenterCredential'] | Out-Null
            }
            catch{
                # will add powercli error to the exception
                ThrowErrorCode -ErrorCode $ErrorCode_InvalidVcenterCred -innerException $_.Exception
            }
        }

        # run actual operation
        Switch ($FunctionName) {
            'Invoke-PsbSnapshotJob'          {
                $Parameters['thread_id'] = $thread_id
                $Parameters['workflowId'] = $workflowId
                $result = Invoke-BackupOp @Parameters
            }
            'New-PsbVolumeSet'       {
                $result = Invoke-CreateVolumeSetOp @Parameters
            }
            'Mount-PsbSnapshotSet'              { $result = Invoke-MountOp               @Parameters }
            'Get-PsbSnapshotJobHistory'         { $result = Invoke-GetHistoryOp          @Parameters }
            'Remove-PsbSnapshotSet'             { $result = Invoke-RemoveOp              @Parameters }
            'Dismount-PsbSnapshotSet'           { $result = Invoke-DismountOp            @Parameters }
            'Get-PsbSnapshotSetMountHistory'    { $result = Invoke-GetMountHistoryOp     @Parameters }
            'Get-PsbVolumeSet'                  { $result = Invoke-GetVolumeSetOp        @Parameters }
            'Remove-PsbVolumeSet'               { $result = Invoke-RemoveVolumeSetOp     @Parameters }
            'Get-PsbVolumeSetProtectionGroup'   { $result = Invoke-GetVolumeSetPgroupsOp @Parameters }
            'Get-PSBAvailableDrive'             { $result = Invoke-GetAvailableDriveOp   @Parameters }
            'Get-PSBVMPersistentId'             { $result = Invoke-GetVMPersistentIdOp   @Parameters }
        }

    }
    catch
    {
        New-LogFatalErrorAndThrow -RestClient $RestClient -ID $workflowId -FunctionName $FunctionName -ErrorMsg $_.Exception.Message -Exception $_ -thread_id $thread_id
    }
    finally{
        if($removeSessions){

            if(Test-Path variable:ComputerSession){
                # Cleanup remote sessions
                Remove-PSSession -Session $ComputerSession -ErrorAction SilentlyContinue | Out-Null
            }
        }
    }

    # log completion
    Write-Progress -Activity $FunctionName -Status $script:OpFinishedMsg -Completed
    Write-Log -Level INFO -FunctionName $FunctionName -Msg $script:OpFinishedMsg
    if($RestClient){
        New-PhoneHomeWorkflowLogEntry -RestClient $RestClient -Event $script:CompleteWorkflow -ID $workflowId -Name $FunctionName -thread_id $thread_id
    }


    return $result

}

################# CMDLETS IMPLEMENTATION #################
function Invoke-BackupOp {
    [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')]
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $VolumeSetName,

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

        [parameter(Mandatory = $false)]
        [System.Management.Automation.Runspaces.PSSession]
        $ComputerSession,

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

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

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

        [parameter(Mandatory = $false)]
        [PSCredential]
        $VCenterCredential,

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

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

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

        [parameter(ParameterSetName='ExistingPG', Mandatory = $true)]
        [string]
        $PgroupName,

        [parameter(ParameterSetName='BestMatch', Mandatory = $true)]
        [switch]
        $UseBestPgroupMatch,

        [parameter(ParameterSetName='CreatePG', Mandatory = $true)]
        [switch]
        $CreatePgroup,

        [parameter(ParameterSetName='CreatePG', Mandatory = $true)]
        [string]
        $NewPgroupSuffix,

        [parameter(ParameterSetName='NoPgroup', Mandatory = $true)]
        [switch]
        $NoPgroup,

        [parameter(ParameterSetName='ExistingPG', Mandatory = $false)]
        [parameter(ParameterSetName='BestMatch', Mandatory = $false)]
        [switch]
        $ReplicateNow,

        [parameter(Mandatory = $true)]
        [string]
        $thread_id,

        [parameter(Mandatory = $true)]
        [string]
        $workflowId,

        [parameter(Mandatory = $true)]
        [string]
        $FunctionName,

        [parameter(Mandatory = $true)]
        $RestClient,

        [parameter(Mandatory = $false)]
        [switch]
        $Force
    )

    if ($Force){
        $ConfirmPreference = 'None'
    }

    Write-Progress -Activity $FunctionName -Status $validateVolSetMsg
    $volset = Invoke-CreateVolumeSetOp -FunctionName "Invoke-CreateVolumeSetOp" `
        -VolumeSetName $VolumeSetName `
        -ComputerAddress $ComputerAddress `
        -ComputerSession $ComputerSession `
        -Path $Path `
        -VolumeType $VolumeType `
        -RestClient $RestClient `
        -VCenterAddress $VCenterAddress `
        -VCenterCredential $VCenterCredential `
        -VMName $VMName `
        -VMPersistentId $VMPersistentId
    $affectedVolumes = $volset.Resources

    Write-Progress -Activity $FunctionName -Status $validatePgroupMsg
    $results = @()
    $useReplicateNow = $false

    $newHistoryId = (Get-Date -UFormat %s).replace(".", "")

    if ($NoPgroup) {
        ### uses volume snapshots instead of PG

        if($affectedVolumes.Count -gt 1){
            # Asks the user for confirmation to use non pgroup snapshots for multiple volumes (can result in inconsistent snapshots)
            if ($PSCmdlet.ShouldProcess($noPgroupDescMsg, $noPgroupWarnMsg, $noPgroupTitleMsg)){
                New-PhoneHomeWorkflowLogEntry -RestClient $RestClient -Event $script:ContextWorkflow -ID $workflowId -Name $FunctionName -Context $noPgroupDescMsg -thread_id $thread_id
            }
            else {
                # non-pgroup backup aborted by user
                New-PhoneHomeWorkflowLogEntry -RestClient $RestClient -Event $script:AbortWorkflow -ID $workflowId -Name $FunctionName -thread_id $thread_id
                Write-Log -Level INFO -FunctionName $FunctionName -Msg $userAbortedMsg
                return $false
            }
        }
        # no confirmation needed for pgroup snaps
        $ConfirmPreference = "None"
        $FinalPgroupName = $null

        # taking volumes snapshots
        Write-Progress -Activity $FunctionName -Status $takingSnapshotMsg
        $volSnaps = New-Pfa2VolumeSnapshot -Array $RestClient -SourceIds $affectedVolumes.Id -Suffix "$($SDKPrefix)-$($newHistoryId)" -ErrorAction Stop

        # tag snapshots as non-pgroup snapshots
        Set-Pfa2VolumeSnapshotTagsBatch -Array $RestClient -ResourceIds $volSnaps.Id -TagCopyable $false -TagNamespace $NamespaceMeta -TagKey "$([PureTagKeys]::SnapType)" -TagValue 'NoPgroup' | Out-Null
        $results = $volSnaps
    }
    else {
        ### uses pgroup snapshots

        # no confirmation needed for pgroup snaps
        $ConfirmPreference = "None"

        # process pgroup option
        $FinalPgroupName = ProcessPgroupOption -RestClient $RestClient -affectedVolumes $affectedVolumes -PgroupName $PgroupName -CreatePgroup:$CreatePgroup -UseBestPgroupMatch:$UseBestPgroupMatch -NewPgroupSuffix $NewPgroupSuffix
        $pgroupObj = Get-Pfa2ProtectionGroup -Array $RestClient -Name $FinalPgroupName

        # will use replicate_now param if the user enable replicate now switch and the pgroup used has a target set for replication
        # using replicate now with retention police can cause snapshots to disappear from the source array
        $useReplicateNow = $replicateNow -and ($pgroupObj.TargetCount -gt 0)

        # take a pgroup snapshot
        Write-Progress -Activity $FunctionName -Status $takingSnapshotMsg
        $results = New-Pfa2ProtectionGroupSnapshot -Array $RestClient -SourceNames $pgroupObj.Name -ApplyRetention $true -Suffix "$($SDKPrefix)-$($newHistoryId)" -ErrorAction Stop

        # get volume snapshots from pgroup snapshot
        $volSnaps = Get-Pfa2VolumeSnapshot -Array $RestClient -Filter "name='$($results.Name).*'"

        # tag volume snapshots as part of a pgroup snapshot
        Set-Pfa2VolumeSnapshotTagsBatch -Array $RestClient -ResourceIds $volSnaps.Id -TagCopyable $false -TagNamespace $NamespaceMeta -TagKey "$([PureTagKeys]::SnapType)" -TagValue 'Pgroup' | Out-Null

        # filter volume snapshots to only invlude volumes that were part of the job
        $volSnaps = $volSnaps | where-object {$_.Source.Id -in $affectedVolumes.Id}
    }

    # tag history id to volume snapshots
    Set-Pfa2VolumeSnapshotTagsBatch -Array $RestClient -ResourceIds $volSnaps.Id -TagCopyable $false -TagNamespace $NamespaceMeta -TagKey "$([PureTagKeys]::HistoryId)" -TagValue "$($VolumeSetName)#$($newHistoryId)" | Out-Null

    #replicate pgroup snapshot
    if($useReplicateNow){
        Write-Progress -Activity $FunctionName -Status $replicatingSnapshotMsg
        $rep_res = New-Pfa2RemoteProtectionGroupSnapshot -Name $results.Name -Array $RestClient -ErrorAction SilentlyContinue
        if(-not $rep_res){
            $msg = GetErrorMessage -ErrorCode $ErrorCode_FailToReplicate
            New-PhoneHomeWorkflowLogEntry -RestClient $RestClient -Event $script:ErrorWorkflow -ID $workflowId -Name $FunctionName -ErrorMsg $msg -thread_id $thread_id
            Write-Error $msg
        }
    }

    $msg = $results | Format-List | Out-String
    Write-Log -Level INFO -FunctionName $FunctionName -Msg $Msg

    return New-HistoryObject -HistoryId "$($VolumeSetName)#$($newHistoryId)" -VolumeSet $volset -Snapshots $volSnaps.Name -CreationDate $volSnaps[0].Created -PgroupName $FinalPgroupName -SDKSnap $true
}

function Invoke-GetHistoryOp {
    Param (
        [parameter(ParameterSetName='VolSet', Mandatory = $true)]
        [string]
        $VolumeSetName,

        [parameter(ParameterSetName="All", Mandatory=$true)]
        [switch]
        $All,

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

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

        [parameter(Mandatory = $false)]
        [int]
        $Limit=10,

        [parameter(Mandatory = $true)]
        [string]
        $FunctionName,

        [parameter(Mandatory = $true)]
        $RestClient
    )

    Write-Progress -Activity $FunctionName -Status $LoadTagsMsg

    $history = @()
    # gets pgroup snapshots
    $history += RetrievePgroupSnapshots -RestClient $RestClient -VolumeSetName $VolumeSetName -IncludeNonSDKSnapshots:$IncludeNonSDKSnapshots -UseLocalTime:$UseLocalTime -Limit $Limit

    # get non-pgroup snapshots
    $history += RetrieveNonPgroupSnapshots -RestClient $RestClient -VolumeSetName $VolumeSetName -UseLocalTime:$UseLocalTime -Limit $Limit

    return $history | Sort-Object -descending {$_.CreationDate} | Select-Object -First $limit
}

function Invoke-RemoveOp {
    [cmdletbinding(SupportsShouldProcess=$true, ConfirmImpact='High')]
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $HistoryId,

        [parameter(Mandatory = $true)]
        [string]
        $FunctionName,

        [parameter(Mandatory = $true)]
        $RestClient,

        [parameter(Mandatory = $false)]
        [switch]
        $Force
    )

    if ($Force){
        $ConfirmPreference = 'None'
    }

    $HistoryItem = Get-HistoryObject -RestClient $RestClient -HistoryId $HistoryId

    if (-not $HistoryItem.SDKSnap) {
        ThrowErrorCode -ErrorCode $ErrorCode_CantDelNonSDKSnaps
    }

    if ($HistoryItem.ProtectionGroup) {
        $snapName = $HistoryItem.Snapshots | select-object -first 1
        $snapNameParts = $snapName.Split(".")
        if($snapNameParts.Count -ne 3){
            ThrowErrorCode -ErrorCode $ErrorCode_CantParseSnapshot
        }
        $pgroupSnapName = "$($snapNameParts[0]).$($snapNameParts[1])"
        Remove-Pfa2ProtectionGroupSnapshot -Array $RestClient -Name $pgroupSnapName -ErrorAction SilentlyContinue | Out-Null
    }
    else {
        foreach ($snap in $HistoryItem.Snapshots){
            Remove-Pfa2VolumeSnapshot -Array $RestClient -Name $snap -ErrorAction SilentlyContinue | Out-Null
        }
    }
    return $true
}

function Invoke-MountOp {
    [CmdletBinding()]
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $HistoryId,

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

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

        [parameter(Mandatory = $false)]
        [System.Management.Automation.Runspaces.PSSession]
        $ComputerSession,

        [Parameter(Mandatory = $false)]
        [string]
        $VMName,

        [Parameter(Mandatory = $false)]
        [string]
        $VMPersistentId,

        [Parameter(Mandatory = $false)]
        [string]
        $VCenterAddress,

        [Parameter(Mandatory = $false)]
        [PSCredential]
        $VCenterCredential,

        [Parameter(Mandatory = $false)]
        [string]
        $DatastoreName,

        [parameter(Mandatory = $true)]
        [string]
        $FunctionName,

        [parameter(Mandatory = $true)]
        $RestClient,

        [parameter(Mandatory = $false)]
        [int]
        $DiskTimeout = 5,

        [parameter(Mandatory = $false)]
        [int]
        $DiskLoadRetries = 5
    )

    $FlashArrayAddress = $RestClient.ArrayName
    # get history object from history Id
    $HistoryItem = Get-HistoryObject -RestClient $RestClient -HistoryId $HistoryId
    if(-not $HistoryItem){
        ThrowErrorCode -ErrorCode $ErrorCode_HistoryNotFound -params @($HistoryId)
    }

    # get virtualization info if rdm or vvol
    $VCenterAddress, $ComputerAddress, $MountVM, $hostports = Get-VirtualizationInfo -HistoryItem $HistoryItem -VCenterAddress $VCenterAddress -VCenterCredential $VCenterCredential -Computer $ComputerAddress -VMName $VMName -VMPersistentId $VMPersistentId

    # get drive letters for mount
    # TODO: need to update to work with mount point - TMAN-18554
    $Paths = $Path -split ','

    # Validate destination, which could be a new drive letter or an existing mount point
    $Paths = ValidatePath -Paths $Paths -ComputerAddress $ComputerAddress -ComputerSession $ComputerSession -DiskCount $HistoryItem.Snapshots.Count

    $pathsToStore = @()
    foreach ($p in @($Paths)) {
        if ($p.Length -eq 1){
            $pathsToStore += "$($p):"
        }
        else{
            $pathsToStore += $p
        }
    }

    # will throw exception early on if tag too long or if it has invalid chars
    CheckMountedVolumeTags -ComputerAddress $ComputerAddress -Paths $pathsToStore -VCenterAddress $VCenterAddress -VMPersistentId $VMPersistentId -VolumeType $HistoryItem.VolumeType

    # if it's not a virtualized environment, get hostports directly from computer
    if (-not $VCenterAddress) {
        $FAIqns = @(Get-Pfa2Port -Array $RestClient  | Where-Object {$_.iqn}).Iqn
        Write-Log -Level INFO -FunctionName $FunctionName -Msg "Flash Array IQNs: $($FAIqns -join ', ')"
        $hostports = Get-InitiatorPorts -session $ComputerSession -iqns $FAIqns
    }

    # if not vvols, get matching hosts/hostgroups for the hostports found
    if ("VVOL" -ine $HistoryItem.VolumeType) {
        # find matching hosts/hostgroups to connect mounted volumes to
        $matchingHosts = Get-MatchingHosts -hostports $hostports -RestClient $RestClient
        if (-not $matchingHosts) {
            # We assume the host already exists
            ThrowErrorCode -ErrorCode $ErrorCode_HostNotFound -params @($hostports, $FlashArrayAddress)
        }
        $matchingHGs = Get-MatchingHostGroups -matchingHosts $matchingHosts
    }

    # get snapshot objects for the history item
    $snapshots = $HistoryItem.Snapshots | Get-Pfa2VolumeSnapshot -Array $RestClient

    # create volumes from snapshots included on history entry
    $newVolumesSerials = @()
    $diskIds = @()

    $newMountId = (Get-Date -UFormat %s).replace(".", "")
    try{
        foreach ($snapshot in $snapshots) {
            if("VVOL" -ieq $HistoryItem.VolumeType) {
                # create vvol on vm
                $disk,$newVolume = $MountVM | Copy-PfaSnapshotToNewVvolVmdk -SnapshotName $snapshot.Name -RestClient $RestClient -DatastoreName $DatastoreName
                # validate vvol
                if (-not ($disk.PSObject.Properties.Name -contains "ExtensionData")) {
                    ThrowErrorCode -ErrorCode $ErrorCode_ExtentionDataMissing -params @($disk.gettype())
                }
                # get volume serial from vvol uuid
                $vmHardDiskUuid = $disk.ExtensionData.Backing.uuid | foreach {$_.replace(' ','').replace('-','')}

                #get volume serial
                $diskIds += $vmHardDiskUuid
                $newVolumesSerials += $newVolume.Serial
            }
            else {
                # create vol copy from snap
                $newVolName = NewVolumeName -sourceId $snapshot.Source.Id -uniqueId $newMountId
                $EV = $null
                $newVolume = New-Pfa2Volume -Array $RestClient -Name $newVolName -SourceName $snapshot.Name -ErrorAction SilentlyContinue -ErrorVariable EV
                if ($EV) {
                    ThrowErrorCode -ErrorCode $ErrorCode_VolumeCreateFailed -innerException $EV.Exception
                }

                # connect volume to host on FA
                Connect-VolToHost -volumeName $newVolume.Name -matchingHosts $matchingHosts -matchingHGs $matchingHGs -RestClient $RestClient

                # get volume serial
                $diskIds += $newVolume.Serial
                $newVolumesSerials += $newVolume.Serial
            }
        }


        if("RDM" -ieq $HistoryItem.VolumeType) {
            # Connect RDM volumes to vm on vcenter
            Connect-PfaVMHardDisk -VolumeSerials $newVolumesSerials -VMName $MountVM.Name -VCenterAddress $VCenterAddress -VCenterCredential $VCenterCredential `
            -RestClient $RestClient -VMPersistentId $MountVM.PersistentId -DatastoreName $DatastoreName | Out-Null
        }

        #generate mount id
        $MountId = "$($newMountId)#$($HistoryID)"

        # tag newly created volumes with mount history
        $volumes = New-MountedVolumesTags -MountId $MountId -ComputerAddress $ComputerAddress -Paths $pathsToStore -VCenterAddress $VCenterAddress -VMPersistentId $MountVM.PersistentId -MountVolumesSerials $newVolumesSerials -VolumeType $HistoryItem.VolumeType -RestClient $RestClient

        # expose newly created volumes to the selected drive letters
        Expose-Disks -session $ComputerSession -Paths $Paths -Serials $diskIds -VolumeType $HistoryItem.VolumeType -DiskTimeout $DiskTimeout -DiskLoadRetries $DiskLoadRetries

        # create mount object to be returned
        $mountObject = New-MountObject -MountId $newMountId -HistoryId $HistoryId -Volumetype $HistoryItem.VolumeType -Computer $ComputerAddress -Paths ($pathsToStore -Join ',') -vCenter $VCenterAddress -VMId $MountVM.PersistentId -Resources $volumes
    }
    catch{
        $Msg = "Mount operation failed. Cleaning up volume copies."
        Write-Progress -Activity $FunctionName -Status $Msg
        Write-Log -Level WARN -FunctionName $FunctionName -Msg "$Msg [$($newVolumesSerials -join ",")]"

        # unexpose disks on $diskIds
        if($diskIds){
            CleanupMountByDiskId -ComputerSession $ComputerSession -RestClient $RestClient -DiskIds $diskIds -newVolumesSerials $newVolumesSerials`                    -VCenterAddress $VCenterAddress -VCenterCredential $VCenterCredential -VMName $MountVM.Name -FunctionName $FunctionName -ErrorAction Continue
        }

        # destroy volumes on $newvolumeSerials
        if($newVolumesSerials){
            CleanupMountByVolSerial -RestClient $RestClient -newVolumesSerials $newVolumesSerials -VolumeType $HistoryItem.VolumeType -VCenterAddress $VCenterAddress`                 -VCenterCredential $VCenterCredential -MountVM $MountVM -FunctionName $FunctionName -ErrorAction Continue
        }

        throw
    }

    $Msg = "Snapshot successfully mounted to drive(s) $PathStr. Mount ID: $($MountId)."
    Write-Progress -Activity $FunctionName -Status $Msg
    Write-Log -Level INFO -FunctionName $FunctionName -Msg $Msg

    #TODO: cleanup if anything fails - TMAN-18555
    return $mountObject
}

function Invoke-DismountOp{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $MountId,

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

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

        [Parameter(Mandatory = $false)]
        [string]
        $VCenterAddress,

        [Parameter(Mandatory = $false)]
        [PSCredential]
        $VCenterCredential,

        [parameter(Mandatory = $true)]
        [string]
        $FunctionName,

        [parameter(Mandatory = $true)]
        $RestClient
    )

    # Gets mount history details from Mount ID
    $History = Get-MountObject -RestClient $RestClient -MountId $MountId

    # Logs dismount details
    $msg = "History:[$($History.Id)] Drive(s): [$($History.Paths -Join ',')] on Host: [$($History.Computer)]"
    Write-Log -Level INFO  -FunctionName $FunctionName -Msg $msg

    # validate basic mount info
    if ([String]::IsNullOrWhiteSpace($History.Computer)) {
        ThrowErrorCode -Error $Script:DismountPathNull
    }
    if (-not $History.Paths -or $History.Paths.Count -eq 0) {
        ThrowErrorCode -Error $Script:DismountComputerNull
    }

    $mountedPaths = @()
    foreach ($p in @($History.Paths)){
        if ($p.Length -eq 2){
            # it's a drive letter like 'E:'
            $mountedPaths += "$($p[0])"
        }
        else{
            # it's a mount point like 'C:/mountpoint'
            $mountedPaths += $p
        }
    }

    # gets virtualization info
    if($History.vCenter) {
        # TODO: Do we still need vCenter in History?
        $EsxiDetails = Get-ESXiDetails -VCenterAddress $VCenterAddress -VCenterCredential $VCenterCredential -VMPersistentId $History.VMId
        $VM = $EsxiDetails.VM
    }

    if ($History.Computer -ne $ComputerAddress) {
        ThrowErrorCode -ErrorCode $ErrorCode_MissingCreds -params @($History.Computer)
    }
    $mountedVolumes = $History.Resources | Get-Pfa2Volume -Array $RestClient

    $success = $true

    if ("VVOL" -ieq $History.VolumeType) {
        #vvol
        foreach ($SinglePath in $mountedPaths) {
            # gets vvol disk info
            Write-Log -Level INFO  -FunctionName $FunctionName -Msg "Removing Disk $SinglePath..."
            $verboseDisk = Get-PfaVerboseDiskInfo -Session $ComputerSession -RestClient $RestClient -Path $SinglePath -VCenterAddress $VCenterAddress -VCenterCredential $VCenterCredential -VMName $VM.Name
            if ($null -eq $verboseDisk[$SinglePath]) {
                # vvol was probably removed manually, log error but continues
                $Msg = "Could not find matching disk to Drive:$($SinglePath) in VM: $($VM.Name)"
                Write-Log -Level ERROR -FunctionName $FunctionName -Msg $Msg
                Write-Error $Msg
                $success = $false
                continue
            }
            if(-not $verboseDisk[$SinglePath].ArrayVolumeInfo.VolumeSerial -in $mountedVolumes.Serial){
                ThrowErrorCode -ErrorCode $ErrorCode_MountVolsSerialError -params @($verboseDisk[$SinglePath].ArrayVolumeInfo.VolumeSerial,$SinglePath, $VM.Name, ($mountedVolumes.Serial -Join ';'))
            }

            # unexpose vvol from VM
            Write-Log -Level INFO  -FunctionName $FunctionName -Msg "Unexposing $SinglePath"

            $success = $success -and (Unexpose-Disks -Session $ComputerSession -Paths $SinglePath)

            # destroy vvol
            $verboseDisk[$SinglePath].VMWareInfo.VMDisk | Remove-HardDisk -DeletePermanently -Confirm:$false
        }
    }
    else {
        # take disks offline
        $success = Unexpose-Disks -Session $ComputerSession -Paths $mountedPaths -VolumeSerialNumbers @($mountedVolumes.Serial)

        foreach ($Volume in @($mountedVolumes)) {
            # remove RDM volumes from VM
            if ("RDM" -ieq $History.VolumeType) {
                Remove-PfaVMHardDisk -VolumeSerial $Volume.Serial -VMName $($VM.Name) -VMPersistentId $($VM.PersistentId) -VCenterAddress $VCenterAddress -VCenterCredential $VCenterCredential
            }

            # remove all host/hostgroup connections
            Disconnect-VolHosts -volumeName $Volume.Name -RestClient $RestClient

            # destroy vol
            Remove-Pfa2Volume -Array $RestClient -Name $Volume.Name
        }
    }
    return $success
}

function Invoke-GetMountHistoryOp {
    [CmdletBinding()]
    Param (
        [parameter(Mandatory = $false)]
        [string]
        $HistoryId,

        [parameter(Mandatory = $true)]
        [string]
        $FunctionName,

        [parameter(Mandatory = $true)]
        $RestClient
    )

    # get mounted volumes by Mount Id
    # each mountid will be returned once per volume mounted
    # group results by mount id so we can gather the volumes for each mount
    $MountIds = Get-Pfa2VolumeTag -Array $RestClient -Namespaces $NamespaceMount -Filter "key='$([PureTagKeys]::MountId)'" -ResourceDestroyed:$false | Group-Object -Property {$_.Value} #### REST CALL ####

    # if user selected a history Id, filter results to only include mountIDs for that history
    if ($historyId) {
        $MountIds = $MountIds | Where-object {$_.Name.EndsWith("#$($HistoryID)")}
    }

    $mountHistory = @()

    foreach ($group in $MountIds) {
        $parts = $group.Name -Split '#'

        # id format: MountId#HistoryId
        # HistoryId: volset#guid
        if ($parts.Count -ne 3) {
            #invalid ids
            continue
        }

        # parse Ids
        $MountId = $parts[0]
        $HistoryId = "$($parts[1])#$($parts[2])"

        # get mounted volumes list
        $mountedVolumes = $group.Group.Resource

        # get metadata for each volume in mount entry
        $metadata = Get-Pfa2VolumeTag -Array $RestClient -Namespaces $NamespaceMount -Filter "key='$([PureTagKeys]::Metadata)'" -ResourceIds $mountedVolumes.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
            continue
        }

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

    return $mountHistory
}

function Invoke-CreateVolumeSetOp {
    [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')]
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $VolumeSetName,

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

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

        [parameter(Mandatory = $true)]
        [string]
        $Path,

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

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

        [parameter(Mandatory = $false)]
        [PSCredential]
        $VCenterCredential,

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

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

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

        [parameter(Mandatory = $true)]
        [string]
        $FunctionName,

        [parameter(Mandatory = $true)]
        $RestClient,

        [parameter(Mandatory = $false)]
        [switch]
        $Force
    )

    if ($Force){
        $ConfirmPreference = 'None'
    }

    if (-not (ValidatePureObjectName -Name $VolumeSetName)) {
        ThrowErrorCode -ErrorCode $ErrorCode_PurityNameRequirements -params @("VolumeSetName",$VolumeSetName)
    }

    # error out if tag already exist
    Write-Progress -Activity $FunctionName -Status $LoadTagsMsg
    $volumeSetTags = Get-Pfa2VolumeTag -Array $RestClient -Namespaces $NamespaceVolSet -Filter "Key=""$VolumeSetName"""

    $ValidatedVolumeType = ValidateVolumeType -VCenterAddress $VCenterAddress -VolumeType $VolumeType

    if (-not [String]::IsNullOrEmpty($VCenterAddress)) {
        # caller supplied VCenterAddress - our SQL Server is on a VM
        if ($null -eq $VCenterCredential) {
            ThrowErrorCode -ErrorCode $ErrorCode_MissingVcenterCreds
        }
        $VCenter = Connect-VCenter -VCenterAddress $VCenterAddress -Credential $VCenterCredential

        try {
            $VM = Get-SingleVMOnly -Name $VMName -VMPersistentId $VMPersistentId -Server $VCenter
            # Get the current name and Id from the VM we found.
            # Handles the case where the user supplied just the name or id,
            # or if they gave both, but the name doesn't coorespond to the Id.
            $VMName = $VM.Name
            $VMPersistentId = $VM.PersistentId
        } catch [VMware.VimAutomation.Sdk.Types.V1.ErrorHandling.VimException.VimException] {
            if ($_.Exception.PSObject.Properties.Name -contains "ErrorCategory" -and $_.Exception.ErrorCategory -eq "ObjectNotFound") {
                ThrowErrorCode -ErrorCode $ErrorCode_VmMissingOnVcenter -params @((Get-VmNameToString $VMName $VMPersistentId), $VCenterAddress) -innerException $_.Exception
            }
            throw
        }
    }


    Write-Progress -Activity $FunctionName -Status $GetCorVolMsg
    # gather volumes list
    if($VolumeType -eq "physical"){
        $affectedVolumes = Get-PfaVerboseDiskInfo -Session $ComputerSession -RestClient $RestClient -Path $Path
    }
    else{
        $affectedVolumes = Get-PfaVerboseDiskInfo -Session $ComputerSession `
        -RestClient $RestClient -Path $Path -VCenterAddress $VCenterAddress -VCenterCredential $VCenterCredential -VMName $VMName
    }

    if (-not $SkipValidation) {
        # Validate all volumes are of the same type specified in the config
        foreach ($diskPath  in $affectedVolumes.Keys) {
            $disk = $affectedVolumes[$diskPath]
            if (-not $disk.ComputerDiskInfo){
                # drive letter/path did not exist on target computer
                ThrowErrorCode -ErrorCode $ErrorCode_InvalidPath -params @($diskPath)
            }
            $diskVolumeType = $disk.VolumeType
            if ($ValidatedVolumeType -ne $diskVolumeType){
                # disk type did not match type specified
                ThrowErrorCode -ErrorCode $ErrorCode_InvalidVolType -params @($diskPath,$diskVolumeType)
            }
        }
    }

    # if volumeSetTags already existed, a volume set with this name already exists on the FA, check if they are the same
    if($volumeSetTags.Count -gt 0){
        $areVolumeSetsDifferent = -not (CheckSameListsOfUniqueElements -List1 $volumeSetTags.Resource.Id -List2 $affectedVolumes.Values.ArrayVolumeInfo.VolumeId)
        # if volume sets are different, check if user wants to proceed
        if ($areVolumeSetsDifferent -and -not ($PSCmdlet.ShouldProcess($vSetExistsDescMsg, $vSetExistsWarnMsg, $vSetExistsTitleMsg))){
            ThrowErrorCode -ErrorCode $ErrorCode_userAbort
        }

        # removes old volume set
        Invoke-RemoveVolumeSetOp -FunctionName "Invoke-RemoveVolumeSetOp" -VolumeSetName $VolumeSetName -RestClient $RestClient | Out-Null
    }
    $ConfirmPreference = "None"

    # tag volumes
    Write-Progress -Activity $FunctionName -Status $TagVolsMsg

    # TODO: any problems using # as a delimiter, better options?
    # verify that values do not contain delimiter
    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 )}

    foreach ($diskPath  in $affectedVolumes.Keys) {
        # note: tag values can have up to 256 character
        # VolCount: 1 to 3 chars
        # VolType: 3 or 8 chars
        # Delimiters: 5 chars
        # Remaining: 240Chars / 4 = ~60 chars per field
        $envTagVal = "$($affectedVolumes.Count)#$($ValidatedVolumeType)#$($ComputerAddress)#$($diskPath)#$($VCenterAddress)#$($VMPersistentId)"
        if ($envTagVal.Length -gt 256) {
            ThrowErrorCode -ErrorCode $ErrorCode_EnvTagTooLong -params @($envTagVal)
        }

        $disk = $affectedVolumes[$diskPath]
        $envTag = Set-Pfa2VolumeTagBatch -Array $RestClient -ResourceIds $disk.ArrayVolumeInfo.VolumeId -TagCopyable $false -TagNamespace $NamespaceVolSet -TagKey $VolumeSetName -TagValue $envTagVal
        if (-not $envTag) { ThrowErrorCode -ErrorCode $ErrorCode_FailToSetMetaTag }
    }

    $volumes = @()
    foreach ($vol in $affectedVolumes.Values.ArrayVolumeInfo) {
        $volumes += @{
            Id = $vol.VolumeId
            Name = $vol.VolumeName
        }
    }

    return New-VolumeSetObject -Name $VolumeSetName -VolumeType $ValidatedVolumeType -Computer $ComputerAddress -Paths $affectedVolumes.Keys -vCenter $VCenterAddress -VMId $VMPersistentId -Resources $volumes
}

function Invoke-GetVolumeSetOp {
    [CmdletBinding()]
    Param (
        [parameter(Mandatory = $false)]
        [string]
        $VolumeSetName,

        [parameter(Mandatory = $true)]
        [string]
        $FunctionName,

        [parameter(Mandatory = $true)]
        $RestClient
    )

    Write-Progress -Activity $FunctionName -Status $LoadTagsMsg
    if ($VolumeSetName) {
        $volumeSetTags = Get-Pfa2VolumeTag -Array $RestClient -Namespaces $NamespaceVolSet -Filter "Key=""$VolumeSetName"""
    }
    else {
        $volumeSetTags = Get-Pfa2VolumeTag -Array $RestClient -Namespaces $NamespaceVolSet
    }

    Write-Progress -Activity $FunctionName -Status $BldVolSetMsg
    $volumeSetNames = $volumeSetTags.Key | Sort-Object | Get-Unique

    $volumeSets = @()
    foreach ($name in $volumeSetNames) {
        $volumeTags = $volumeSetTags   | where-object {$_.Key -eq $name}
        $volumeSets += ParseMetadataTags -Tags $volumeTags
    }

    Write-Progress -Activity $FunctionName -Status $OpFinishedMsg -Completed

    return $volumeSets
}

function Invoke-RemoveVolumeSetOp {
    [CmdletBinding()]
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $VolumeSetName,

        [parameter(Mandatory = $true)]
        [string]
        $FunctionName,

        [parameter(Mandatory = $true)]
        $RestClient
    )

    Write-Progress -Activity $FunctionName -Status $LoadTagsMsg

    $volumeSetTags = Get-Pfa2VolumeTag -Array $RestClient -Namespaces $NamespaceVolSet -Filter "Key=""$VolumeSetName"" " -ErrorAction Stop
    $resources = $volumeSetTags.Resource.Id

    Write-Progress -Activity $FunctionName -Status $RemMetaMsg
    # TODO: check for confirmation?
    Remove-Pfa2VolumeTag -Array $RestClient -ResourceIds $resources -Namespaces $NamespaceVolSet -Keys @("$VolumeSetName") -ErrorAction Stop | Out-Null

    Write-Progress -Activity $FunctionName -Status $ -Completed
}

function Invoke-GetVolumeSetPgroupsOp {
    [CmdletBinding()]
    Param (
        [parameter(Mandatory = $true)]
        [string]
        $VolumeSetName,

        [parameter(Mandatory = $true)]
        [string]
        $FunctionName,

        [parameter(Mandatory = $true)]
        $RestClient
    )

    Write-Progress -Activity $FunctionName -Status $LoadTagsMsg

    $volumeSetTags = Get-Pfa2VolumeTag -Array $RestClient -Namespaces $NamespaceVolSet -Filter "Key=""$VolumeSetName"" "

    Write-Progress -Activity $FunctionName -Status $FindMatPgMsg
    $matchingPgroups = Get-MatchingPgroups -RestClient $RestClient -VolumesNames $volumeSetTags.Resource.name

    Write-Progress -Activity $FunctionName -Status $OpFinishedMsg

    return $matchingPgroups
}


# ExportedUtils
Export-ModuleMember -Function Get-PSBAvailableDrive
Export-ModuleMember -Function Get-PSBVMPersistentId

# Volume set
Export-ModuleMember -Function New-PsbVolumeSet
Export-ModuleMember -Function Get-PsbVolumeSet
Export-ModuleMember -Function Remove-PsbVolumeSet
Export-ModuleMember -Function Get-PsbVolumeSetProtectionGroup

# Main Workflow
Export-ModuleMember -Function Invoke-PsbSnapshotJob
Export-ModuleMember -Function Get-PsbSnapshotJobHistory
Export-ModuleMember -Function Remove-PsbSnapshotSet
Export-ModuleMember -Function Mount-PsbSnapshotSet
Export-ModuleMember -Function Dismount-PsbSnapshotSet
Export-ModuleMember -Function Get-PsbSnapshotSetMountHistory
# SIG # Begin signature block
# MIIn+AYJKoZIhvcNAQcCoIIn6TCCJ+UCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU3i5FqVAAEDPKpxOcTVrl8XxF
# xgqggiEoMIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG9w0B
# 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
# FML6G2R31XMzInsWO3nuJ54Ca24HMA0GCSqGSIb3DQEBAQUABIICAGvi/z0/0irD
# oojBnE6I/tB9DMA/HKR9P5D+zOuwlcc9VN4s3AThSTBxo0VX2fW73ICeJNgco9Es
# QqwL6yhXBCNGPv17jQsp0sFkxZ0/1JP+KIIoHND5rX2im6XHWx9Smr6NKLDq3ayZ
# c5LiN1UNMZAOFOhDaIZsCR5NpYLsjypOG3K1otkKH1r11hKpyjpEp+Vhw1o7IdkO
# i1rifU+s36i49HI/DB4uJH+g2CwJ59hlrsAuDc+6y2EO4/hFQHl79kPw6Q9boI4n
# Ae0SNVhxBapBzTRPK0l3clnR/hdovjQn7l5be0fjt2fzz7JQcdjo+dec5plWSEWn
# p+YURH5himOUbORyYKMiH5ahD05XBezyqNs+c6yQsCSiF28MfyieBHPh+pkAg39E
# yIBqjtHGzmEYQiVIOxpA/O+hMwcSIJADNl1RNYFwQzP6UuNG+J7WwVcFUIW4e2Iw
# J+WHb9L2heiWiZaDnccZ35x8QS+DfaYJ5q4CQ9fUDziTC7Jb8d17aT6TG5hi2bfA
# UZa0pPg1orpPLHuHKrSi+aYVu8Lz8LEViERgeT45eNRx1X42RvTNn+pdwrCEN3RG
# lpHDA7t7uHTKTTG37CpFXf2CR4BxYz05qI7ZC0rgHLdUdf2/MfQOiecPxgrzxgNA
# pBKlGbLTx0PqExXmTuY+9xhcZpAW0NWnoYIDIDCCAxwGCSqGSIb3DQEJBjGCAw0w
# ggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
# MTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRp
# bWVTdGFtcGluZyBDQQIQBUSv85SdCDmmv9s/X+VhFjANBglghkgBZQMEAgEFAKBp
# MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMTEx
# MDAwMjY0M1owLwYJKoZIhvcNAQkEMSIEIO+st5YOFw9sm1qgco+hfdR4Ah2Qrwvd
# RniN8uxJxLEVMA0GCSqGSIb3DQEBAQUABIICAHS7G2PrNw2N+0VM3KsAL1V4dLA1
# 3YGCKYnli0+/fM3oxScunjJaGgVrIJy3XNuUkEYNgB5LbCXRcLBGfsZQucD0S2Bb
# 2G2XXJm04GzW1Lfu1nP1wpExglae26cSZpH9ppnePh2Kp729o9ugCbJd7iKGHKLa
# WJP0u6JWo/HBHykmVlqA8C18ZDUaoQ7NyxVLzvM1s8lhAA2D6fo2QRpuFSKc9Px7
# aw+cQP0b8Rm8dfqN4c4P0kdOU7QDuHXXE4v52DxfO6f+/oC1AFxFLoL/DxFYDtUu
# W4QlVsDy9SOAttYclTQzq6AEHWCS6iyYhf6FTaGkFN/uSHydi0vEE+rDM+gOiI1f
# bMm7bUK3zpPWyD71NmLHXSO1qFBUjtvET+AEZfK4MN0KUDpI/IpUKJ0GQn9TFuCm
# l2K6COQ80bztf1I4OxSQFWk9yOn/vxCz8xXNuUyJsXIlnIY7okwKKTbZVk/Y6T0L
# yK3i92mJIkpl8mMmyu7uaI7fSlSXJDsuINWzmPdBNfoo+IH2xpTbtEpQ51un47DF
# AMD2BMGkFINa536bajpHSw+o5I7KWXMbQh5B7SsmkIh3KrVwzTuCbArFiJVTLRqg
# JjpPcjlaM9A+Q25FHicXucsRvJ+7OG6K8oj9VziRj4pIZ9UN1OrhJO/vqQTAWrUs
# +3oSIGV+soLhve5+
# SIG # End signature block