Public/template.ps1

function Get-CSTemplate {
    <#
    .SYNOPSIS
        Lists templates in CloudStack.

    .DESCRIPTION
        Retrieves templates with optional filtering by ID, name, zone, or
        hypervisor. Wraps the listTemplates API call.

    .PARAMETER Id
        Filter by template ID

    .PARAMETER Name
        Filter by template name

    .PARAMETER Keyword
        Filter by keyword (partial match on name)

    .PARAMETER TemplateFilter
        Which templates to list. Defaults to 'executable', i.e. the templates
        the caller is allowed to deploy from.

    .PARAMETER ZoneId
        Filter by zone ID

    .PARAMETER Hypervisor
        Filter by hypervisor type

    .PARAMETER IsReady
        Only return templates that have finished downloading and are ready to use

    .PARAMETER Account
        Filter by account name (must be used with DomainId)

    .PARAMETER DomainId
        Filter by domain ID

    .PARAMETER ProjectId
        Filter by project ID

    .PARAMETER ListAll
        List all templates the caller has access to (requires appropriate permissions)

    .EXAMPLE
        Get-CSTemplate
        Lists every template available to deploy from

    .EXAMPLE
        Get-CSTemplate -Name "Ubuntu 22.04"
        Gets a specific template by name

    .EXAMPLE
        Get-CSTemplate -ZoneId $zoneId -Hypervisor KVM -IsReady
        Lists ready KVM templates in a zone
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [Parameter(ParameterSetName = 'ById')]
        [string]$Id,

        [Parameter(ParameterSetName = 'ByName')]
        [string]$Name,

        [Parameter(ParameterSetName = 'Default')]
        [string]$Keyword,

        [Parameter()]
        [ValidateSet('featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community', 'all')]
        [string]$TemplateFilter = 'executable',

        [Parameter()]
        [string]$ZoneId,

        [Parameter()]
        [string]$Hypervisor,

        [Parameter()]
        [switch]$IsReady,

        [Parameter()]
        [string]$Account,

        [Parameter()]
        [string]$DomainId,

        [Parameter()]
        [string]$ProjectId,

        [Parameter()]
        [switch]$ListAll
    )

    # listTemplates requires templatefilter, so it is always sent
    $apiParams = @{
        'templatefilter' = $TemplateFilter
    }

    if ($PSBoundParameters.ContainsKey('Id')) {
        $apiParams['id'] = $Id
    }

    if ($PSBoundParameters.ContainsKey('Name')) {
        $apiParams['name'] = $Name
    }

    if ($PSBoundParameters.ContainsKey('Keyword')) {
        $apiParams['keyword'] = $Keyword
    }

    if ($PSBoundParameters.ContainsKey('ZoneId')) {
        $apiParams['zoneid'] = $ZoneId
    }

    if ($PSBoundParameters.ContainsKey('Hypervisor')) {
        $apiParams['hypervisor'] = $Hypervisor
    }

    if ($IsReady) {
        $apiParams['isready'] = 'true'
    }

    if ($PSBoundParameters.ContainsKey('Account')) {
        $apiParams['account'] = $Account
    }

    if ($PSBoundParameters.ContainsKey('DomainId')) {
        $apiParams['domainid'] = $DomainId
    }

    if ($PSBoundParameters.ContainsKey('ProjectId')) {
        $apiParams['projectid'] = $ProjectId
    }

    if ($ListAll) {
        $apiParams['listall'] = 'true'
    }

    $response = Invoke-CSApiRequest -Command 'listTemplates' -Parameters $apiParams
    Write-Verbose "API Parameters: $($apiParams | Out-String)"

    if ($response.listtemplatesresponse.template) {
        return $response.listtemplatesresponse.template
    }
    else {
        Write-Verbose "No templates found matching the criteria."
        return $null
    }
}

function New-CSTemplate {
    <#
    .SYNOPSIS
        Creates a template from a stopped instance's volume or a snapshot.

    .DESCRIPTION
        Wraps createTemplate. The source is either a volume (whose instance must be
        stopped) or a volume snapshot; give -VolumeId or -SnapshotId, not both. The
        new template is private to the creating account. This is an asynchronous
        job; use -Wait to block until it finishes and return the template.

    .PARAMETER Name
        Name for the new template

    .PARAMETER OsTypeId
        The OS type ID the template runs (see Get-CSOsType)

    .PARAMETER DisplayText
        Description shown in the UI. Defaults to the name.

    .PARAMETER VolumeId
        Create the template from this volume. Mutually exclusive with -SnapshotId.

    .PARAMETER SnapshotId
        Create the template from this volume snapshot. Mutually exclusive with -VolumeId.

    .PARAMETER VirtualMachineId
        With -VolumeId, the instance the volume belongs to

    .PARAMETER Bits
        32 or 64 bit

    .PARAMETER TemplateTag
        Template tag used by service offerings to pin placement

    .PARAMETER Details
        Extra key/value details as a hashtable

    .PARAMETER IsDynamicallyScalable
        Mark the template as supporting dynamic scaling of CPU and memory

    .PARAMETER IsFeatured
        Mark the template as featured

    .PARAMETER IsPublic
        Make the template public within the domain

    .PARAMETER PasswordEnabled
        The template has the password-reset script installed

    .PARAMETER RequiresHvm
        The template requires hardware-assisted virtualization

    .PARAMETER ProjectId
        Create the template in this project

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

    .EXAMPLE
        New-CSTemplate -Name 'web-golden' -OsTypeId $osId -VolumeId $rootVolId -Wait
        Captures a stopped VM's root volume as a template.

    .EXAMPLE
        $snap = Get-CSSnapshot -Volume 'db-01-data' | Select-Object -First 1
        New-CSTemplate -Name 'db-baseline' -OsTypeId $osId -SnapshotId $snap.id -Wait
        Builds a template from a volume snapshot.

    .EXAMPLE
        New-CSTemplate -Name 'app-tmpl' -OsTypeId $osId -VolumeId $volId -IsPublic -IsDynamicallyScalable
        Creates a public, dynamically scalable template and returns the job handle.
    #>

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

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

        [string]$DisplayText,

        [string]$VolumeId,

        [string]$SnapshotId,

        [string]$VirtualMachineId,

        [ValidateSet(32, 64)]
        [int]$Bits,

        [string]$TemplateTag,

        [hashtable]$Details,

        [switch]$IsDynamicallyScalable,

        [switch]$IsFeatured,

        [switch]$IsPublic,

        [switch]$PasswordEnabled,

        [switch]$RequiresHvm,

        [string]$ProjectId,

        [switch]$Wait
    )

    process {
        if ($PSBoundParameters.ContainsKey('VolumeId') -and $PSBoundParameters.ContainsKey('SnapshotId')) {
            throw 'Specify -VolumeId or -SnapshotId, not both.'
        }
        if (-not $PSBoundParameters.ContainsKey('VolumeId') -and -not $PSBoundParameters.ContainsKey('SnapshotId')) {
            throw 'Specify a source with -VolumeId or -SnapshotId.'
        }
        $apiParams = @{ name = $Name; ostypeid = $OsTypeId }
        Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
            DisplayText = 'displaytext'; VolumeId = 'volumeid'; SnapshotId = 'snapshotid'; VirtualMachineId = 'virtualmachineid'
            Bits = 'bits'; TemplateTag = 'templatetag'; IsDynamicallyScalable = 'isdynamicallyscalable'; IsFeatured = 'isfeatured'
            IsPublic = 'ispublic'; PasswordEnabled = 'passwordenabled'; RequiresHvm = 'requireshvm'; ProjectId = 'projectid'
        })
        Add-CSMapParameter -ApiParameters $apiParams -Name 'details' -Map $Details

        if ($PSCmdlet.ShouldProcess("template $Name", 'Create')) {
            Invoke-CSAsyncApiRequest -Command 'createTemplate' -Parameters $apiParams -Wait:$Wait
        }
    }
}

function Register-CSTemplate {
    <#
    .SYNOPSIS
        Registers an existing template image by URL.

    .DESCRIPTION
        Wraps registerTemplate. The URL must be reachable by CloudStack secondary
        storage (or a hypervisor host for -DirectDownload). Register into one zone
        with -ZoneId, several with -ZoneIds, or every zone with -ZoneId 'all'. This
        call is synchronous and returns the registered template(s).

    .PARAMETER Name
        Name for the template

    .PARAMETER Format
        The image format: QCOW2, RAW, VHD, VHDX, OVA, or VMDK

    .PARAMETER Hypervisor
        The hypervisor the template runs on (for example KVM, VMware, XenServer)

    .PARAMETER Url
        URL of the template image

    .PARAMETER OsTypeId
        The OS type ID (see Get-CSOsType). Required unless -IsRouting.

    .PARAMETER DisplayText
        Description shown in the UI. Defaults to the name.

    .PARAMETER ZoneId
        Register into this zone, or 'all' for every zone. Use this or -ZoneIds.

    .PARAMETER ZoneIds
        Register into several zones. Use this or -ZoneId.

    .PARAMETER Account
        Register on behalf of this account. Must be used with -DomainId.

    .PARAMETER DomainId
        The domain of -Account

    .PARAMETER ProjectId
        Register into this project

    .PARAMETER Arch
        CPU architecture: x86_64 or aarch64

    .PARAMETER Bits
        32 or 64 bit

    .PARAMETER Checksum
        Checksum of the image, as {algorithm}hash, to verify the download

    .PARAMETER TemplateTag
        Template tag used by service offerings to pin placement

    .PARAMETER Details
        Extra key/value details as a hashtable

    .PARAMETER DirectDownload
        Download the template straight to primary storage on the host (KVM)

    .PARAMETER IsDynamicallyScalable
        Template supports dynamic scaling of CPU and memory

    .PARAMETER IsExtractable
        Template can be extracted/downloaded by its owner

    .PARAMETER IsFeatured
        Mark the template as featured

    .PARAMETER IsPublic
        Make the template public within the domain

    .PARAMETER IsRouting
        The template is a system VM (router) template

    .PARAMETER PasswordEnabled
        The template has the password-reset script installed

    .PARAMETER RequiresHvm
        The template requires hardware-assisted virtualization

    .PARAMETER SshKeyEnabled
        The template supports SSH key injection

    .EXAMPLE
        Register-CSTemplate -Name 'Ubuntu 24.04' -Format QCOW2 -Hypervisor KVM -Url 'https://images.example.com/ubuntu-24.qcow2' -OsTypeId $osId -ZoneId $zoneId
        Registers a KVM template into one zone.

    .EXAMPLE
        Register-CSTemplate -Name 'Rocky 9' -Format QCOW2 -Hypervisor KVM -Url 'https://images.example.com/rocky-9.qcow2' -OsTypeId $osId -ZoneId all -IsPublic
        Registers a public template into every zone.
    #>

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

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

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

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

        [string]$OsTypeId,

        [string]$DisplayText,

        [string]$ZoneId,

        [string[]]$ZoneIds,

        [string]$Account,

        [string]$DomainId,

        [string]$ProjectId,

        [ValidateSet('x86_64', 'aarch64')]
        [string]$Arch,

        [ValidateSet(32, 64)]
        [int]$Bits,

        [string]$Checksum,

        [string]$TemplateTag,

        [hashtable]$Details,

        [switch]$DirectDownload,

        [switch]$IsDynamicallyScalable,

        [switch]$IsExtractable,

        [switch]$IsFeatured,

        [switch]$IsPublic,

        [switch]$IsRouting,

        [switch]$PasswordEnabled,

        [switch]$RequiresHvm,

        [switch]$SshKeyEnabled
    )

    if ($PSBoundParameters.ContainsKey('ZoneId') -and $PSBoundParameters.ContainsKey('ZoneIds')) {
        throw 'Specify -ZoneId or -ZoneIds, not both.'
    }
    if (-not $PSBoundParameters.ContainsKey('ZoneId') -and -not $PSBoundParameters.ContainsKey('ZoneIds')) {
        throw 'Specify a destination with -ZoneId (or ''all'') or -ZoneIds.'
    }
    if ($PSBoundParameters.ContainsKey('Account') -and -not $PSBoundParameters.ContainsKey('DomainId')) {
        throw '-DomainId is required when -Account is specified.'
    }
    if (-not $PSBoundParameters.ContainsKey('OsTypeId') -and -not $IsRouting) {
        throw '-OsTypeId is required unless -IsRouting is set.'
    }

    $apiParams = @{ name = $Name; format = $Format; hypervisor = $Hypervisor; url = $Url }
    Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
        OsTypeId = 'ostypeid'; DisplayText = 'displaytext'; ZoneId = 'zoneid'; ZoneIds = 'zoneids'; Account = 'account'
        DomainId = 'domainid'; ProjectId = 'projectid'; Arch = 'arch'; Bits = 'bits'; Checksum = 'checksum'; TemplateTag = 'templatetag'
        DirectDownload = 'directdownload'; IsDynamicallyScalable = 'isdynamicallyscalable'; IsExtractable = 'isextractable'
        IsFeatured = 'isfeatured'; IsPublic = 'ispublic'; IsRouting = 'isrouting'; PasswordEnabled = 'passwordenabled'
        RequiresHvm = 'requireshvm'; SshKeyEnabled = 'sshkeyenabled'
    })
    Add-CSMapParameter -ApiParameters $apiParams -Name 'details' -Map $Details

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

function Copy-CSTemplate {
    <#
    .SYNOPSIS
        Copies a template to one or more zones.

    .DESCRIPTION
        Wraps copyTemplate. Give -DestinationZoneId for a single zone or
        -DestinationZoneIds for several. -SourceZoneId is needed when the template
        exists in more than one zone. This is an asynchronous job; use -Wait to
        block until it finishes. Accepts template objects on the pipeline.

    .PARAMETER Id
        The template to copy. Binds from a piped template's id.

    .PARAMETER DestinationZoneId
        Copy to this single zone. Mutually exclusive with -DestinationZoneIds.

    .PARAMETER DestinationZoneIds
        Copy to these zones. Mutually exclusive with -DestinationZoneId.

    .PARAMETER SourceZoneId
        The zone to copy from, when the template exists in several

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

    .EXAMPLE
        Copy-CSTemplate -Id $templateId -DestinationZoneId $drZoneId -Wait
        Copies a template to a DR zone and waits for it to land.

    .EXAMPLE
        Get-CSTemplate -Name 'web-golden' | Copy-CSTemplate -DestinationZoneIds $zoneA, $zoneB
        Fans a template out to two zones, returning the job handle.
    #>

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

        [string]$DestinationZoneId,

        [string[]]$DestinationZoneIds,

        [string]$SourceZoneId,

        [switch]$Wait
    )

    process {
        if ($PSBoundParameters.ContainsKey('DestinationZoneId') -and $PSBoundParameters.ContainsKey('DestinationZoneIds')) {
            throw 'Specify -DestinationZoneId or -DestinationZoneIds, not both.'
        }
        if (-not $PSBoundParameters.ContainsKey('DestinationZoneId') -and -not $PSBoundParameters.ContainsKey('DestinationZoneIds')) {
            throw 'Specify a destination with -DestinationZoneId or -DestinationZoneIds.'
        }
        $apiParams = @{ id = $Id }
        Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
            DestinationZoneId = 'destzoneid'; DestinationZoneIds = 'destzoneids'; SourceZoneId = 'sourcezoneid'
        })
        if ($PSCmdlet.ShouldProcess("template $Id", 'Copy to other zones')) {
            Invoke-CSAsyncApiRequest -Command 'copyTemplate' -Parameters $apiParams -Wait:$Wait
        }
    }
}

function Remove-CSTemplate {
    <#
    .SYNOPSIS
        Deletes a template from a zone or from all zones.

    .DESCRIPTION
        Wraps deleteTemplate. With -ZoneId the template is removed from that zone
        only; without it, from every zone it is in. Instances already deployed from
        the template are unaffected. This is an asynchronous job; use -Wait to block
        until it finishes. Accepts template objects on the pipeline.

    .PARAMETER Id
        The template to delete. Binds from a piped template's id.

    .PARAMETER ZoneId
        Delete only from this zone. Omit to delete from all zones.

    .PARAMETER Forced
        Force deletion across all zones even if some are unreachable

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

    .EXAMPLE
        Remove-CSTemplate -Id $templateId -Wait
        Deletes a template from every zone and waits for the job.

    .EXAMPLE
        Get-CSTemplate -Name 'old-image' -ListAll | Remove-CSTemplate -ZoneId $zoneId
        Removes a template from a single zone.
    #>

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

        [string]$ZoneId,

        [switch]$Forced,

        [switch]$Wait
    )

    process {
        $apiParams = @{ id = $Id }
        Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
            ZoneId = 'zoneid'; Forced = 'forced'
        })
        $target = if ($PSBoundParameters.ContainsKey('ZoneId')) { "template $Id in zone $ZoneId" } else { "template $Id in all zones" }
        if ($PSCmdlet.ShouldProcess($target, 'Delete')) {
            Invoke-CSAsyncApiRequest -Command 'deleteTemplate' -Parameters $apiParams -Wait:$Wait
        }
    }
}

function Export-CSTemplate {
    <#
    .SYNOPSIS
        Extracts a template for download or upload to a URL.

    .DESCRIPTION
        Wraps extractTemplate. HTTP_DOWNLOAD returns a download URL; FTP_UPLOAD
        uploads the extracted image to -Url. This is an asynchronous job; use -Wait
        to block and return the result (which carries the download URL). Accepts
        template objects on the pipeline.

    .PARAMETER Id
        The template to extract. Binds from a piped template's id.

    .PARAMETER Mode
        HTTP_DOWNLOAD (get a download URL) or FTP_UPLOAD (push to -Url)

    .PARAMETER ZoneId
        The zone to extract the template from

    .PARAMETER Url
        Destination URL. Required when -Mode is FTP_UPLOAD.

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

    .EXAMPLE
        Get-CSTemplate -Name 'web-golden' | Export-CSTemplate -Mode HTTP_DOWNLOAD -ZoneId $zoneId -Wait
        Requests a download URL for a template and waits for it.

    .EXAMPLE
        Export-CSTemplate -Id $templateId -Mode FTP_UPLOAD -Url 'ftp://backup.example.com/templates/' -ZoneId $zoneId
        Uploads the extracted template to an FTP destination.
    #>

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

        [Parameter(Mandatory = $true)]
        [ValidateSet('HTTP_DOWNLOAD', 'FTP_UPLOAD')]
        [string]$Mode,

        [string]$ZoneId,

        [string]$Url,

        [switch]$Wait
    )

    process {
        if ($Mode -eq 'FTP_UPLOAD' -and -not $PSBoundParameters.ContainsKey('Url')) {
            throw '-Url is required when -Mode is FTP_UPLOAD.'
        }
        $apiParams = @{ id = $Id; mode = $Mode }
        Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
            ZoneId = 'zoneid'; Url = 'url'
        })
        if ($PSCmdlet.ShouldProcess("template $Id", "Extract ($Mode)")) {
            Invoke-CSAsyncApiRequest -Command 'extractTemplate' -Parameters $apiParams -Wait:$Wait
        }
    }
}

function Initialize-CSTemplate {
    <#
    .SYNOPSIS
        Seeds a template into a zone's primary storage.

    .DESCRIPTION
        Wraps prepareTemplate, which copies a template onto primary storage ahead
        of time so the first deployment from it is fast. This is an asynchronous
        job; use -Wait to block until seeding finishes. Accepts template objects on
        the pipeline.

    .PARAMETER Id
        The template to seed. Binds from a piped template's id.

    .PARAMETER ZoneId
        The zone whose primary storage to seed the template into

    .PARAMETER StorageId
        Seed only this primary storage pool instead of all pools in the zone

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

    .EXAMPLE
        Initialize-CSTemplate -Id $templateId -ZoneId $zoneId -Wait
        Pre-seeds a template into every primary pool in a zone.

    .EXAMPLE
        Get-CSTemplate -Name 'web-golden' | Initialize-CSTemplate -ZoneId $zoneId -StorageId $poolId
        Seeds a template into one specific storage pool.
    #>

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

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

        [string]$StorageId,

        [switch]$Wait
    )

    process {
        $apiParams = @{ templateid = $Id; zoneid = $ZoneId }
        if ($PSBoundParameters.ContainsKey('StorageId')) { $apiParams['storageid'] = $StorageId }
        if ($PSCmdlet.ShouldProcess("template $Id", "Seed into primary storage in zone $ZoneId")) {
            Invoke-CSAsyncApiRequest -Command 'prepareTemplate' -Parameters $apiParams -Wait:$Wait
        }
    }
}

function Set-CSTemplate {
    <#
    .SYNOPSIS
        Updates a template's attributes.

    .DESCRIPTION
        Wraps updateTemplate. Only the attributes you supply are changed. Accepts
        template objects on the pipeline.

    .PARAMETER Id
        The template to update. Binds from a piped template's id.

    .PARAMETER Name
        New name

    .PARAMETER DisplayText
        New description

    .PARAMETER OsTypeId
        New OS type ID

    .PARAMETER Bootable
        Whether the template is bootable

    .PARAMETER PasswordEnabled
        Whether the password-reset script is installed

    .PARAMETER SshKeyEnabled
        Whether the template supports SSH key injection

    .PARAMETER IsDynamicallyScalable
        Whether the template supports dynamic scaling of CPU and memory

    .PARAMETER IsRouting
        Whether the template is a system VM (router) template

    .PARAMETER SortKey
        Ordering key used when listing templates

    .PARAMETER TemplateTag
        Template tag used by service offerings to pin placement

    .PARAMETER Details
        Replacement key/value details as a hashtable

    .PARAMETER CleanupDetails
        Clear all stored details instead of setting them

    .EXAMPLE
        Set-CSTemplate -Id $templateId -DisplayText 'Ubuntu 24.04 LTS (golden)' -OsTypeId $osId
        Updates a template's description and OS type.

    .EXAMPLE
        Get-CSTemplate -Name 'web-golden' | Set-CSTemplate -PasswordEnabled $true -IsDynamicallyScalable $true
        Enables password reset and dynamic scaling on a template.
    #>

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

        [string]$Name,

        [string]$DisplayText,

        [string]$OsTypeId,

        [bool]$Bootable,

        [bool]$PasswordEnabled,

        [bool]$SshKeyEnabled,

        [bool]$IsDynamicallyScalable,

        [bool]$IsRouting,

        [int]$SortKey,

        [string]$TemplateTag,

        [hashtable]$Details,

        [switch]$CleanupDetails
    )

    process {
        $apiParams = @{ id = $Id }
        Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
            Name = 'name'; DisplayText = 'displaytext'; OsTypeId = 'ostypeid'; Bootable = 'bootable'; PasswordEnabled = 'passwordenabled'
            SshKeyEnabled = 'sshkeyenabled'; IsDynamicallyScalable = 'isdynamicallyscalable'; IsRouting = 'isrouting'
            SortKey = 'sortkey'; TemplateTag = 'templatetag'; CleanupDetails = 'cleanupdetails'
        })
        Add-CSMapParameter -ApiParameters $apiParams -Name 'details' -Map $Details
        ConvertFrom-CSResponse -Response (Invoke-CSApiRequest -Command 'updateTemplate' -Parameters $apiParams) -Command 'updateTemplate'
    }
}

function Get-CSTemplatePermission {
    <#
    .SYNOPSIS
        Gets a template's visibility and account permissions.

    .DESCRIPTION
        Wraps listTemplatePermissions. Returns whether the template is public and
        which accounts or projects it is shared with. Accepts template objects on
        the pipeline.

    .PARAMETER Id
        The template to inspect. Binds from a piped template's id.

    .EXAMPLE
        Get-CSTemplatePermission -Id $templateId
        Shows who can see and use a template.

    .EXAMPLE
        Get-CSTemplate -Name 'web-golden' | Get-CSTemplatePermission
        Pipes a template in to read its permissions.
    #>

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

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

function Set-CSTemplatePermission {
    <#
    .SYNOPSIS
        Changes a template's visibility and sharing.

    .DESCRIPTION
        Wraps updateTemplatePermissions. When granting or revoking access with
        -Accounts or -ProjectIds, also give -Operation (add, remove, or reset).
        Accepts template objects on the pipeline.

    .PARAMETER Id
        The template to change. Binds from a piped template's id.

    .PARAMETER Operation
        add, remove, or reset the listed accounts/projects

    .PARAMETER Accounts
        Accounts to add or remove (with -Operation)

    .PARAMETER ProjectIds
        Projects to add or remove (with -Operation)

    .PARAMETER IsPublic
        Whether the template is public within the domain

    .PARAMETER IsFeatured
        Whether the template is featured

    .PARAMETER IsExtractable
        Whether the template can be extracted/downloaded

    .EXAMPLE
        Set-CSTemplatePermission -Id $templateId -IsPublic $true
        Makes a template public within its domain.

    .EXAMPLE
        Get-CSTemplate -Name 'web-golden' | Set-CSTemplatePermission -Operation add -Accounts 'build','qa'
        Shares a template with two accounts.
    #>

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

        [ValidateSet('add', 'remove', 'reset')]
        [Alias('Op')]
        [string]$Operation,

        [string[]]$Accounts,

        [string[]]$ProjectIds,

        [bool]$IsPublic,

        [bool]$IsFeatured,

        [bool]$IsExtractable
    )

    process {
        if (($PSBoundParameters.ContainsKey('Accounts') -or $PSBoundParameters.ContainsKey('ProjectIds')) -and -not $PSBoundParameters.ContainsKey('Operation')) {
            throw '-Operation is required when -Accounts or -ProjectIds is specified.'
        }
        $apiParams = @{ id = $Id }
        Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
            Operation = 'op'; Accounts = 'accounts'; ProjectIds = 'projectids'; IsPublic = 'ispublic'; IsFeatured = 'isfeatured'; IsExtractable = 'isextractable'
        })
        ConvertFrom-CSResponse -Response (Invoke-CSApiRequest -Command 'updateTemplatePermissions' -Parameters $apiParams) -Command 'updateTemplatePermissions'
    }
}

function Get-CSTemplateUploadParams {
    <#
    .SYNOPSIS
        Gets the signed parameters for uploading a template image.

    .DESCRIPTION
        Wraps getUploadParamsForTemplate. Returns CloudStack's upload URL,
        signature, metadata, and upload ID; the caller then POSTs the template file
        using those values.

    .PARAMETER Name
        Name for the template

    .PARAMETER Format
        The image format: QCOW2, RAW, VHD, VHDX, OVA, or VMDK

    .PARAMETER Hypervisor
        The hypervisor the template runs on

    .PARAMETER ZoneId
        The zone to upload the template into

    .PARAMETER OsTypeId
        The OS type ID (see Get-CSOsType)

    .PARAMETER DisplayText
        Description shown in the UI

    .PARAMETER Account
        Upload on behalf of this account. Must be used with -DomainId.

    .PARAMETER DomainId
        The domain of -Account

    .PARAMETER ProjectId
        Upload into this project

    .PARAMETER Bits
        32 or 64 bit

    .PARAMETER Checksum
        Checksum of the image, as {algorithm}hash

    .PARAMETER TemplateTag
        Template tag used by service offerings to pin placement

    .PARAMETER IsDynamicallyScalable
        Template supports dynamic scaling of CPU and memory

    .PARAMETER IsExtractable
        Template can be extracted/downloaded by its owner

    .PARAMETER IsFeatured
        Mark the template as featured

    .PARAMETER IsPublic
        Make the template public within the domain

    .PARAMETER PasswordEnabled
        The template has the password-reset script installed

    .PARAMETER RequiresHvm
        The template requires hardware-assisted virtualization

    .EXAMPLE
        Get-CSTemplateUploadParams -Name 'Ubuntu 24.04' -Format QCOW2 -Hypervisor KVM -ZoneId $zoneId -OsTypeId $osId
        Gets the values needed to upload a template into a zone.
    #>

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

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

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

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

        [string]$OsTypeId,

        [string]$DisplayText,

        [string]$Account,

        [string]$DomainId,

        [string]$ProjectId,

        [ValidateSet(32, 64)]
        [int]$Bits,

        [string]$Checksum,

        [string]$TemplateTag,

        [switch]$IsDynamicallyScalable,

        [switch]$IsExtractable,

        [switch]$IsFeatured,

        [switch]$IsPublic,

        [switch]$PasswordEnabled,

        [switch]$RequiresHvm
    )

    if ($PSBoundParameters.ContainsKey('Account') -and -not $PSBoundParameters.ContainsKey('DomainId')) {
        throw '-DomainId is required when -Account is specified.'
    }
    $apiParams = @{ name = $Name; format = $Format; hypervisor = $Hypervisor; zoneid = $ZoneId }
    Add-CSOptionalParameter -ApiParameters $apiParams -BoundParameters $PSBoundParameters -Map ([ordered]@{
        OsTypeId = 'ostypeid'; DisplayText = 'displaytext'; Account = 'account'; DomainId = 'domainid'; ProjectId = 'projectid'
        Bits = 'bits'; Checksum = 'checksum'; TemplateTag = 'templatetag'; IsDynamicallyScalable = 'isdynamicallyscalable'
        IsExtractable = 'isextractable'; IsFeatured = 'isfeatured'; IsPublic = 'ispublic'; PasswordEnabled = 'passwordenabled'; RequiresHvm = 'requireshvm'
    })
    ConvertFrom-CSResponse -Response (Invoke-CSApiRequest -Command 'getUploadParamsForTemplate' -Parameters $apiParams) -Command 'getUploadParamsForTemplate'
}

function Update-CSRouterTemplate {
    <#
    .SYNOPSIS
        Upgrades virtual routers to the newest system VM template.

    .DESCRIPTION
        Wraps upgradeRouterTemplate. Scope the upgrade with one of -Id (a single
        router), -ClusterId, -PodId, -ZoneId, -DomainId, or -Account. CloudStack
        starts one async job per affected router and returns their job handles, so
        this wrapper returns that list rather than taking -Wait.

    .PARAMETER Id
        Upgrade a single router by ID

    .PARAMETER ClusterId
        Upgrade all routers in this cluster

    .PARAMETER PodId
        Upgrade all routers in this pod

    .PARAMETER ZoneId
        Upgrade all routers in this zone

    .PARAMETER Account
        Upgrade routers owned by this account. Must be used with -DomainId.

    .PARAMETER DomainId
        Upgrade routers in this domain (or the scope of -Account)

    .EXAMPLE
        Update-CSRouterTemplate -ZoneId $zoneId
        Upgrades every virtual router in a zone after a system VM template change.

    .EXAMPLE
        Get-CSRouter -Name 'r-1234-VM' | Update-CSRouterTemplate
        Upgrades a single router piped in by object.
    #>

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

        [string]$ClusterId,

        [string]$PodId,

        [string]$ZoneId,

        [string]$Account,

        [string]$DomainId
    )

    process {
        if (-not ($PSBoundParameters.ContainsKey('Id') -or $PSBoundParameters.ContainsKey('ClusterId') -or
                $PSBoundParameters.ContainsKey('PodId') -or $PSBoundParameters.ContainsKey('ZoneId') -or
                $PSBoundParameters.ContainsKey('Account') -or $PSBoundParameters.ContainsKey('DomainId'))) {
            throw 'Specify a scope with -Id, -ClusterId, -PodId, -ZoneId, -Account, or -DomainId.'
        }
        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'; ClusterId = 'clusterid'; PodId = 'podid'; ZoneId = 'zoneid'; Account = 'account'; DomainId = 'domainid'
        })
        $scope = if ($Id) { "router $Id" } elseif ($ClusterId) { "cluster $ClusterId" } elseif ($PodId) { "pod $PodId" } elseif ($ZoneId) { "zone $ZoneId" } elseif ($Account) { "account $Account" } else { "domain $DomainId" }
        if ($PSCmdlet.ShouldProcess($scope, 'Upgrade routers to the newest template')) {
            ConvertFrom-CSResponse -Response (Invoke-CSApiRequest -Command 'upgradeRouterTemplate' -Parameters $apiParams) -Command 'upgradeRouterTemplate'
        }
    }
}