Public/cloud-template.ps1

# Define aliases
#Set-Alias -Name New-CloudVM -value Invoke-CloudTemplate
#Set-Alias -Name New-CloudInstance -value Invoke-CloudTemplate

function Get-CloudTemplate {
    <#
    .SYNOPSIS
        Gets template from the Cloud Server.
     
    .DESCRIPTION
        Retrieves a list of templates. Automatically handles token refresh.
     
    .PARAMETER Name
        Optional. Filter by Template name.
     
    .EXAMPLE
        # List all templates
        Get-CloudTemplate
         
    .EXAMPLE
        # List templates matching a name string
        Get-CloudTemplate -Name "web-server-01"
    #>

    
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string]$Name,
        
        [Parameter(Mandatory = $false)]
        [int]$ID
    )
    
    # Build the URI - adjust this to match your actual API endpoint
    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/cloud/template"
    
    # Use the helper function which handles token refresh automatically
    $response = Invoke-CloudApiRequest -Uri $uri -Method Get
    
    # Extract templates from the response
    $templates = $response.template
    
    # Filter by name if specified
    if ($Name) {
        $templates = $templates | Where-Object { $_.name -like "*$Name*" }
    }
    # Filter by ID if specified - use PSBoundParameters to check if parameter was provided
    elseif ($PSBoundParameters.ContainsKey('ID')) {
            $templates = $templates | Where-Object { ($_.id -eq $ID) -and ($null -ne $_.id) }
    }

    return $templates
}

function Invoke-CloudTemplate {
    <#
    .SYNOPSIS
        Instantiates a virtual machine from a template on the Cloud Server.
     
    .DESCRIPTION
        Instantiates a virtual machine from a template. Prompts for custom inputs if required.
        Allows overriding CPU, memory, and disk size. Automatically handles token refresh.
     
    .PARAMETER ID
        Required. ID of template to be instantiated.
     
    .PARAMETER Name
        Optional. Name of the new VM to be instantiated - optional if JSON is used.
     
    .PARAMETER VCPU
        Optional. Number of virtual CPUs (overrides template default).
     
    .PARAMETER Memory
        Optional. Memory in MB (overrides template default).
     
    .PARAMETER DiskSize
        Optional. Disk size in MB (overrides template default for DISK 0).
     
    .PARAMETER CPU
        Optional. CPU allocation (e.g., 0.2, 1.0) (overrides template default).
     
    .PARAMETER Hold
        Optional. Whether to hold the VM after instantiation. Default is false.
     
    .PARAMETER DiskCopy
        Optional. Whether to copy disk. Default is false.
     
    .PARAMETER JSON
        Optional. Path to a JSON file containing VM configuration parameters.
        When specified, most other parameters are ignored (except ID).
     
    .EXAMPLE
        # Create an instance from a template
        Invoke-CloudTemplate -ID 31 -Name "MyNewVM"
         
    .EXAMPLE
        # Create an instance from a template with specific, parameterized overrides
        Invoke-CloudTemplate -ID 31 -Name "MyNewVM" -VCPU 4 -Memory 4096 -DiskSize 20480
         
    .EXAMPLE
        # Create an instance from a template with an override configuration from a JSON file
        Invoke-CloudTemplate -ID 31 -JSON "C:\configs\myvm.json"
         
        Example JSON contents:
        {
          "NAME": "MyNewVM",
          "CPU": "0.3",
          "VCPU": 4,
          "MEMORY": 4096,
          "DISK": {
            "SIZE": "20480"
          },
          "NIC": {
            "METHOD": "DHCP",
            "NETWORK": "Dev",
            "SECURITY_GROUPS": "0,100"
          },
          "GRAPHICS": {
            "LISTEN": "0.0.0.0",
            "TYPE": "VNC",
            "RANDOM_PASSWD": "NO"
          }
        }
    #>

    
    [CmdletBinding()]
    #[Alias("new-cloudvm","new-cloudinstance")]
    param(
        [Parameter(Mandatory = $true)]
        [int]$ID,
        
        [Parameter(Mandatory = $false)]
        [string]$Name,
        
        [Parameter(Mandatory = $false)]
        [int]$VCPU,
        
        [Parameter(Mandatory = $false)]
        [int]$Memory,
        
        [Parameter(Mandatory = $false)]
        [int]$DiskSize,
        
        [Parameter(Mandatory = $false)]
        [decimal]$CPU,
        
        [Parameter(Mandatory = $false)]
        [bool]$Hold = $false,
        
        [Parameter(Mandatory = $false)]
        [bool]$DiskCopy = $false,
        
        [Parameter(Mandatory = $false)]
        [string]$JSON
    )
    
    # If JSON file is provided, use that configuration
    if ($JSON) {
        if (-not (Test-Path $JSON)) {
            throw "JSON file not found: $JSON"
        }
        
        Write-Host "Loading configuration from JSON file: $JSON" -ForegroundColor Cyan
        
        try {
            $jsonContent = Get-Content -Path $JSON -Raw | ConvertFrom-Json
            
            # If DISK object exists with SIZE but no IMAGE_ID, fetch from template
            if ($jsonContent.DISK -and $jsonContent.DISK.SIZE -and -not $jsonContent.DISK.IMAGE_ID) {
                Write-Verbose "DISK.SIZE specified without IMAGE_ID, fetching from template..."
                
                # Get template details to extract IMAGE_ID
                $templateUri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/cloud/template/${ID}"
                Write-Verbose "Getting template details from: $templateUri"
                $templateResponse = Invoke-CloudApiRequest -Uri $templateUri -Method Get
                
                # Extract IMAGE_ID from template_text (it's a string, not a parsed object)
                if ($templateResponse.template.template_text) {
                    $templateText = $templateResponse.template.template_text
                    Write-Verbose "Parsing template_text for IMAGE_ID..."
                    
                    # Use regex to find IMAGE_ID in the DISK line
                    if ($templateText -match 'IMAGE_ID\s*=\s*(\d+)') {
                        $imageId = $matches[1]
                        Write-Host "Auto-detected IMAGE_ID from template: $imageId" -ForegroundColor Cyan
                        # Add IMAGE_ID to the JSON object
                        $jsonContent.DISK | Add-Member -NotePropertyName "IMAGE_ID" -NotePropertyValue $imageId -Force
                    }
                    else {
                        Write-Warning "Could not find IMAGE_ID in template_text"
                    }
                }
                else {
                    Write-Warning "Could not find template_text in template response"
                }
            }
            
            # Extract Name from JSON if not provided as parameter
            if (-not $Name -and $jsonContent.NAME) {
                $Name = $jsonContent.NAME
            }
            elseif (-not $Name) {
                throw "Name must be specified either as a parameter or in the JSON file (NAME property)"
            }
            
            # Build DATA string from JSON properties
            $dataComponents = @()
            
            # Map common properties
            if ($jsonContent.VCPU) { $dataComponents += "VCPU = $($jsonContent.VCPU)" }
            if ($jsonContent.MEMORY) { $dataComponents += "MEMORY = $($jsonContent.MEMORY)" }
            if ($jsonContent.CPU) { $dataComponents += "CPU = $($jsonContent.CPU)" }
            
            # Handle DISK_SIZE from JSON or parameter (parameter takes precedence)
            # Don't process DISK_SIZE if there's a full DISK object
            if ($PSBoundParameters.ContainsKey('DiskSize')) {
                $dataComponents += "DISK_SIZE = $DiskSize"
                Write-Verbose "Setting DISK_SIZE to $DiskSize MB (from parameter)"
            }
            elseif ($jsonContent.DISK_SIZE -and -not ($jsonContent.DISK -and $jsonContent.DISK -is [PSCustomObject])) {
                $dataComponents += "DISK_SIZE = $($jsonContent.DISK_SIZE)"
                Write-Verbose "Setting DISK_SIZE to $($jsonContent.DISK_SIZE) MB (from JSON)"
            }
            
            # Handle full DISK configuration (only if it's a full object with multiple properties)
            if ($jsonContent.DISK -and $jsonContent.DISK -is [PSCustomObject]) {
                $diskParts = @()
                $jsonContent.DISK.PSObject.Properties | ForEach-Object {
                    Write-Verbose "Processing DISK property: $($_.Name) = $($_.Value)"
                    # Quote string values that contain spaces or are clearly strings
                    if ($_.Value -is [string] -and ($_.Value -match '\s' -or $_.Name -in @('DATASTORE', 'IMAGE', 'DISK_TYPE', 'DRIVER', 'FORMAT', 'POOL_NAME', 'SOURCE', 'TARGET', 'TM_MAD', 'TM_MAD_SYSTEM', 'TYPE', 'CLONE', 'CLONE_TARGET', 'LN_TARGET', 'READONLY', 'SAVE'))) {
                        $diskParts += "$($_.Name) = `"$($_.Value)`""
                    } else {
                        $diskParts += "$($_.Name) = $($_.Value)"
                    }
                }
                if ($diskParts.Count -gt 0) {
                    $diskString = $diskParts -join ", "
                    $dataComponents += "DISK = [ $diskString ]"
                    Write-Verbose "Added DISK to data: DISK = [ $diskString ]"
                }
            }
            
            # Handle NIC configuration
            if ($jsonContent.NIC) {
                $nicParts = @()
                $jsonContent.NIC.PSObject.Properties | ForEach-Object {
                    # Quote string values that contain spaces or are clearly strings (like IP addresses, network names, MACs)
                    # Also quote values with commas (like SECURITY_GROUPS)
                    if ($_.Value -is [string] -and ($_.Value -match '\s' -or $_.Value -match ',' -or $_.Name -in @('IP', 'MAC', 'NETWORK', 'BRIDGE', 'BRIDGE_TYPE', 'GATEWAY', 'METHOD', 'IP6_METHOD', 'MODEL', 'NAME', 'TARGET', 'VN_MAD', 'PCI_TYPE', 'SECURITY_GROUPS'))) {
                        $nicParts += "$($_.Name) = `"$($_.Value)`""
                    } else {
                        $nicParts += "$($_.Name) = $($_.Value)"
                    }
                }
                $nicString = $nicParts -join ", "
                $dataComponents += "NIC = [ $nicString ]"
            }
            
            # Handle GRAPHICS configuration
            if ($jsonContent.GRAPHICS) {
                $graphicsParts = @()
                $jsonContent.GRAPHICS.PSObject.Properties | ForEach-Object {
                    # Quote string values (LISTEN, TYPE)
                    if ($_.Name -in @('LISTEN', 'TYPE')) {
                        $graphicsParts += "$($_.Name) = `"$($_.Value)`""
                    } else {
                        $graphicsParts += "$($_.Name) = $($_.Value)"
                    }
                }
                $graphicsString = $graphicsParts -join ", "
                $dataComponents += "GRAPHICS = [ $graphicsString ]"
            }
            
            # Handle CONTEXT configuration
            if ($jsonContent.CONTEXT) {
                $contextParts = @()
                $jsonContent.CONTEXT.PSObject.Properties | ForEach-Object {
                    # Most context values should be quoted
                    if ($_.Value -is [string]) {
                        $contextParts += "$($_.Name) = `"$($_.Value)`""
                    } else {
                        $contextParts += "$($_.Name) = $($_.Value)"
                    }
                }
                $contextString = $contextParts -join ", "
                $dataComponents += "CONTEXT = [ $contextString ]"
            }
            
            # Add any additional custom properties from JSON (excluding complex objects)
            $standardProps = @('NAME', 'VCPU', 'MEMORY', 'CPU', 'DISK', 'DISK_SIZE', 'CONTEXT', 'GRAPHICS', 'NIC', 'OS', 'SECURITY_GROUP_RULE', 'TEMPLATE_ID', 'VMID', 'NIC_DEFAULT', 'CPU_MODEL', 'HOLD', 'DISK_COPY')
            $jsonContent.PSObject.Properties | Where-Object { $_.Name -notin $standardProps } | ForEach-Object {
                if ($_.Value -is [string] -or $_.Value -is [int] -or $_.Value -is [decimal]) {
                    # Quote string values
                    if ($_.Value -is [string]) {
                        $dataComponents += "$($_.Name) = `"$($_.Value)`""
                    } else {
                        $dataComponents += "$($_.Name) = $($_.Value)"
                    }
                }
            }
            
            $instantiatedata = $dataComponents -join "`n"
            
            # Override Hold and DiskCopy from JSON if present
            if ($jsonContent.PSObject.Properties.Name -contains 'HOLD') {
                $Hold = [bool]$jsonContent.HOLD
            }
            if ($jsonContent.PSObject.Properties.Name -contains 'DISK_COPY') {
                $DiskCopy = [bool]$jsonContent.DISK_COPY
            }
            
            Write-Host "Configuration loaded from JSON:" -ForegroundColor Cyan
            Write-Host "--- DATA being sent ---" -ForegroundColor Yellow
            Write-Host $instantiatedata -ForegroundColor Gray
            Write-Host "--- END DATA ---" -ForegroundColor Yellow
            
        }
        catch {
            throw "Failed to parse JSON file: $_"
        }
    }
    else {
        # Original logic for non-JSON mode
        if (-not $Name) {
            throw "Name parameter is required when not using JSON file"
        }
        
        # First, get the template details to check for required inputs
        $templateUri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/cloud/template/${ID}"
        
        Write-Verbose "Getting template details from: $templateUri"
        $templateResponse = Invoke-CloudApiRequest -Uri $templateUri -Method Get
        
        # Extract inputs_order if it exists
        $inputsraw = $null
        if ($templateResponse.template.template.values.PSObject.Properties.Name -contains 'inputs_order') {
            $inputsraw = $templateResponse.template.template.values.inputs_order
        }
        
        # Initialize array to hold all DATA components
        $dataComponents = @()
        
        # Add resource overrides if specified
        if ($PSBoundParameters.ContainsKey('VCPU')) {
            $dataComponents += "VCPU = $VCPU"
            Write-Verbose "Setting VCPU to $VCPU"
        }
        
        if ($PSBoundParameters.ContainsKey('Memory')) {
            $dataComponents += "MEMORY = $Memory"
            Write-Verbose "Setting MEMORY to $Memory MB"
        }
        
        if ($PSBoundParameters.ContainsKey('CPU')) {
            $dataComponents += "CPU = $CPU"
            Write-Verbose "Setting CPU to $CPU"
        }
        
        if ($PSBoundParameters.ContainsKey('DiskSize')) {
            $dataComponents += "DISK_SIZE = $DiskSize"
            Write-Verbose "Setting DISK_SIZE to $DiskSize MB"
        }
        
        # Process custom inputs if they exist
        if ($inputsraw -and $inputsraw.Trim() -ne "") {
            Write-Host "Template requires custom inputs..." -ForegroundColor Cyan
            
            $custominputs = $inputsraw.Split(",") | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
            
            if ($custominputs.Count -gt 0) {
                # Remove dynamic variables if they already exist
                $custominputs | ForEach-Object { 
                    Remove-Variable -Name $_ -ErrorAction SilentlyContinue
                }
                
                # Request user inputs for each required input
                foreach ($input in $custominputs) {
                    $value = Read-Host "Please enter value for $input"
                    New-Variable -Name $input -Value $value -Force
                }
                
                # Add custom inputs to data components
                foreach ($input in $custominputs) {
                    $var = Get-Variable -Name $input -ErrorAction SilentlyContinue
                    if ($var) {
                        $dataComponents += "$($var.Name) = `"$($var.Value)`""
                    }
                }
            }
        }
        else {
            Write-Host "Template does not require custom inputs." -ForegroundColor Green
        }
        
        # Combine all DATA components with newlines (OpenNebula template format)
        $instantiatedata = $dataComponents -join "`n"
        
        if ($instantiatedata) {
            Write-Host "Configuration overrides:" -ForegroundColor Cyan
            Write-Host $instantiatedata -ForegroundColor Gray
        }
    }
    
    # Build the instantiation object (hashtable, NOT JSON string)
    # Invoke-CloudApiRequest will convert it to JSON
    $instantiateBody = @{
        NAME = $Name
        DATA = $instantiatedata
        HOLD = $Hold
        DISK_COPY = $DiskCopy
    }
    
    Write-Verbose "Instantiation body: $($instantiateBody | ConvertTo-Json)"
    
    # Build the instantiate URI
    $instantiateUri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/cloud/template/${ID}/instantiate"
    
    Write-Host "Instantiating VM '$Name' from template ID $ID..." -ForegroundColor Yellow
    
    # Pass the hashtable - Invoke-CloudApiRequest will convert to JSON
    $response = Invoke-CloudApiRequest -Uri $instantiateUri -Method PATCH -Body $instantiateBody
    $vmid = ($response).template
    Write-Host "VM instantiation request completed successfully! VM ID: $vmid" -ForegroundColor Green
}

function Rename-CloudTemplate {
    <#
    .SYNOPSIS
        Rename a virtual template in the Cloud Server.
     
    .DESCRIPTION
        Renames a virtual template. Automatically handles token refresh.
     
    .PARAMETER Name
        New name of the template
         
    .PARAMETER ID
        ID of the template
             
    .EXAMPLE
        # Rename a template
        Rename-CloudTemplate -Name "new-name" -ID 5
    #>

    
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,
        
        [Parameter(Mandatory = $true)]
        [int]$ID
    )
        
    $newname = @{
        name = $Name
    }
    
    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/cloud/template/${ID}/name"
       
    $response = Invoke-CloudApiRequest -Uri $uri -Method Patch -Body $newname
    
    return $response
}

function Remove-CloudTemplate {
    <#
    .SYNOPSIS
        Removes a template from the Cloud Server.
     
    .DESCRIPTION
        Removes a template. Automatically handles token refresh.
     
    .PARAMETER Name
        Required. Template ID.
     
    .EXAMPLE
        # Remove template with confirmation prompt (default behavior):
        Remove-CloudTemplate -ID 3
        Confirm
        Are you sure you want to perform this action?
        Performing the operation "Remove Template" on target "Template ID 3 (MyTemplate)".
        [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): Y
 
    .EXAMPLE
        # Remove template without confirmation prompt:
        Remove-CloudTemplate -ID 3 -Confirm:$false
    #>

    
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [int]$ID
    )
    
    process {
        $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/cloud/template/${ID}"
        
        Invoke-CloudResourceRemoval `
            -CallerPSCmdlet $PSCmdlet `
            -ResourceType "Template" `
            -ID $ID `
            -Uri $uri `
            -GetResourceScript { Get-CloudTemplate -ID $ID }
    }
}

function Lock-CloudTemplate {
    <#
    .SYNOPSIS
        Lock a virtual template in the Cloud Server.
     
    .DESCRIPTION
        Lock a virtual template. Automatically handles token refresh.
        
    .PARAMETER ID
        ID of the template
 
    .PARAMETER Level
        Lock level (USE, MANAGE, ADMIN, ALL)
 
    .PARAMETER Test
        When set, performs a test lock instead of a real one.
 
    .EXAMPLE
        # Lock a template
        Lock-CloudTemplate -ID 5 -Level ADMIN
 
    .EXAMPLE
        # Test locking a template
        Lock-CloudTemplate -ID 5 -Level USE -Test
 
    #>

    
    [CmdletBinding()]
    param(
        
        [Parameter(Mandatory = $true)]
        [int]$ID,
        
        [Parameter(Mandatory = $true)]
        [ValidateSet('USE','MANAGE','ADMIN','ALL')]
        [string]$Level,
        
        [switch]$Test
    )

    $lockdata = @{
        level = $Level
        test  = [bool]$Test
    }
    
    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/cloud/template/${ID}/lock"
    $response = Invoke-CloudApiRequest -Uri $uri -Method Patch -Body $lockdata
    return $response
}

function Unlock-CloudTemplate {
    <#
    .SYNOPSIS
        Unlock a virtual template in the Cloud Server.
     
    .DESCRIPTION
        Unlock a virtual template. Automatically handles token refresh.
        
    .PARAMETER ID
        ID of the template
 
    .EXAMPLE
        # Unlock a template
        Unlock-CloudTemplate -ID 5
 
    #>

    
    [CmdletBinding()]
    param(
        
        [Parameter(Mandatory = $true)]
        [int]$ID        

    )
   
    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/cloud/template/${ID}/unlock"
    $response = Invoke-CloudApiRequest -Uri $uri -Method Patch #-Body $lockdata
    return $response
}
 
function New-CloudTemplate {
   <#
    .SYNOPSIS
        Creates a new VM template on the Cloud Server.
     
    .DESCRIPTION
        Creates a new VM template using an existing image. Supports customization of
        CPU, memory, network, and other VM settings.
     
    .PARAMETER Name
        Required. Name of the new template.
     
    .PARAMETER ImageID
        Required. ID of the image to use as the boot disk.
     
    .PARAMETER CPU
        Optional. CPU allocation (default: 1). Can be fractional (e.g., 0.5).
     
    .PARAMETER VCPU
        Optional. Number of virtual CPUs (default: 2).
     
    .PARAMETER Memory
        Optional. Memory in MB (default: 4096).
     
    .PARAMETER NetworkID
        Optional. Network ID to attach. If not specified, no network is attached.
     
    .PARAMETER NetworkName
        Optional. Network name to attach (alternative to NetworkID).
     
    .PARAMETER SecurityGroupIDs
        Optional. Comma-separated security group IDs (e.g., "0,100").
     
    .PARAMETER CDROMID
        Optional. Image ID for a CDROM/ISO to attach.
     
    .PARAMETER Description
        Optional. Description for the template.
     
    .PARAMETER EnableVNC
        Optional. Enable VNC graphics (default: true).
     
    .PARAMETER EnableCloudInit
        Optional. Enable cloud-init context (default: true).
     
    .PARAMETER SSHPublicKey
        Optional. SSH public key to inject via cloud-init.
     
    .PARAMETER Logo
        Optional. Path to logo image (e.g., "images/logos/ubuntu.png").
     
    .EXAMPLE
        # Basic Ubuntu template
        New-CloudTemplate -Name "Ubuntu-22.04-Base" -ImageID 57 -CPU 1 -Memory 2048
         
    .EXAMPLE
        # Template with networking without allowing VNC
        New-CloudTemplate -Name "Web-Server-Template" -ImageID 57 -NetworkID 2 -SecurityGroupIDs "0,100" -EnableVNC:$false
         
    .EXAMPLE
        # Template with CDROM
        New-CloudTemplate -Name "Ubuntu-Install" -ImageID 50 -CDROMID 49 -CPU 2 -VCPU 4 -Memory 8192
         
    .EXAMPLE
        # Template with SSH key
        $sshKey = Get-Content ~/.ssh/id_rsa.pub -Raw
        New-CloudTemplate -Name "Dev-Server" -ImageID 57 -NetworkID 2 -SSHPublicKey $sshKey
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,
        
        [Parameter(Mandatory = $true)]
        [int]$ImageID,
        
        [Parameter(Mandatory = $false)]
        [double]$CPU = 1,
        
        [Parameter(Mandatory = $false)]
        [int]$VCPU = 2,
        
        [Parameter(Mandatory = $false)]
        [int]$Memory = 4096,
        
        [Parameter(Mandatory = $false)]
        [int]$NetworkID,
        
        [Parameter(Mandatory = $false)]
        [string]$NetworkName,
        
        [Parameter(Mandatory = $false)]
        [string]$SecurityGroupIDs,
        
        [Parameter(Mandatory = $false)]
        [int]$CDROMID,
        
        [Parameter(Mandatory = $false)]
        [string]$Description,
        
        [Parameter(Mandatory = $false)]
        [bool]$EnableVNC = $true,
        
        [Parameter(Mandatory = $false)]
        [bool]$EnableCloudInit = $true,
        
        [Parameter(Mandatory = $false)]
        [string]$SSHPublicKey,
        
        [Parameter(Mandatory = $false)]
        [string]$Logo
    )
    
    # Build the template string - NAME MUST be included here
    $templateParts = @()
    
    # NAME is first
    $templateParts += "NAME = $Name"
    
    # Basic settings
    $templateParts += "CPU = $CPU"
    $templateParts += "VCPU = $VCPU"
    $templateParts += "MEMORY = $Memory"
    $templateParts += "HYPERVISOR = kvm"
    
    # Description
    if ($Description) {
        $templateParts += "DESCRIPTION = `"$Description`""
    }
    
    # Logo
    if ($Logo) {
        $templateParts += "LOGO = $Logo"
    }
    
    # Boot disk
    $diskParts = @("IMAGE_ID = $ImageID")
    $templateParts += "DISK = [ $($diskParts -join ', ') ]"
    
    # CDROM if specified
    if ($CDROMID) {
        $cdromParts = @("IMAGE_ID = $CDROMID", "TYPE = CDROM")
        $templateParts += "DISK = [ $($cdromParts -join ', ') ]"
    }
    
    # Network if specified
    if ($NetworkID -or $NetworkName) {
        $nicParts = @()
        $nicParts += "MODEL = virtio"
        $nicParts += "NAME = NIC0"
        
        if ($NetworkID) {
            $nicParts += "NETWORK_ID = $NetworkID"
        } elseif ($NetworkName) {
            $nicParts += "NETWORK = `"$NetworkName`""
        }
        
        if ($SecurityGroupIDs) {
            $nicParts += "SECURITY_GROUPS = `"$SecurityGroupIDs`""
        }
        
        $templateParts += "NIC = [ $($nicParts -join ', ') ]"
    }
    
    # Graphics (VNC)
    if ($EnableVNC) {
        $graphicsParts = @(
            "LISTEN = 0.0.0.0",
            "TYPE = VNC",
            "RANDOM_PASSWD = NO"
        )
        $templateParts += "GRAPHICS = [ $($graphicsParts -join ', ') ]"
    }
    
    # Context (Cloud-init)
    if ($EnableCloudInit) {
        $contextParts = @(
            "NETWORK = YES",
            "REPORT_READY = NO"
        )
        
        if ($SSHPublicKey) {
            # Escape quotes in SSH key
            $escapedKey = $SSHPublicKey.Trim() -replace '"', '\"'
            $contextParts += "SSH_PUBLIC_KEY = `"$escapedKey`""
        } else {
            $contextParts += "SSH_PUBLIC_KEY = `"`$USER[SSH_PUBLIC_KEY]`""
        }
        
        $contextParts += "TOKEN = YES"
        
        $templateParts += "CONTEXT = [ $($contextParts -join ', ') ]"
    }
    
    # OS settings
    $templateParts += "OS = [ FIRMWARE_SECURE = NO ]"
    
    # RAW settings
    $templateParts += "RAW = [ TYPE = kvm, VALIDATE = NO ]"
    
    # Memory resize mode
    $templateParts += "MEMORY_RESIZE_MODE = BALLOONING"
    
    # Hot resize settings
    $templateParts += "HOT_RESIZE = [ CPU_HOT_ADD_ENABLED = NO, MEMORY_HOT_ADD_ENABLED = NO ]"
    
    # Backup config
    $templateParts += "BACKUP_CONFIG = [ BACKUP_VOLATILE = NO ]"
    
    # Join all parts with newlines
    $templateString = $templateParts -join "`n"
    
    # Build the configuration - everything goes in "data" field per API spec
    $config = [PSCustomObject]@{
        data = $templateString
    }
    
    # Build the URI
    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/cloud/template"
    
    Write-Verbose "Creating template '$Name'"
    Write-Verbose "Image ID: $ImageID"
    Write-Verbose "CPU: $CPU, VCPU: $VCPU, Memory: ${Memory}MB"
    Write-Verbose "Request body: $($config | ConvertTo-Json -Compress)"
    
    try {
        # Use the helper function which handles token refresh automatically
        $response = Invoke-CloudApiRequest -Uri $uri -Method Post -Body $config
        
        Write-Host "Template '$Name' created successfully (ID: $($response.template))." -ForegroundColor Green
        return $response
    }
    catch {
        Write-Error "Failed to create template: $_"
        throw
    }
}

function Copy-CloudTemplate {
    <#
    .SYNOPSIS
        Clone a template in the Cloud Server.
     
    .DESCRIPTION
        Clones a template. Automatically handles token refresh.
     
    .PARAMETER Name
        Required. Name of the new template
         
    .PARAMETER ID
        Required. ID of the the template you wish to clone
         
    .PARAMETER CloneDisk
        Define whether or not to clone the disk. Defaults to false
 
    .EXAMPLE
        # Clone the template and do not clone the associated image
        Copy-CloudTemplate -Name "gateway-template" -ID 5
 
    .EXAMPLE
        # Clone the template and clone the associated image
        Copy-CloudTemplate -Name "gateway-template" -ID 5 -CloneDisk
 
    #>

    
    [CmdletBinding(DefaultParameterSetName='Individual')]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,
        
        [Parameter(Mandatory = $true)]
        [int]$ID,
                
        [Parameter(Mandatory = $false)]
        [switch]$cloneDisk
    )

    if ($cloneDisk) {
        $clone = $true
    }
    
    else {
        $clone = $false
    }
    

    $templateData = [PSCustomObject]@{
    NAME=$Name
    DISK=$clone
    }
 
    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/cloud/template/${ID}/clone"
    
    Write-Verbose "Request URI: $uri"
    Write-Verbose "Template:`n$TemplateData"
    
    $response = Invoke-CloudApiRequest -Uri $uri -Method POST -Body $templateData
    
    return $response
}
 
function Update-CloudTemplateOwner {
    <#
    .SYNOPSIS
        Updates the ownership of a template in the Cloud Server.
     
    .DESCRIPTION
        Changes the user and/or group ownership of a template. Prompts for confirmation.
     
    .PARAMETER ID
        Required. ID of the template.
     
    .PARAMETER UserID
        Required. ID of the user to own the template.
         
    .PARAMETER GroupID
        Optional. ID of the group to own the template.
         
    .EXAMPLE
        # Change the ownership of a template to a specific user
        Update-CloudTemplateOwner -ID 5 -UserID 2
         
    .EXAMPLE
        # Change the ownership of a template to a specific user and group
        Update-CloudTemplateOwner -ID 5 -UserID 2 -GroupID 100
         
    .EXAMPLE
        # Change the ownership of a template to a specific user, bypassing the confirmation prompt
        Update-CloudTemplateOwner -ID 5 -UserID 2 -Confirm:$false
    #>

    
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [int]$ID,
        
        [Parameter(Mandatory = $true)]
        [Alias('User')]
        [int]$UserID,
                
        [Parameter(Mandatory = $false)]
        [Alias('Group')]
        [int]$GroupID
    )
    
    process {
        # Build the update body
        $body = [PSCustomObject]@{
            user = $UserID
        }
        
        if ($PSBoundParameters.ContainsKey('GroupID')) {
            $body | Add-Member -NotePropertyName 'group' -NotePropertyValue $GroupID
        }
        
        $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/cloud/template/${ID}/ownership"
        
        # Build action description for confirmation
        $actionParts = @("Change owner to User ID $UserID")
        if ($PSBoundParameters.ContainsKey('GroupID')) {
            $actionParts += "Group ID $GroupID"
        }
        $actionDescription = $actionParts -join " and "
        
        # Use the helper function
        Invoke-CloudResourceUpdate `
            -CallerPSCmdlet $PSCmdlet `
            -ResourceType "Template" `
            -ID $ID `
            -Uri $uri `
            -Body $body `
            -Action $actionDescription `
            -GetResourceScript { Get-CloudTemplate -ID $ID } `
            -SuccessMessage "Template $ID ownership updated successfully."
    }
}

function Update-CloudTemplatePermissions {
    <#
    .SYNOPSIS
        Updates the permissions of a template in the Cloud Server.
     
    .DESCRIPTION
        Changes the permission of a template. Prompts for confirmation.
     
    .PARAMETER ID
        Required. ID of the template.
     
    .PARAMETER OwnerUse
        Optional. True or false, enable OwnerUse
 
    .PARAMETER OwnerManage
        Optional. True or false, enable OwnerManage
 
    .PARAMETER OwnerAdmin
        Optional. True or false, enable OwnerAdmin
 
    .PARAMETER GroupUse
        Optional. True or false, enable GroupUse
         
    .PARAMETER GroupManage
        Optional. True or false, enable GroupManage
         
    .PARAMETER GroupAdmin
        Optional. True or false, enable GroupAdmin
         
    .PARAMETER OtherUse
        Optional. True or false, enable OtherUse
         
    .PARAMETER OtherManage
        Optional. True or false, enable OtherManage
         
    .PARAMETER OtherAdmin
        Optional. True or false, enable OtherManage
         
    .EXAMPLE
        # Give group users permission to use the template
        Update-CloudTemplatePermissions -ID 35 -GroupUse $true
         
    .EXAMPLE
        # Deny group admin permission to the template
        Update-CloudTemplatePermissions -ID 35 -GroupAdmin $false
         
    .EXAMPLE
        # Set multiple permissions at once
        Update-CloudTemplatePermissions -ID 35 -GroupUse $true -GroupManage $true -OtherUse $true
    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [int]$ID,
        
        [Parameter(Mandatory = $false)]
        [bool]$OwnerUse,
        
        [Parameter(Mandatory = $false)]
        [bool]$OwnerManage,
        
        [Parameter(Mandatory = $false)]
        [bool]$OwnerAdmin,
        
        [Parameter(Mandatory = $false)]
        [bool]$GroupUse,
        
        [Parameter(Mandatory = $false)]
        [bool]$GroupManage,
        
        [Parameter(Mandatory = $false)]
        [bool]$GroupAdmin,
        
        [Parameter(Mandatory = $false)]
        [bool]$OtherUse,
        
        [Parameter(Mandatory = $false)]
        [bool]$OtherManage,
        
        [Parameter(Mandatory = $false)]
        [bool]$OtherAdmin
    )
    
    process {
        # Build the permissions object structure as shown in the API spec
        $permissions = @{}
        
        if ($PSBoundParameters.ContainsKey('OwnerUse')) { $permissions['owner_use'] = $OwnerUse }
        if ($PSBoundParameters.ContainsKey('OwnerManage')) { $permissions['owner_manage'] = $OwnerManage }
        if ($PSBoundParameters.ContainsKey('OwnerAdmin')) { $permissions['owner_admin'] = $OwnerAdmin }
        
        if ($PSBoundParameters.ContainsKey('GroupUse')) { $permissions['group_use'] = $GroupUse }
        if ($PSBoundParameters.ContainsKey('GroupManage')) { $permissions['group_manage'] = $GroupManage }
        if ($PSBoundParameters.ContainsKey('GroupAdmin')) { $permissions['group_admin'] = $GroupAdmin }
        
        if ($PSBoundParameters.ContainsKey('OtherUse')) { $permissions['other_use'] = $OtherUse }
        if ($PSBoundParameters.ContainsKey('OtherManage')) { $permissions['other_manage'] = $OtherManage }
        if ($PSBoundParameters.ContainsKey('OtherAdmin')) { $permissions['other_admin'] = $OtherAdmin }
        
        # Create the body with the permissions nested structure
        $body = @{
            permissions = $permissions
        }
        
        $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/cloud/template/${ID}/permissions"
        
        Invoke-CloudResourceUpdate `
            -CallerPSCmdlet $PSCmdlet `
            -ResourceType "Template" `
            -ID $ID `
            -Uri $uri `
            -Body $body `
            -Action "Update permissions" `
            -GetResourceScript { Get-CloudTemplate -ID $ID }
    }
}

function Update-CloudTemplate {
    <#
    .SYNOPSIS
        Updates an existing VM template on the Cloud Server.
     
    .DESCRIPTION
        Updates a VM template's configuration. You can choose to merge with the existing template
        or replace it entirely. Merge is enabled by default. Automatically handles token refresh.
     
    .PARAMETER ID
        Required. ID of the template to update.
     
    .PARAMETER Name
        Optional. New name for the template.
     
    .PARAMETER ImageID
        Optional. ID of the image to use as the boot disk.
     
    .PARAMETER CPU
        Optional. CPU allocation. Can be fractional (e.g., 0.5).
     
    .PARAMETER VCPU
        Optional. Number of virtual CPUs.
     
    .PARAMETER Memory
        Optional. Memory in MB.
     
    .PARAMETER NetworkID
        Optional. Network ID to attach.
     
    .PARAMETER NetworkName
        Optional. Network name to attach (alternative to NetworkID).
     
    .PARAMETER SecurityGroupIDs
        Optional. Comma-separated security group IDs (e.g., "0,100").
     
    .PARAMETER CDROMID
        Optional. Image ID for a CDROM/ISO to attach.
     
    .PARAMETER Description
        Optional. Description for the template.
     
    .PARAMETER EnableVNC
        Optional. Enable VNC graphics.
     
    .PARAMETER EnableCloudInit
        Optional. Enable cloud-init context.
     
    .PARAMETER SSHPublicKey
        Optional. SSH public key to inject via cloud-init.
     
    .PARAMETER Logo
        Optional. Path to logo image (e.g., "images/logos/ubuntu.png").
     
    .PARAMETER Merge
        Optional. If true (default), merges with existing template. If false, replaces the template entirely.
        Use -Merge:$false to completely replace the template configuration.
     
    .EXAMPLE
        # Update template memory and CPU (merge with existing)
        Update-CloudTemplate -ID 123 -CPU 2 -Memory 8192
         
    .EXAMPLE
        # Update template name and description
        Update-CloudTemplate -ID 123 -Name "Ubuntu-22.04-Production" -Description "Production web server template"
         
    .EXAMPLE
        # Add network to template
        Update-CloudTemplate -ID 123 -NetworkID 2 -SecurityGroupIDs "0,100"
         
    .EXAMPLE
        # Replace entire template configuration
        Update-CloudTemplate -ID 123 -Name "New-Template" -ImageID 57 -CPU 2 -VCPU 4 -Memory 8192 -Merge:$false
         
    .EXAMPLE
        # Add SSH key to template
        $sshKey = Get-Content ~/.ssh/id_rsa.pub -Raw
        Update-CloudTemplate -ID 123 -SSHPublicKey $sshKey
         
    .EXAMPLE
        # Disable VNC on template
        Update-CloudTemplate -ID 123 -EnableVNC:$false
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
    param(
        [Parameter(Mandatory = $true)]
        [int]$ID,
        
        [Parameter(Mandatory = $false)]
        [string]$Name,
        
        [Parameter(Mandatory = $false)]
        [int]$ImageID,
        
        [Parameter(Mandatory = $false)]
        [double]$CPU,
        
        [Parameter(Mandatory = $false)]
        [int]$VCPU,
        
        [Parameter(Mandatory = $false)]
        [int]$Memory,
        
        [Parameter(Mandatory = $false)]
        [int]$NetworkID,
        
        [Parameter(Mandatory = $false)]
        [string]$NetworkName,
        
        [Parameter(Mandatory = $false)]
        [string]$SecurityGroupIDs,
        
        [Parameter(Mandatory = $false)]
        [int]$CDROMID,
        
        [Parameter(Mandatory = $false)]
        [string]$Description,
        
        [Parameter(Mandatory = $false)]
        [bool]$EnableVNC,
        
        [Parameter(Mandatory = $false)]
        [bool]$EnableCloudInit,
        
        [Parameter(Mandatory = $false)]
        [string]$SSHPublicKey,
        
        [Parameter(Mandatory = $false)]
        [string]$Logo,
        
        [Parameter()]
        [bool]$Merge = $true
    )
    
    # Build the template string - only include specified parameters
    $templateParts = @()
    
    # Only add parameters that were actually specified
    if ($PSBoundParameters.ContainsKey('Name')) {
        $templateParts += "NAME = $Name"
    }
    
    if ($PSBoundParameters.ContainsKey('CPU')) {
        $templateParts += "CPU = $CPU"
    }
    
    if ($PSBoundParameters.ContainsKey('VCPU')) {
        $templateParts += "VCPU = $VCPU"
    }
    
    if ($PSBoundParameters.ContainsKey('Memory')) {
        $templateParts += "MEMORY = $Memory"
    }
    
    # Description
    if ($PSBoundParameters.ContainsKey('Description')) {
        $templateParts += "DESCRIPTION = `"$Description`""
    }
    
    # Logo
    if ($PSBoundParameters.ContainsKey('Logo')) {
        $templateParts += "LOGO = $Logo"
    }
    
    # Boot disk
    if ($PSBoundParameters.ContainsKey('ImageID')) {
        $diskParts = @("IMAGE_ID = $ImageID")
        $templateParts += "DISK = [ $($diskParts -join ', ') ]"
    }
    
    # CDROM if specified
    if ($PSBoundParameters.ContainsKey('CDROMID')) {
        $cdromParts = @("IMAGE_ID = $CDROMID", "TYPE = CDROM")
        $templateParts += "DISK = [ $($cdromParts -join ', ') ]"
    }
    
    # Network if specified
    if ($PSBoundParameters.ContainsKey('NetworkID') -or $PSBoundParameters.ContainsKey('NetworkName')) {
        $nicParts = @()
        $nicParts += "MODEL = virtio"
        $nicParts += "NAME = NIC0"
        
        if ($PSBoundParameters.ContainsKey('NetworkID')) {
            $nicParts += "NETWORK_ID = $NetworkID"
        } elseif ($PSBoundParameters.ContainsKey('NetworkName')) {
            $nicParts += "NETWORK = `"$NetworkName`""
        }
        
        if ($PSBoundParameters.ContainsKey('SecurityGroupIDs')) {
            $nicParts += "SECURITY_GROUPS = `"$SecurityGroupIDs`""
        }
        
        $templateParts += "NIC = [ $($nicParts -join ', ') ]"
    }
    
    # Graphics (VNC)
    if ($PSBoundParameters.ContainsKey('EnableVNC')) {
        if ($EnableVNC) {
            $graphicsParts = @(
                "LISTEN = 0.0.0.0",
                "TYPE = VNC",
                "RANDOM_PASSWD = NO"
            )
            $templateParts += "GRAPHICS = [ $($graphicsParts -join ', ') ]"
        } else {
            # To disable VNC, we would need to remove the GRAPHICS section
            # This might require a replace operation rather than merge
            Write-Warning "Disabling VNC may require -Merge:`$false to fully remove graphics configuration"
        }
    }
    
    # Context (Cloud-init)
    if ($PSBoundParameters.ContainsKey('EnableCloudInit') -or $PSBoundParameters.ContainsKey('SSHPublicKey')) {
        $contextParts = @(
            "NETWORK = YES",
            "REPORT_READY = NO"
        )
        
        if ($PSBoundParameters.ContainsKey('SSHPublicKey')) {
            # Escape quotes in SSH key
            $escapedKey = $SSHPublicKey.Trim() -replace '"', '\"'
            $contextParts += "SSH_PUBLIC_KEY = `"$escapedKey`""
        } else {
            $contextParts += "SSH_PUBLIC_KEY = `"`$USER[SSH_PUBLIC_KEY]`""
        }
        
        $contextParts += "TOKEN = YES"
        
        $templateParts += "CONTEXT = [ $($contextParts -join ', ') ]"
    }
    
    # Only proceed if there are changes to make
    if ($templateParts.Count -eq 0) {
        Write-Warning "No parameters specified for update. Please specify at least one parameter to update."
        return
    }
    
    # Join all parts with newlines
    $templateString = $templateParts -join "`n"
    
    # Build the request body
    $body = @{
        merge = $Merge
        data = $templateString
    }
    
    # Build the URI
    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/cloud/template/${ID}"
    
    Write-Verbose "Request URI: $uri"
    Write-Verbose "Merge: $Merge"
    Write-Verbose "Template:`n$templateString"
    
    try {
        Invoke-CloudResourceUpdate `
            -CallerPSCmdlet $PSCmdlet `
            -ResourceType "Template" `
            -ID $ID `
            -Uri $uri `
            -Body $body `
            -Action "Update" `
            -GetResourceScript { Get-CloudTemplate -ID $ID }
        Write-Verbose "Successfully updated template ID: $ID"
    }
    catch {
        Write-Error "Failed to update template '$ID': $_"
        throw
    }
}