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 } } |