Public/Set-PVEVMCloudInit.ps1
|
function Set-PVEVMCloudInit { <# .SYNOPSIS Changes the cloud-init settings of QEMU virtual machines, and adds a cloud-init drive if needed. .DESCRIPTION Sends the settings with POST /nodes/{node}/qemu/{vmid}/config and writes the VM's PVEPS.CloudInit object. Only the settings given are changed. An empty value ('' or @()) removes a setting. The IP settings apply to one network adapter, -NetworkIndex (net0 by default), and are merged with that adapter's current ipconfigN: -Gateway alone keeps the current address. cloud-init needs a cloud-init drive. With -Storage, a VM without one gets one (on -DriveBus, at the lowest free index); without -Storage, such a VM gets a warning, because the settings are saved but unused until a drive is added. -Regenerate rewrites the drive's contents at once (PUT /nodes/{node}/qemu/{vmid}/cloudinit); Get-PVEVMCloudInit shows in IsPending whether the drive is out of date. .PARAMETER VMId VM ID(s). Binds from piped PVEPS.VM objects. .PARAMETER VMName VM name(s). Wildcards allowed. .PARAMETER User User whose password and SSH keys cloud-init sets, instead of the image's default user. .PARAMETER Password Password for that user. Accepts a string or a SecureString. The API documentation advises SSH keys instead, and notes that older cloud-init versions do not support hashed passwords. .PARAMETER SshPublicKey Public SSH keys (OpenSSH format), one per element. Replaces the current keys. .PARAMETER NetworkIndex Index of the network adapter the IP settings are for. Defaults to 0 (net0). .PARAMETER IPAddress IPv4 address in CIDR notation (192.0.2.10/24), or dhcp. .PARAMETER Gateway IPv4 gateway. .PARAMETER IPv6Address IPv6 address in CIDR notation, dhcp, or auto (stateless autoconfiguration; needs cloud-init 19.4 or newer according to the API documentation). .PARAMETER Gateway6 IPv6 gateway. .PARAMETER Nameserver DNS server addresses. .PARAMETER SearchDomain DNS search domains. .PARAMETER Upgrade Whether cloud-init upgrades packages after the first boot. .PARAMETER Type cloud-init data format: nocloud, configdrive2 or opennebula. By default PVE picks it from the OS type. .PARAMETER Storage Storage for a cloud-init drive, added if the VM has none. .PARAMETER DriveBus Bus for an added cloud-init drive: ide (the default), sata or scsi. .PARAMETER Regenerate Rewrite the cloud-init drive's contents after the change. .PARAMETER Server Connection(s) to use. Defaults to the default connection; binds from piped objects. .EXAMPLE Set-PVEVMCloudInit -VMId 100 -User debian -SshPublicKey (Get-Content ~/.ssh/id_ed25519.pub) -IPAddress dhcp -Storage local-lvm .EXAMPLE Set-PVEVMCloudInit -VMId 100 -IPAddress 192.0.2.10/24 -Gateway 192.0.2.1 -Nameserver 192.0.2.53 -Regenerate #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'ShouldProcess is called on this cmdlet through $PSCmdlet by Invoke-PVEGuestChange.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', 'Password', Justification = 'Deliberately accepts a string or a SecureString, as Connect-PVE does.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUsernameAndPasswordParams', '', Justification = '-User and -Password are the separate ciuser and cipassword options; either can be set alone.')] [CmdletBinding(DefaultParameterSetName = 'Id', SupportsShouldProcess, ConfirmImpact = 'Medium')] [OutputType('PVEPS.CloudInit')] param( [Parameter(Mandatory, ParameterSetName = 'Id', ValueFromPipelineByPropertyName)] [ValidateRange(100, 999999999)] [int[]]$VMId, [Parameter(Mandatory, ParameterSetName = 'Name', Position = 0)] [SupportsWildcards()] [ValidateNotNullOrEmpty()] [string[]]$VMName, [AllowEmptyString()] [string]$User, [ValidateScript({ $_ -is [string] -or $_ -is [securestring] }, ErrorMessage = 'Password must be a string or a SecureString.')] [object]$Password, [AllowEmptyCollection()] [string[]]$SshPublicKey, [ValidateRange(0, [int]::MaxValue)] [int]$NetworkIndex = 0, [AllowEmptyString()] [string]$IPAddress, [AllowEmptyString()] [string]$Gateway, [AllowEmptyString()] [string]$IPv6Address, [AllowEmptyString()] [string]$Gateway6, [AllowEmptyCollection()] [string[]]$Nameserver, [AllowEmptyCollection()] [string[]]$SearchDomain, [bool]$Upgrade, [ValidateSet('nocloud', 'configdrive2', 'opennebula')] [string]$Type, [ValidateNotNullOrEmpty()] [string]$Storage, [ValidateSet('ide', 'sata', 'scsi')] [string]$DriveBus = 'ide', [switch]$Regenerate, [Parameter(ValueFromPipelineByPropertyName)] [Alias('Connection')] [object[]]$Server ) begin { # Settings that are the same for every VM: option name -> value, $null to delete. $settings = [ordered]@{} if ($PSBoundParameters.ContainsKey('User')) { $settings.ciuser = if ($User) { $User } else { $null } } if ($null -ne $Password) { $settings.cipassword = if ($Password -is [securestring]) { ConvertFrom-PVESecureString -SecureString $Password } else { $Password } } if ($PSBoundParameters.ContainsKey('SshPublicKey')) { # The sshkeys option has the schema format 'urlencoded': PVE expects the value already URL-encoded. $settings.sshkeys = if ($SshPublicKey) { [uri]::EscapeDataString(($SshPublicKey -join "`n")) } else { $null } } if ($PSBoundParameters.ContainsKey('Nameserver')) { $settings.nameserver = if ($Nameserver) { $Nameserver -join ' ' } else { $null } } if ($PSBoundParameters.ContainsKey('SearchDomain')) { $settings.searchdomain = if ($SearchDomain) { $SearchDomain -join ' ' } else { $null } } if ($PSBoundParameters.ContainsKey('Upgrade')) { $settings.ciupgrade = [int]$Upgrade } if ($Type) { $settings.citype = $Type } $ip = [ordered]@{} foreach ($pair in @(@('IPAddress', 'ip'), @('Gateway', 'gw'), @('IPv6Address', 'ip6'), @('Gateway6', 'gw6'))) { if ($PSBoundParameters.ContainsKey($pair[0])) { $value = $PSBoundParameters[$pair[0]] $ip[$pair[1]] = if ($value) { $value } else { $null } } } if ($settings.Count -eq 0 -and $ip.Count -eq 0 -and -not $Storage -and -not $Regenerate) { $PSCmdlet.ThrowTerminatingError((New-PVEArgumentError -Message 'No change given; specify at least one setting, -Storage or -Regenerate.' -Target $null)) } $slots = @{ scsi = 31; sata = 6; ide = 4 } $targets = [System.Collections.Generic.List[psobject]]::new() } process { Add-PVEGuestTarget -Cmdlet $PSCmdlet -Target $targets -Server $Server -VMId $VMId -Name $VMName } end { foreach ($guest in @(Resolve-PVEGuestTarget -Cmdlet $PSCmdlet -Type qemu -Target $targets)) { $label = Get-PVEGuestLabel -Guest $guest try { $config = Get-PVEGuestConfig -Guest $guest $drive = @(Get-PVEGuestDevice -Guest $guest -Kind CloudInitDrive -Config $config) | Select-Object -First 1 } catch { $PSCmdlet.WriteError($_) continue } $body = @{} $remove = [System.Collections.Generic.List[string]]::new() foreach ($key in $settings.Keys) { if ($null -eq $settings[$key]) { $remove.Add($key) } else { $body[$key] = $settings[$key] } } if ($ip.Count -gt 0) { $key = "ipconfig$NetworkIndex" $option = ConvertFrom-PVEPropertyString -Value ([string](Get-PVEValue $config $key)) foreach ($part in $ip.Keys) { $option[$part] = $ip[$part] } $value = ConvertTo-PVEPropertyString -Option $option if ($value) { $body[$key] = $value } else { $remove.Add($key) } } $what = @($body.Keys) + @($remove) if (-not $drive) { if ($Storage) { try { $slot = Get-PVEFreeDeviceIndex -Prefix $DriveBus -Used @(Get-PVEConfigKey -Config $config) -Count $slots[$DriveBus] } catch { $PSCmdlet.WriteError($_) continue } $body["$DriveBus$slot"] = '{0}:cloudinit' -f $Storage $what += "cloud-init drive $DriveBus$slot on $Storage" } else { Write-Warning "$label has no cloud-init drive; the settings are saved but not used until one is added (-Storage)." } } if ($body.Count -gt 0 -or $remove.Count -gt 0) { if ($remove.Count -gt 0) { $body.delete = $remove -join ',' } $body.digest = Get-PVEValue $config 'digest' if (-not (Invoke-PVEGuestChange -Cmdlet $PSCmdlet -Guest $guest -Description "Set cloud-init: $($what -join ', ')" -Body $body)) { continue } } if ($Regenerate) { if (-not (Invoke-PVEGuestChange -Cmdlet $PSCmdlet -Guest $guest -Description 'Regenerate the cloud-init drive' -Body @{} -Endpoint cloudinit)) { continue } } try { $null = Update-PVEGuestStatus -Guest $guest Write-PVEPendingWarning -Guest $guest ConvertTo-PVECloudInit -Guest $guest -Config (Get-PVEGuestConfig -Guest $guest) } catch { $PSCmdlet.WriteError($_) } } } } |