Public/snapshot.ps1

# Volume snapshots, snapshot policies, and VM snapshots.
# Restore-CSVMSnapshot (revertToVMSnapshot) lives in vm.ps1.

function Get-CSSnapshot {
    <#
    .SYNOPSIS
        Lists volume snapshots.

    .DESCRIPTION
        Wraps listSnapshots. Accepts volume objects (or volume names) on the
        pipeline and lists each volume's snapshots, so
        'Get-CSVM web-01 | Get-CSVolume | Get-CSSnapshot' returns every snapshot
        of a VM's disks.

    .PARAMETER Id
        Filter by snapshot ID

    .PARAMETER Ids
        Filter by several snapshot IDs. Mutually exclusive with -Id.

    .PARAMETER Name
        Filter by snapshot name

    .PARAMETER Keyword
        Filter by keyword

    .PARAMETER Volume
        A volume name or volume object whose snapshots to list. Binds from the pipeline.

    .PARAMETER VolumeId
        List the snapshots of this volume ID

    .PARAMETER SnapshotType
        MANUAL or RECURRING (taken by a snapshot policy)

    .PARAMETER IntervalType
        Filter by the policy interval: HOURLY, DAILY, WEEKLY, or MONTHLY

    .PARAMETER ZoneId
        Filter by zone ID

    .PARAMETER StorageId
        Filter by primary storage pool ID

    .PARAMETER ImageStoreId
        Filter by image store (secondary storage) ID

    .PARAMETER LocationType
        primary or secondary. Only applies with -ShowUnique $false.

    .PARAMETER ShowUnique
        $false lists a snapshot once per zone and storage it is on

    .PARAMETER Tags
        Filter by resource tags, as a hashtable of key = value pairs

    .PARAMETER Account
        Filter by account name. Must be used with -DomainId.

    .PARAMETER DomainId
        Filter by domain ID

    .PARAMETER ProjectId
        Filter by project ID (-1 for all projects)

    .PARAMETER IsRecursive
        With -DomainId, also include snapshots in subdomains

    .PARAMETER ListAll
        List every snapshot the caller is allowed to see

    .PARAMETER Page
        Page number of results to return

    .PARAMETER PageSize
        Number of results per page

    .EXAMPLE
        Get-CSSnapshot -Volume 'web-01-data'
        Lists the snapshots of a volume found by name.

    .EXAMPLE
        Get-CSVM -Name 'web-01' | Get-CSVolume | Get-CSSnapshot
        Lists the snapshots of every disk on a VM.

    .EXAMPLE
        Get-CSSnapshot -SnapshotType MANUAL -ListAll | Where-Object { [datetime]$_.created -lt (Get-Date).AddDays(-90) }
        Finds manual snapshots older than 90 days.
    #>

    [CmdletBinding()]
    param(
        [string]$Id,

        [string[]]$Ids,

        [string]$Name,

        [string]$Keyword,

        [Parameter(ValueFromPipeline = $true)]
        [object]$Volume,

        [string]$VolumeId,

        [ValidateSet('MANUAL', 'RECURRING')]
        [string]$SnapshotType,

        [ValidateSet('HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY')]
        [string]$IntervalType,

        [string]$ZoneId,

        [string]$StorageId,

        [string]$ImageStoreId,

        [ValidateSet('primary', 'secondary')]
        [string]$LocationType,

        [bool]$ShowUnique,

        [hashtable]$Tags,

        [string]$Account,

        [string]$DomainId,

        [string]$ProjectId,

        [switch]$IsRecursive,

        [switch]$ListAll,

        [int]$Page,

        [int]$PageSize
    )

    process {
        if ($PSBoundParameters.ContainsKey('Id') -and $PSBoundParameters.ContainsKey('Ids')) {
            throw 'Specify either -Id or -Ids, not both.'
        }
        if ($PSBoundParameters.ContainsKey('Volume') -and $PSBoundParameters.ContainsKey('VolumeId')) {
            throw 'Specify either -Volume or -VolumeId, not both.'
        }
        if ($PSBoundParameters.ContainsKey('Account') -and -not $PSBoundParameters.ContainsKey('DomainId')) {
            throw 'DomainId is required when Account is specified.'
        }

        $apiParams = @{}
        Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
            Id = 'id'; Ids = 'ids'; Name = 'name'; Keyword = 'keyword'; VolumeId = 'volumeid'
            SnapshotType = 'snapshottype'; IntervalType = 'intervaltype'; ZoneId = 'zoneid'; StorageId = 'storageid'
            ImageStoreId = 'imagestoreid'; LocationType = 'locationtype'; ShowUnique = 'showunique'; Account = 'account'
            DomainId = 'domainid'; ProjectId = 'projectid'; IsRecursive = 'isrecursive'; ListAll = 'listall'
            Page = 'page'; PageSize = 'pagesize'
        })
        if ($PSBoundParameters.ContainsKey('Volume')) { $apiParams['volumeid'] = Resolve-CSVolumeId -Volume $Volume }
        Add-CSMapParameter -ApiParameters $apiParams -Name 'tags' -Map $Tags -KeyField 'key' -ValueField 'value'

        ConvertFrom-CSResponse -Response (Invoke-CSApiRequest -Command 'listSnapshots' -Parameters $apiParams) -Command 'listSnapshots'
    }
}

function New-CSSnapshot {
    <#
    .SYNOPSIS
        Takes a snapshot of a volume.

    .DESCRIPTION
        Wraps createSnapshot. Accepts volume objects from Get-CSVolume on the
        pipeline. This is an asynchronous job; use -Wait to get the finished
        snapshot back instead of the job handle.

    .PARAMETER VolumeId
        The volume to snapshot (binds from a piped volume's id)

    .PARAMETER Name
        A name for the snapshot. Never bound from the pipeline, because a piped
        volume's name is the volume's.

    .PARAMETER Tags
        Resource tags for the snapshot, as a hashtable of key = value pairs

    .PARAMETER ZoneIds
        Extra zones to make the snapshot available in. The volume's own zone is always included.

    .PARAMETER PolicyId
        The snapshot policy to associate the snapshot with. Defaults to the manual policy.

    .PARAMETER LocationType
        Managed storage only: keep the snapshot on primary (default) or secondary storage

    .PARAMETER QuiesceVM
        Quiesce the VM before taking the snapshot

    .PARAMETER AsyncBackup
        Back the snapshot up to secondary storage in the background

    .PARAMETER Account
        Account that will own the snapshot. Must be used with -DomainId.

    .PARAMETER DomainId
        Domain of the owning account

    .PARAMETER Wait
        Wait for the async job to finish and return the snapshot

    .EXAMPLE
        New-CSSnapshot -VolumeId vol-uuid -Name 'before-upgrade' -Wait
        Snapshots a volume and waits for the snapshot to finish.

    .EXAMPLE
        Get-CSVM -Name 'db-01' | Get-CSVolume | New-CSSnapshot -QuiesceVM -Tags @{ reason = 'pre-patch' }
        Snapshots every disk on a VM, quiescing it first.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [string]$VolumeId,

        [string]$Name,

        [hashtable]$Tags,

        [string[]]$ZoneIds,

        [string]$PolicyId,

        [ValidateSet('primary', 'secondary')]
        [string]$LocationType,

        [switch]$QuiesceVM,

        [switch]$AsyncBackup,

        [string]$Account,

        [string]$DomainId,

        [switch]$Wait
    )

    process {
        if ($PSBoundParameters.ContainsKey('Account') -and -not $PSBoundParameters.ContainsKey('DomainId')) {
            throw 'DomainId is required when Account is specified.'
        }
        $apiParams = @{ volumeid = $VolumeId }
        Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
            Name = 'name'; ZoneIds = 'zoneids'; PolicyId = 'policyid'; LocationType = 'locationtype'
            QuiesceVM = 'quiescevm'; AsyncBackup = 'asyncbackup'; Account = 'account'; DomainId = 'domainid'
        })
        Add-CSMapParameter -ApiParameters $apiParams -Name 'tags' -Map $Tags -KeyField 'key' -ValueField 'value'
        if ($PSCmdlet.ShouldProcess("volume $VolumeId", 'Take snapshot')) {
            Invoke-CSAsyncApiRequest -Command 'createSnapshot' -Parameters $apiParams -Wait:$Wait
        }
    }
}

function Remove-CSSnapshot {
    <#
    .SYNOPSIS
        Deletes a volume snapshot.

    .DESCRIPTION
        Wraps deleteSnapshot. Without -ZoneId the snapshot is deleted from every
        zone it was copied to. This is an asynchronous job; use -Wait to block
        until it finishes. Accepts snapshot objects from Get-CSSnapshot on the pipeline.

    .PARAMETER Id
        The ID of the snapshot to delete (binds from a piped snapshot's id)

    .PARAMETER ZoneId
        Only delete the snapshot's copy in this zone. Never bound from the
        pipeline, so a piped snapshot is deleted everywhere unless you say otherwise.

    .PARAMETER Wait
        Wait for the async job to finish and return its result

    .EXAMPLE
        Remove-CSSnapshot -Id snapshot-uuid
        Deletes a snapshot after prompting for confirmation.

    .EXAMPLE
        Get-CSSnapshot -SnapshotType MANUAL -ListAll | Where-Object { [datetime]$_.created -lt (Get-Date).AddDays(-90) } | Remove-CSSnapshot -Confirm:$false
        Deletes manual snapshots older than 90 days without prompting.

    .EXAMPLE
        Remove-CSSnapshot -Id snapshot-uuid -ZoneId dr-zone-uuid -WhatIf
        Shows what removing only the DR-zone copy would do, without doing it.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$Id,

        [string]$ZoneId,

        [switch]$Wait
    )

    process {
        $apiParams = @{ id = $Id }
        if ($PSBoundParameters.ContainsKey('ZoneId')) { $apiParams['zoneid'] = $ZoneId }
        $target = if ($ZoneId) { "snapshot $Id in zone $ZoneId" } else { "snapshot $Id in all zones" }
        if ($PSCmdlet.ShouldProcess($target, 'Delete')) {
            Invoke-CSAsyncApiRequest -Command 'deleteSnapshot' -Parameters $apiParams -Wait:$Wait
        }
    }
}

function Restore-CSSnapshot {
    <#
    .SYNOPSIS
        Reverts a volume to one of its snapshots.

    .DESCRIPTION
        Wraps revertSnapshot (KVM only). Everything written to the volume since the
        snapshot was taken is lost. The volume's VM should be stopped. This is an
        asynchronous job; use -Wait to block until it finishes. Accepts snapshot
        objects from Get-CSSnapshot on the pipeline. To create a new volume from a
        snapshot instead, use New-CSVolume -SnapshotId.

    .PARAMETER Id
        The ID of the snapshot to revert to (binds from a piped snapshot's id)

    .PARAMETER Wait
        Wait for the async job to finish and return the snapshot

    .EXAMPLE
        Restore-CSSnapshot -Id snapshot-uuid -Wait
        Reverts a volume to a snapshot after prompting for confirmation.

    .EXAMPLE
        Get-CSSnapshot -Volume 'db-01-root' | Sort-Object { [datetime]$_.created } | Select-Object -Last 1 | Restore-CSSnapshot
        Reverts a volume to its most recent snapshot.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$Id,

        [switch]$Wait
    )

    process {
        if ($PSCmdlet.ShouldProcess("snapshot $Id", 'Revert its volume to this snapshot')) {
            Invoke-CSAsyncApiRequest -Command 'revertSnapshot' -Parameters @{ id = $Id } -Wait:$Wait
        }
    }
}

function Move-CSSnapshot {
    <#
    .SYNOPSIS
        Moves a snapshot from primary storage to secondary storage.

    .DESCRIPTION
        Wraps archiveSnapshot, which copies a snapshot kept on primary storage to
        secondary storage and frees the space on primary. ('Archive' is not an
        approved PowerShell verb.) This is an asynchronous job; use -Wait to get
        the moved snapshot back. Accepts snapshot objects from Get-CSSnapshot on
        the pipeline.

    .PARAMETER Id
        The ID of the snapshot to move (binds from a piped snapshot's id)

    .PARAMETER Wait
        Wait for the async job to finish and return the snapshot

    .EXAMPLE
        Move-CSSnapshot -Id snapshot-uuid -Wait
        Moves a snapshot to secondary storage.

    .EXAMPLE
        Get-CSSnapshot -ShowUnique $false -LocationType primary -ListAll | Move-CSSnapshot
        Moves every snapshot still on primary storage to secondary storage.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$Id,

        [switch]$Wait
    )

    process {
        if ($PSCmdlet.ShouldProcess("snapshot $Id", 'Move to secondary storage')) {
            Invoke-CSAsyncApiRequest -Command 'archiveSnapshot' -Parameters @{ id = $Id } -Wait:$Wait
        }
    }
}

function Copy-CSSnapshot {
    <#
    .SYNOPSIS
        Copies a snapshot to one or more other zones.

    .DESCRIPTION
        Wraps copySnapshot, e.g. to keep a copy in a disaster-recovery zone. This
        is an asynchronous job; use -Wait to get the snapshot back. Accepts
        snapshot objects from Get-CSSnapshot on the pipeline.

    .PARAMETER Id
        The ID of the snapshot to copy (binds from a piped snapshot's id)

    .PARAMETER DestinationZoneId
        One or more zones to copy the snapshot to

    .PARAMETER SourceZoneId
        The zone to copy from. Defaults to the zone of the snapshot's volume.

    .PARAMETER Wait
        Wait for the async job to finish and return the snapshot

    .EXAMPLE
        Copy-CSSnapshot -Id snapshot-uuid -DestinationZoneId dr-zone-uuid -Wait
        Copies a snapshot to a DR zone.

    .EXAMPLE
        Get-CSVM -Name 'db-01' | Get-CSVolume | Get-CSSnapshot | Copy-CSSnapshot -DestinationZoneId dr-zone-a, dr-zone-b
        Copies every snapshot of a VM's disks to two zones.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$Id,

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

        [string]$SourceZoneId,

        [switch]$Wait
    )

    process {
        $apiParams = @{ id = $Id }
        # destzoneid and destzoneids are mutually exclusive; use the single form when it fits.
        if ($DestinationZoneId.Count -eq 1) { $apiParams['destzoneid'] = $DestinationZoneId[0] }
        else { $apiParams['destzoneids'] = $DestinationZoneId -join ',' }
        if ($PSBoundParameters.ContainsKey('SourceZoneId')) { $apiParams['sourcezoneid'] = $SourceZoneId }
        if ($PSCmdlet.ShouldProcess("snapshot $Id", "Copy to zone(s) $($DestinationZoneId -join ', ')")) {
            Invoke-CSAsyncApiRequest -Command 'copySnapshot' -Parameters $apiParams -Wait:$Wait
        }
    }
}

function Export-CSSnapshot {
    <#
    .SYNOPSIS
        Gets a download URL for a snapshot.

    .DESCRIPTION
        Wraps extractSnapshot. The snapshot must be in the BackedUp state. This is
        an asynchronous job; use -Wait to get the result, whose 'url' property is
        the download link. Accepts snapshot objects from Get-CSSnapshot on the
        pipeline; the snapshot's zone binds to -ZoneId automatically.

    .PARAMETER Id
        The ID of the snapshot (binds from a piped snapshot's id)

    .PARAMETER ZoneId
        The zone the snapshot is in (binds from a piped snapshot's zoneid)

    .PARAMETER Wait
        Wait for the async job to finish and return the extract result

    .EXAMPLE
        (Export-CSSnapshot -Id snapshot-uuid -ZoneId zone-uuid -Wait).url
        Gets a download URL for a snapshot.

    .EXAMPLE
        Get-CSSnapshot -Name 'before-upgrade' | Export-CSSnapshot -Wait | Select-Object name, url
        Gets a download URL for a snapshot found by name; its zone comes from the snapshot object.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$Id,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$ZoneId,

        [switch]$Wait
    )

    process {
        Invoke-CSAsyncApiRequest -Command 'extractSnapshot' -Parameters @{ id = $Id; zoneid = $ZoneId } -Wait:$Wait
    }
}

function Get-CSSnapshotDetail {
    <#
    .SYNOPSIS
        Gets the storage details of a volume snapshot.

    .DESCRIPTION
        Wraps getVolumeSnapshotDetails, which returns the iSCSI name of a snapshot
        held on managed storage. Accepts snapshot objects from Get-CSSnapshot on
        the pipeline.

    .PARAMETER Id
        The ID of the snapshot (binds from a piped snapshot's id)

    .EXAMPLE
        Get-CSSnapshotDetail -Id snapshot-uuid
        Returns a snapshot's iSCSI name.

    .EXAMPLE
        Get-CSSnapshot -Volume 'db-01-data' | Get-CSSnapshotDetail
        Returns the iSCSI name of every snapshot of a volume.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$Id
    )

    process {
        ConvertFrom-CSResponse -Response (Invoke-CSApiRequest -Command 'getVolumeSnapshotDetails' -Parameters @{ snapshotid = $Id }) -Command 'getVolumeSnapshotDetails'
    }
}

function New-CSSnapshotFromVMSnapshot {
    <#
    .SYNOPSIS
        Creates a volume snapshot from a VM snapshot.

    .DESCRIPTION
        Wraps createSnapshotFromVMSnapshot, which turns one disk of a VM snapshot
        into a standalone volume snapshot (which can then be copied, extracted, or
        turned into a volume). This is an asynchronous job; use -Wait to get the
        snapshot back. Accepts VM snapshot objects from Get-CSVMSnapshot on the pipeline.

    .PARAMETER VMSnapshotId
        The VM snapshot (binds from a piped VM snapshot's id)

    .PARAMETER VolumeId
        The volume of the VM whose disk to take from the VM snapshot

    .PARAMETER Name
        A name for the new snapshot

    .PARAMETER Wait
        Wait for the async job to finish and return the snapshot

    .EXAMPLE
        New-CSSnapshotFromVMSnapshot -VMSnapshotId vmsnapshot-uuid -VolumeId vol-uuid -Name 'db-data-nightly' -Wait
        Extracts one disk from a VM snapshot as a volume snapshot.

    .EXAMPLE
        $root = Get-CSVM -Name 'web-01' | Get-CSVolume -Type ROOT
        Get-CSVMSnapshot -VM 'web-01' | Select-Object -Last 1 | New-CSSnapshotFromVMSnapshot -VolumeId $root.id
        Turns the root disk of web-01's latest VM snapshot into a volume snapshot.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [string]$VMSnapshotId,

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

        [string]$Name,

        [switch]$Wait
    )

    process {
        $apiParams = @{ vmsnapshotid = $VMSnapshotId; volumeid = $VolumeId }
        if ($PSBoundParameters.ContainsKey('Name')) { $apiParams['name'] = $Name }
        if ($PSCmdlet.ShouldProcess("VM snapshot $VMSnapshotId", "Create snapshot of volume $VolumeId")) {
            Invoke-CSAsyncApiRequest -Command 'createSnapshotFromVMSnapshot' -Parameters $apiParams -Wait:$Wait
        }
    }
}

function Get-CSSnapshotPolicy {
    <#
    .SYNOPSIS
        Lists snapshot policies.

    .DESCRIPTION
        Wraps listSnapshotPolicies. Accepts volume objects (or volume names) on the
        pipeline and lists each volume's policies.

    .PARAMETER Id
        Filter by policy ID

    .PARAMETER Volume
        A volume name or volume object whose policies to list. Binds from the pipeline.

    .PARAMETER VolumeId
        List the policies of this volume ID

    .PARAMETER ForDisplay
        Filter by the display flag (root admin only)

    .PARAMETER Keyword
        Filter by keyword

    .PARAMETER Page
        Page number of results to return

    .PARAMETER PageSize
        Number of results per page

    .EXAMPLE
        Get-CSSnapshotPolicy -Volume 'db-01-data'
        Lists a volume's snapshot policies.

    .EXAMPLE
        Get-CSVM -Name 'db-01' | Get-CSVolume | Get-CSSnapshotPolicy
        Lists the snapshot policies on every disk of a VM.
    #>

    [CmdletBinding()]
    param(
        [string]$Id,

        [Parameter(ValueFromPipeline = $true)]
        [object]$Volume,

        [string]$VolumeId,

        [bool]$ForDisplay,

        [string]$Keyword,

        [int]$Page,

        [int]$PageSize
    )

    process {
        if ($PSBoundParameters.ContainsKey('Volume') -and $PSBoundParameters.ContainsKey('VolumeId')) {
            throw 'Specify either -Volume or -VolumeId, not both.'
        }
        $apiParams = @{}
        Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
            Id = 'id'; VolumeId = 'volumeid'; ForDisplay = 'fordisplay'; Keyword = 'keyword'; Page = 'page'; PageSize = 'pagesize'
        })
        if ($PSBoundParameters.ContainsKey('Volume')) { $apiParams['volumeid'] = Resolve-CSVolumeId -Volume $Volume }
        ConvertFrom-CSResponse -Response (Invoke-CSApiRequest -Command 'listSnapshotPolicies' -Parameters $apiParams) -Command 'listSnapshotPolicies'
    }
}

function New-CSSnapshotPolicy {
    <#
    .SYNOPSIS
        Creates a recurring snapshot schedule for a volume.

    .DESCRIPTION
        Wraps createSnapshotPolicy. The -Schedule format depends on -IntervalType
        (MM = minute, HH = hour, DD = day):
          HOURLY 'MM' e.g. '15' (every hour at :15)
          DAILY 'MM:HH' e.g. '30:02' (every day at 02:30)
          WEEKLY 'MM:HH:DD' e.g. '00:03:1' (DD 1-7, 1 = Sunday)
          MONTHLY 'MM:HH:DD' e.g. '00:04:1' (DD 1-28)
        The format is checked before the request is sent. Accepts volume objects
        from Get-CSVolume on the pipeline.

    .PARAMETER VolumeId
        The volume to schedule snapshots for (binds from a piped volume's id)

    .PARAMETER IntervalType
        HOURLY, DAILY, WEEKLY, or MONTHLY

    .PARAMETER Schedule
        When to take the snapshot, in the format for -IntervalType (see the description)

    .PARAMETER Timezone
        Time zone the schedule is evaluated in, e.g. 'America/Detroit'

    .PARAMETER MaxSnaps
        How many snapshots to keep; the oldest is deleted when a new one is taken

    .PARAMETER ZoneIds
        Extra zones to make each snapshot available in

    .PARAMETER Tags
        Resource tags for the policy, as a hashtable of key = value pairs

    .PARAMETER ForDisplay
        Whether end users can see the policy

    .EXAMPLE
        New-CSSnapshotPolicy -VolumeId vol-uuid -IntervalType DAILY -Schedule '30:02' -Timezone 'America/Detroit' -MaxSnaps 7
        Snapshots a volume every night at 02:30 and keeps a week of snapshots.

    .EXAMPLE
        Get-CSVM -Name 'db-01' | Get-CSVolume | New-CSSnapshotPolicy -IntervalType WEEKLY -Schedule '00:03:1' -Timezone 'UTC' -MaxSnaps 4 -ZoneIds dr-zone-uuid
        Snapshots every disk of db-01 on Sundays at 03:00 UTC, keeping four weeks, with a copy in a DR zone.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [string]$VolumeId,

        [Parameter(Mandatory = $true)]
        [ValidateSet('HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY')]
        [string]$IntervalType,

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

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

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$MaxSnaps,

        [string[]]$ZoneIds,

        [hashtable]$Tags,

        [bool]$ForDisplay
    )

    begin {
        $minute = '([0-5]?[0-9])'
        $hour = '([01]?[0-9]|2[0-3])'
        $patterns = @{
            HOURLY  = @("^$minute$", "'MM', e.g. '15'")
            DAILY   = @("^${minute}:${hour}$", "'MM:HH', e.g. '30:02'")
            WEEKLY  = @("^${minute}:${hour}:[1-7]$", "'MM:HH:DD' with DD 1-7, e.g. '00:03:1'")
            MONTHLY = @("^${minute}:${hour}:([1-9]|1[0-9]|2[0-8])$", "'MM:HH:DD' with DD 1-28, e.g. '00:04:1'")
        }
        $pattern = $patterns[$IntervalType.ToUpperInvariant()]
        if ($Schedule -notmatch $pattern[0]) {
            throw "Schedule '$Schedule' does not fit a $IntervalType policy. Use $($pattern[1])."
        }
    }

    process {
        $apiParams = @{
            volumeid = $VolumeId
            intervaltype = $IntervalType.ToUpperInvariant()
            schedule = $Schedule
            timezone = $Timezone
            maxsnaps = $MaxSnaps
        }
        Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
            ZoneIds = 'zoneids'; ForDisplay = 'fordisplay'
        })
        Add-CSMapParameter -ApiParameters $apiParams -Name 'tags' -Map $Tags -KeyField 'key' -ValueField 'value'
        if ($PSCmdlet.ShouldProcess("volume $VolumeId", "Create $IntervalType snapshot policy ($Schedule $Timezone, keep $MaxSnaps)")) {
            ConvertFrom-CSResponse -Response (Invoke-CSApiRequest -Command 'createSnapshotPolicy' -Parameters $apiParams) -Command 'createSnapshotPolicy'
        }
    }
}

function Set-CSSnapshotPolicy {
    <#
    .SYNOPSIS
        Updates a snapshot policy's display flag or custom ID.

    .DESCRIPTION
        Wraps updateSnapshotPolicy. CloudStack cannot change a policy's schedule,
        interval, or retention in place; to change those, create a new policy with
        New-CSSnapshotPolicy and remove the old one. This is an asynchronous job;
        use -Wait to get the updated policy back. Accepts policy objects from
        Get-CSSnapshotPolicy on the pipeline.

    .PARAMETER Id
        The ID of the policy (binds from a piped policy's id)

    .PARAMETER ForDisplay
        Whether end users can see the policy

    .PARAMETER CustomId
        A custom ID for the policy (root admin only)

    .PARAMETER Wait
        Wait for the async job to finish and return the policy

    .EXAMPLE
        Set-CSSnapshotPolicy -Id policy-uuid -ForDisplay $false -Wait
        Hides a policy from end users.

    .EXAMPLE
        Get-CSSnapshotPolicy -Volume 'db-01-data' | Set-CSSnapshotPolicy -ForDisplay $true
        Makes every policy on a volume visible to end users.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$Id,

        [bool]$ForDisplay,

        [string]$CustomId,

        [switch]$Wait
    )

    process {
        $apiParams = @{ id = $Id }
        Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
            ForDisplay = 'fordisplay'; CustomId = 'customid'
        })
        if ($apiParams.Count -eq 1) { throw 'Specify -ForDisplay or -CustomId.' }
        if ($PSCmdlet.ShouldProcess("snapshot policy $Id", 'Update')) {
            Invoke-CSAsyncApiRequest -Command 'updateSnapshotPolicy' -Parameters $apiParams -Wait:$Wait
        }
    }
}

function Remove-CSSnapshotPolicy {
    <#
    .SYNOPSIS
        Deletes snapshot policies.

    .DESCRIPTION
        Wraps deleteSnapshotPolicies. Snapshots already taken by the policy are
        kept. Accepts policy objects from Get-CSSnapshotPolicy on the pipeline.

    .PARAMETER Id
        The ID of the policy to delete (binds from a piped policy's id)

    .EXAMPLE
        Remove-CSSnapshotPolicy -Id policy-uuid
        Deletes a policy after prompting for confirmation.

    .EXAMPLE
        Get-CSVM -Name 'old-app' | Get-CSVolume | Get-CSSnapshotPolicy | Remove-CSSnapshotPolicy -Confirm:$false
        Stops scheduled snapshots of every disk on a VM.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$Id
    )

    process {
        if ($PSCmdlet.ShouldProcess("snapshot policy $Id", 'Delete')) {
            ConvertFrom-CSResponse -Response (Invoke-CSApiRequest -Command 'deleteSnapshotPolicies' -Parameters @{ id = $Id }) -Command 'deleteSnapshotPolicies'
        }
    }
}

function Get-CSVMSnapshot {
    <#
    .SYNOPSIS
        Lists VM snapshots.

    .DESCRIPTION
        Wraps listVMSnapshot. Accepts VM objects (or VM names) on the pipeline and
        lists each VM's snapshots. Pipe the results to Restore-CSVMSnapshot,
        Remove-CSVMSnapshot, or New-CSSnapshotFromVMSnapshot.

    .PARAMETER Id
        Filter by VM snapshot ID

    .PARAMETER Ids
        Filter by several VM snapshot IDs. Mutually exclusive with -Id.

    .PARAMETER Name
        Filter by name or display name

    .PARAMETER Keyword
        Filter by keyword

    .PARAMETER VM
        A VM name or VM object whose snapshots to list. Binds from the pipeline.

    .PARAMETER VirtualMachineId
        List the snapshots of this VM ID

    .PARAMETER State
        Filter by state, e.g. Ready

    .PARAMETER Tags
        Filter by resource tags, as a hashtable of key = value pairs

    .PARAMETER Account
        Filter by account name. Must be used with -DomainId.

    .PARAMETER DomainId
        Filter by domain ID

    .PARAMETER ProjectId
        Filter by project ID (-1 for all projects)

    .PARAMETER IsRecursive
        With -DomainId, also include snapshots in subdomains

    .PARAMETER ListAll
        List every VM snapshot the caller is allowed to see

    .PARAMETER Page
        Page number of results to return

    .PARAMETER PageSize
        Number of results per page

    .EXAMPLE
        Get-CSVMSnapshot -VM 'web-01'
        Lists web-01's VM snapshots.

    .EXAMPLE
        Get-CSVM -Keyword 'dev-' | Get-CSVMSnapshot | Where-Object { [datetime]$_.created -lt (Get-Date).AddDays(-30) } | Remove-CSVMSnapshot
        Deletes VM snapshots older than 30 days on every dev VM.
    #>

    [CmdletBinding()]
    param(
        [string]$Id,

        [string[]]$Ids,

        [string]$Name,

        [string]$Keyword,

        [Parameter(Position = 0, ValueFromPipeline = $true)]
        [object]$VM,

        [string]$VirtualMachineId,

        [string]$State,

        [hashtable]$Tags,

        [string]$Account,

        [string]$DomainId,

        [string]$ProjectId,

        [switch]$IsRecursive,

        [switch]$ListAll,

        [int]$Page,

        [int]$PageSize
    )

    process {
        if ($PSBoundParameters.ContainsKey('Id') -and $PSBoundParameters.ContainsKey('Ids')) {
            throw 'Specify either -Id or -Ids, not both.'
        }
        if ($PSBoundParameters.ContainsKey('VM') -and $PSBoundParameters.ContainsKey('VirtualMachineId')) {
            throw 'Specify either -VM or -VirtualMachineId, not both.'
        }
        if ($PSBoundParameters.ContainsKey('Account') -and -not $PSBoundParameters.ContainsKey('DomainId')) {
            throw 'DomainId is required when Account is specified.'
        }
        $apiParams = @{}
        Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
            Id = 'vmsnapshotid'; Ids = 'vmsnapshotids'; Name = 'name'; Keyword = 'keyword'
            VirtualMachineId = 'virtualmachineid'; State = 'state'; Account = 'account'; DomainId = 'domainid'
            ProjectId = 'projectid'; IsRecursive = 'isrecursive'; ListAll = 'listall'; Page = 'page'; PageSize = 'pagesize'
        })
        if ($PSBoundParameters.ContainsKey('VM')) { $apiParams['virtualmachineid'] = Resolve-CSVMTagId -VM $VM }
        Add-CSMapParameter -ApiParameters $apiParams -Name 'tags' -Map $Tags -KeyField 'key' -ValueField 'value'
        ConvertFrom-CSResponse -Response (Invoke-CSApiRequest -Command 'listVMSnapshot' -Parameters $apiParams) -Command 'listVMSnapshot'
    }
}

function New-CSVMSnapshot {
    <#
    .SYNOPSIS
        Takes a snapshot of a whole virtual machine.

    .DESCRIPTION
        Wraps createVMSnapshot. A VM snapshot covers all of the VM's disks, and
        optionally its memory, at one point in time. Roll back with
        Restore-CSVMSnapshot. Accepts -VM (a name or a piped VM object) or
        -VirtualMachineId. This is an asynchronous job; use -Wait to get the VM
        snapshot back.

    .PARAMETER VirtualMachineId
        The ID of the VM to snapshot

    .PARAMETER VM
        A VM name or VM object. Binds from the pipeline.

    .PARAMETER Name
        A display name for the snapshot

    .PARAMETER Description
        A description for the snapshot

    .PARAMETER SnapshotMemory
        Include the VM's memory, so reverting resumes the running VM rather than booting it

    .PARAMETER QuiesceVM
        Quiesce the VM's file systems before taking the snapshot

    .PARAMETER Wait
        Wait for the async job to finish and return the VM snapshot

    .EXAMPLE
        New-CSVMSnapshot -VM 'web-01' -Name 'before-upgrade' -SnapshotMemory -Wait
        Snapshots web-01, including its memory.

    .EXAMPLE
        Get-CSVM -Keyword 'app-' | New-CSVMSnapshot -Name "pre-patch-$(Get-Date -Format yyyyMMdd)" -QuiesceVM
        Snapshots every app VM before patching.

    .EXAMPLE
        $snap = New-CSVMSnapshot -VM 'web-01' -Name 'before-upgrade' -Wait
        # ...upgrade fails...
        $snap | Restore-CSVMSnapshot -Wait
        Takes a snapshot, then rolls the VM back to it.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
    param(
        [Parameter(Mandatory = $false)]
        [string]$VirtualMachineId,

        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true)]
        [object]$VM,

        [string]$Name,

        [string]$Description,

        [switch]$SnapshotMemory,

        [switch]$QuiesceVM,

        [switch]$Wait
    )

    process {
        $resolvedId = Resolve-CSVMTagId -VirtualMachineId $VirtualMachineId -VM $VM
        $apiParams = @{ virtualmachineid = $resolvedId }
        Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
            Name = 'name'; Description = 'description'; SnapshotMemory = 'snapshotmemory'; QuiesceVM = 'quiescevm'
        })
        if ($PSCmdlet.ShouldProcess("VM $resolvedId", 'Take VM snapshot')) {
            Invoke-CSAsyncApiRequest -Command 'createVMSnapshot' -Parameters $apiParams -Wait:$Wait
        }
    }
}

function Remove-CSVMSnapshot {
    <#
    .SYNOPSIS
        Deletes a VM snapshot.

    .DESCRIPTION
        Wraps deleteVMSnapshot. The VM itself is not affected. This is an
        asynchronous job; use -Wait to block until it finishes. Accepts VM snapshot
        objects from Get-CSVMSnapshot on the pipeline.

    .PARAMETER VMSnapshotId
        The ID of the VM snapshot to delete (binds from a piped VM snapshot's id)

    .PARAMETER Wait
        Wait for the async job to finish and return its result

    .EXAMPLE
        Remove-CSVMSnapshot -VMSnapshotId vmsnapshot-uuid
        Deletes a VM snapshot after prompting for confirmation.

    .EXAMPLE
        Get-CSVMSnapshot -VM 'web-01' -Name 'before-upgrade' | Remove-CSVMSnapshot -Confirm:$false -Wait
        Deletes a VM snapshot found by name once it is no longer needed.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [string]$VMSnapshotId,

        [switch]$Wait
    )

    process {
        if ($PSCmdlet.ShouldProcess("VM snapshot $VMSnapshotId", 'Delete')) {
            Invoke-CSAsyncApiRequest -Command 'deleteVMSnapshot' -Parameters @{ vmsnapshotid = $VMSnapshotId } -Wait:$Wait
        }
    }
}