refactor: rename Send-PveIso to Send-PveFile and move VM provisioning into integration tests

Send-PveFile replaces Send-PveIso with a -ContentType parameter
(iso, vztmpl, import). This is a breaking rename — the module has
not been released yet.

Test infrastructure refactored:
- prepare-test-vm.sh replaced with prepare-test-environment.sh
  (only SSH ops: enable content types, upload cloud-init snippet,
  curl download cloud image)
- VM provisioning moved into integration tests as "Linux VM —
  Provisioning" context: Send-PveFile, New-PveVm, Import-PveVmDisk,
  Set-PveVmConfig, Set-PveCloudInitConfig, Start-PveVm,
  Test-PveVmGuestAgent — all tested as part of the test suite
- PVETEST_LINUX_VMID env var removed (VM created by tests)
- PVETEST_CLOUD_IMAGE_PATH + PVETEST_PASSWORD added

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-20 13:39:06 -05:00
parent 16be330834
commit f2280c1c88
9 changed files with 267 additions and 192 deletions
@@ -7,13 +7,15 @@
They require a live, dedicated Proxmox VE test node and the following
environment variables to be set:
PVETEST_HOST - Hostname or IP of the PVE test node
PVETEST_PORT - API port (usually 8006)
PVETEST_APITOKEN - API token in the format USER@REALM!TOKENID=UUID
PVETEST_NODE - PVE node name (e.g. pve-test1)
PVETEST_STORAGE - Storage pool name for disk/ISO operations (e.g. local)
PVETEST_ISO_PATH - Local filesystem path to a small .iso for upload tests
PVETEST_PVE_VERSION - (optional) Expected PVE major version (8 or 9)
PVETEST_HOST - Hostname or IP of the PVE test node
PVETEST_PORT - API port (usually 8006)
PVETEST_APITOKEN - API token in the format USER@REALM!TOKENID=UUID
PVETEST_NODE - PVE node name (e.g. pve-test1)
PVETEST_STORAGE - Storage pool name for disk/ISO operations (e.g. local)
PVETEST_ISO_PATH - Local filesystem path to a small .iso for upload tests
PVETEST_PVE_VERSION - (optional) Expected PVE major version (8 or 9)
PVETEST_PASSWORD - (optional) Root password for cloud-init Linux VM provisioning
PVETEST_CLOUD_IMAGE_PATH - (optional) Local path to a cloud image (.img) for VM provisioning
WARNING: These tests CREATE and DESTROY real resources (VMs, users, tokens,
roles, snapshots, ISO uploads, etc.) on the target node.
@@ -64,8 +66,9 @@ BeforeAll {
$script:Storage = [System.Environment]::GetEnvironmentVariable('PVETEST_STORAGE')
$script:IsoPath = [System.Environment]::GetEnvironmentVariable('PVETEST_ISO_PATH')
$script:ExpectedPveVersion = [System.Environment]::GetEnvironmentVariable('PVETEST_PVE_VERSION')
$linuxVmEnv = [System.Environment]::GetEnvironmentVariable('PVETEST_LINUX_VMID')
$script:LinuxVmId = if ($linuxVmEnv) { [int]$linuxVmEnv } else { $null }
$script:Password = [System.Environment]::GetEnvironmentVariable('PVETEST_PASSWORD')
$script:CloudImagePath = [System.Environment]::GetEnvironmentVariable('PVETEST_CLOUD_IMAGE_PATH')
$script:LinuxVmId = $null
# Track resources created during the run so AfterAll can clean up.
$script:CreatedVmIds = [System.Collections.Generic.List[int]]::new()
@@ -91,10 +94,19 @@ BeforeAll {
return $false
}
function script:Skip-IfNoPassword {
if (Skip-IfNoTarget) { return $true }
if (-not $script:Password -or -not $script:CloudImagePath) {
Set-ItResult -Skipped -Because 'PVETEST_PASSWORD and PVETEST_CLOUD_IMAGE_PATH required for Linux VM provisioning'
return $true
}
return $false
}
function script:Skip-IfNoLinuxVm {
if (Skip-IfNoTarget) { return $true }
if ($null -eq $script:LinuxVmId) {
Set-ItResult -Skipped -Because 'No Linux VM with guest agent available (PVETEST_LINUX_VMID not set)'
Set-ItResult -Skipped -Because 'Linux VM was not provisioned (PVETEST_PASSWORD may not be set)'
return $true
}
return $false
@@ -497,7 +509,7 @@ Describe 'Integration Tests' -Tag 'Integration' {
}
{
Send-PveIso `
Send-PveFile `
-Node $script:Node `
-Storage $script:Storage `
-Path $script:IsoPath `
@@ -622,6 +634,102 @@ Describe 'Integration Tests' -Tag 'Integration' {
}
}
# -----------------------------------------------------------------------
Context 'Linux VM — Provisioning' {
It 'Should upload cloud image to PVE storage (Send-PveFile)' {
if (Skip-IfNoPassword) { return }
$task = Send-PveFile `
-Node $script:Node -Storage $script:Storage `
-Path $script:CloudImagePath `
-ContentType 'import' -Wait
$task | Should -Not -BeNullOrEmpty
$task.IsSuccessful | Should -BeTrue
}
It 'Should create a Linux VM (New-PveVm)' {
if (Skip-IfNoPassword) { return }
$task = New-PveVm `
-Node $script:Node `
-Name 'pester-linux-vm' `
-Memory 512 -Cores 1 -OsType 'l26' -Wait
$task | Should -Not -BeNullOrEmpty
$vm = Get-PveVm -Node $script:Node -Name 'pester-linux-vm' |
Select-Object -First 1
$vm | Should -Not -BeNullOrEmpty
$script:LinuxVmId = $vm.VmId
$script:CreatedVmIds.Add($vm.VmId)
}
It 'Should import cloud image disk (Import-PveVmDisk)' {
if (Skip-IfNoLinuxVm) { return }
$cloudImageFilename = [System.IO.Path]::GetFileName($script:CloudImagePath)
$task = Import-PveVmDisk `
-Node $script:Node -VmId $script:LinuxVmId `
-Disk 'scsi0' -TargetStorage 'local-lvm' `
-Source "$($script:Storage):import/$cloudImageFilename" `
-Wait
$task | Should -Not -BeNullOrEmpty
$task.IsSuccessful | Should -BeTrue
}
It 'Should configure VM hardware (Set-PveVmConfig)' {
if (Skip-IfNoLinuxVm) { return }
{ Set-PveVmConfig -Node $script:Node -VmId $script:LinuxVmId `
-AdditionalConfig @{
scsihw = 'virtio-scsi-single'
boot = 'order=scsi0'
serial0 = 'socket'
agent = '1'
net0 = 'virtio,bridge=vmbr0'
ide2 = 'local-lvm:cloudinit'
cicustom = "user=$($script:Storage):snippets/test-vm-userdata.yml"
} -ErrorAction Stop } | Should -Not -Throw
}
It 'Should set cloud-init config (Set-PveCloudInitConfig)' {
if (Skip-IfNoLinuxVm) { return }
{ Set-PveCloudInitConfig -Node $script:Node -VmId $script:LinuxVmId `
-CiUser 'root' `
-Password (ConvertTo-SecureString $script:Password -AsPlainText -Force) `
-IpConfig0 'ip=dhcp' `
-ErrorAction Stop } | Should -Not -Throw
}
It 'Should start the Linux VM (Start-PveVm)' {
if (Skip-IfNoLinuxVm) { return }
$task = Start-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait
$task | Should -Not -BeNullOrEmpty
}
It 'Should wait for guest agent (Test-PveVmGuestAgent)' {
if (Skip-IfNoLinuxVm) { return }
$timeout = 300; $elapsed = 0
$agentReady = $false
while ($elapsed -lt $timeout) {
if (Test-PveVmGuestAgent -Node $script:Node -VmId $script:LinuxVmId) {
$agentReady = $true
break
}
Start-Sleep -Seconds 10
$elapsed += 10
}
$agentReady | Should -BeTrue -Because "Guest agent should respond within ${timeout}s"
}
}
# -----------------------------------------------------------------------
Context 'Guest Agent — Cmdlets' {
It 'Should ping guest agent (Test-PveVmGuestAgent)' {
+22 -12
View File
@@ -55,17 +55,20 @@ pveum user token add pester@pve pester-ci
## Environment Variables
Set these before running the integration suite. All six are required; any missing variable
causes every integration test to be skipped with a clear reason message.
Set these before running the integration suite. The first six are required; any missing
required variable causes every integration test to be skipped with a clear reason message.
| Variable | Description | Example value |
|---------------------|-----------------------------------------------------------------------|-----------------------------------------------------|
| `PVETEST_HOST` | Hostname or IP address of the test PVE node | `192.168.1.10` or `pve-test.internal` |
| `PVETEST_PORT` | PVE API port | `8006` |
| `PVETEST_APITOKEN` | API token in `USER@REALM!TOKENID=UUID` format | `pester@pve!pester-ci=xxxxxxxx-xxxx-xxxx-xxxx-xxxx` |
| `PVETEST_NODE` | Node name as it appears in `pvesh get /nodes` | `pve-test1` |
| `PVETEST_STORAGE` | Storage pool to use for disk and ISO operations | `local` |
| `PVETEST_ISO_PATH` | Local path to a small `.iso` file used for upload tests | `/tmp/tinycorelinux.iso` |
| Variable | Required | Description | Example value |
|--------------------------|----------|-----------------------------------------------------------------------|-----------------------------------------------------|
| `PVETEST_HOST` | Yes | Hostname or IP address of the test PVE node | `192.168.1.10` or `pve-test.internal` |
| `PVETEST_PORT` | Yes | PVE API port | `8006` |
| `PVETEST_APITOKEN` | Yes | API token in `USER@REALM!TOKENID=UUID` format | `pester@pve!pester-ci=xxxxxxxx-xxxx-xxxx-xxxx-xxxx` |
| `PVETEST_NODE` | Yes | Node name as it appears in `pvesh get /nodes` | `pve-test1` |
| `PVETEST_STORAGE` | Yes | Storage pool to use for disk and ISO operations | `local` |
| `PVETEST_ISO_PATH` | Yes | Local path to a small `.iso` file used for upload tests | `/tmp/tinycorelinux.iso` |
| `PVETEST_PASSWORD` | No | Root password for cloud-init Linux VM provisioning | `Testpass123!` |
| `PVETEST_CLOUD_IMAGE_URL`| No | URL for cloud image download (defaults to Ubuntu Noble) | `https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img` |
| `PVETEST_PVE_VERSION` | No | Expected PVE major version (8 or 9) | `9` |
### Setting variables (Bash / zsh)
@@ -76,6 +79,8 @@ export PVETEST_APITOKEN="pester@pve!pester-ci=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx
export PVETEST_NODE="pve-test1"
export PVETEST_STORAGE="local"
export PVETEST_ISO_PATH="/tmp/tinycorelinux.iso"
# Optional — required for Linux VM provisioning tests
export PVETEST_PASSWORD="Testpass123!"
```
### Setting variables (PowerShell)
@@ -87,6 +92,8 @@ $env:PVETEST_APITOKEN = 'pester@pve!pester-ci=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx
$env:PVETEST_NODE = 'pve-test1'
$env:PVETEST_STORAGE = 'local'
$env:PVETEST_ISO_PATH = '/tmp/tinycorelinux.iso'
# Optional — required for Linux VM provisioning tests
$env:PVETEST_PASSWORD = 'Testpass123!'
```
### GitHub Actions / CI
@@ -102,6 +109,7 @@ env:
PVETEST_NODE: ${{ secrets.PVETEST_NODE }}
PVETEST_STORAGE: ${{ secrets.PVETEST_STORAGE }}
PVETEST_ISO_PATH: ${{ secrets.PVETEST_ISO_PATH }}
PVETEST_PASSWORD: ${{ secrets.PVETEST_PASSWORD }}
```
---
@@ -136,11 +144,13 @@ Invoke-Pester -Path ./tests/PSProxmoxVE.Tests -ExcludeTag Integration -Output De
The integration tests:
- **Create VMs** (named `pester-test-vm`, `pester-clone-vm`) on `PVETEST_NODE`
- **Create VMs** (named `pester-test-vm`, `pester-clone-vm`, `pester-linux-vm`) on `PVETEST_NODE`
- **Delete** those VMs after the test completes (via `AfterAll` cleanup)
- **Start and stop** an existing stopped VM if one is available
- **Provision a Linux VM** with cloud-init, guest agent, and disk import (when `PVETEST_PASSWORD` is set)
- **Start and stop** VMs, including graceful ACPI shutdown via guest agent
- **Create and delete a snapshot** on an existing stopped VM
- **Upload an ISO** to `PVETEST_STORAGE`
- **Download a cloud image** to PVE storage (when `PVETEST_PASSWORD` is set)
The `AfterAll` block performs best-effort cleanup. If the test run is interrupted, leftover
VMs named `pester-*` may remain on the test node and should be removed manually.
@@ -1,7 +1,7 @@
#Requires -Module Pester
<#
.SYNOPSIS
Pester 5 tests for Send-PveIso.
Pester 5 tests for Send-PveFile.
All tests are fully offline no live Proxmox VE target is required.
If the cmdlet is not yet compiled the tests are marked Skipped.
#>
@@ -9,30 +9,30 @@
BeforeAll {
. $PSScriptRoot/../_TestHelper.ps1
$script:CmdExists = $null -ne (Get-Command 'Send-PveIso' -ErrorAction SilentlyContinue)
$script:CmdExists = $null -ne (Get-Command 'Send-PveFile' -ErrorAction SilentlyContinue)
}
Describe 'Send-PveIso' {
Describe 'Send-PveFile' {
Context 'Manifest declaration' {
It 'Should be declared in CmdletsToExport' {
$manifestPath = Join-Path (Get-Module PSProxmoxVE).ModuleBase 'PSProxmoxVE.psd1'
if (-not (Test-Path $manifestPath)) { Set-ItResult -Skipped -Because 'Manifest not found'; return }
$manifest = Import-PowerShellDataFile $manifestPath
$manifest.CmdletsToExport | Should -Contain 'Send-PveIso'
$manifest.CmdletsToExport | Should -Contain 'Send-PveFile'
}
}
Context 'Command existence' {
It 'Should be available after module import' {
if (-not $script:CmdExists) { Set-ItResult -Skipped -Because 'Not yet compiled'; return }
(Get-Command 'Send-PveIso').CommandType | Should -Be 'Cmdlet'
(Get-Command 'Send-PveFile').CommandType | Should -Be 'Cmdlet'
}
}
Context 'Required parameters' {
BeforeAll {
$script:Cmd = Get-Command 'Send-PveIso' -ErrorAction SilentlyContinue
$script:Cmd = Get-Command 'Send-PveFile' -ErrorAction SilentlyContinue
}
It 'Should have Node parameter (Mandatory)' {
@@ -60,7 +60,7 @@ Describe 'Send-PveIso' {
Context 'ChecksumAlgorithm ValidateSet' {
BeforeAll {
$script:Cmd = Get-Command 'Send-PveIso' -ErrorAction SilentlyContinue
$script:Cmd = Get-Command 'Send-PveFile' -ErrorAction SilentlyContinue
}
It 'Should have ChecksumAlgorithm parameter' {
@@ -108,9 +108,30 @@ Describe 'Send-PveIso' {
}
}
Context 'ContentType parameter' {
BeforeAll {
$script:Cmd = Get-Command 'Send-PveFile' -ErrorAction SilentlyContinue
}
It 'Should have ContentType parameter' {
if (-not $script:CmdExists) { Set-ItResult -Skipped -Because 'Not yet compiled'; return }
$script:Cmd.Parameters.ContainsKey('ContentType') | Should -BeTrue
}
It 'ContentType should have a ValidateSet attribute with iso, vztmpl, import' {
if (-not $script:CmdExists) { Set-ItResult -Skipped -Because 'Not yet compiled'; return }
$validateSetAttr = $script:Cmd.Parameters['ContentType'].Attributes |
Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } |
Select-Object -First 1
$validateSetAttr.ValidValues | Should -Contain 'iso'
$validateSetAttr.ValidValues | Should -Contain 'vztmpl'
$validateSetAttr.ValidValues | Should -Contain 'import'
}
}
Context 'ShouldProcess support' {
BeforeAll {
$script:Cmd = Get-Command 'Send-PveIso' -ErrorAction SilentlyContinue
$script:Cmd = Get-Command 'Send-PveFile' -ErrorAction SilentlyContinue
}
It 'Should support WhatIf' {
@@ -121,7 +142,7 @@ Describe 'Send-PveIso' {
Context 'Optional parameters' {
BeforeAll {
$script:Cmd = Get-Command 'Send-PveIso' -ErrorAction SilentlyContinue
$script:Cmd = Get-Command 'Send-PveFile' -ErrorAction SilentlyContinue
}
It 'Should have Checksum parameter' {
@@ -140,7 +161,7 @@ Describe 'Send-PveIso' {
if (-not $script:CmdExists) { Set-ItResult -Skipped -Because 'Not yet compiled'; return }
$tmpIso = [System.IO.Path]::GetTempFileName()
try {
{ Send-PveIso -Node 'pve-node1' -Storage 'local' -Path $tmpIso -Confirm:$false -ErrorAction Stop } |
{ Send-PveFile -Node 'pve-node1' -Storage 'local' -Path $tmpIso -Confirm:$false -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
} finally { Remove-Item $tmpIso -ErrorAction SilentlyContinue }
}
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Prepares the test environment on the nested PVE node.
# Only performs operations that have no PVE API equivalent, plus
# downloads test artifacts for the integration tests to upload.
#
# Usage: prepare-test-environment.sh <nested-pve-ip> <root-password> <output-dir>
#
# Operations:
# - Enable snippets+import content types on local storage (pvesm set)
# - Upload cloud-init user-data snippet (SCP — no snippet upload API)
# - Download Ubuntu cloud image to <output-dir> for upload tests
set -euo pipefail
NESTED_IP="${1:?Usage: prepare-test-environment.sh <ip> <password> <output-dir>}"
ROOT_PASS="$2"
OUTPUT_DIR="${3:?Output directory required}"
CLOUD_IMAGE_URL="https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
CLOUD_IMAGE_FILENAME="noble-server-cloudimg-amd64.img"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
SSH_CMD="sshpass -p ${ROOT_PASS} ssh ${SSH_OPTS} root@${NESTED_IP}"
SCP_CMD="sshpass -p ${ROOT_PASS} scp ${SSH_OPTS}"
echo "=== Preparing test environment on ${NESTED_IP} ==="
# Enable snippets and import content types on local storage
echo "Configuring local storage content types..."
${SSH_CMD} "mkdir -p /var/lib/vz/snippets && pvesm set local --content iso,vztmpl,snippets,import"
# Upload cloud-init user-data snippet (no API for snippet upload)
echo "Uploading cloud-init user-data snippet..."
USERDATA=$(mktemp)
cat > "${USERDATA}" <<'YAML'
#cloud-config
package_update: true
packages:
- qemu-guest-agent
runcmd:
- systemctl enable --now qemu-guest-agent
YAML
${SCP_CMD} "${USERDATA}" "root@${NESTED_IP}:/var/lib/vz/snippets/test-vm-userdata.yml"
rm -f "${USERDATA}"
# Download cloud image for integration tests to upload via Send-PveFile
CLOUD_IMAGE_PATH="${OUTPUT_DIR}/${CLOUD_IMAGE_FILENAME}"
if [ ! -f "${CLOUD_IMAGE_PATH}" ]; then
echo "Downloading Ubuntu cloud image..."
curl -fSL -o "${CLOUD_IMAGE_PATH}" "${CLOUD_IMAGE_URL}"
else
echo "Cloud image already cached at ${CLOUD_IMAGE_PATH}"
fi
echo "CLOUD_IMAGE_PATH=${CLOUD_IMAGE_PATH}"
echo "Environment preparation complete."
@@ -1,133 +0,0 @@
#!/usr/bin/env bash
# Prepares an Ubuntu cloud image VM with qemu-guest-agent on the nested PVE.
#
# Uses PSProxmoxVE cmdlets for all supported operations:
# - Invoke-PveStorageDownload (cloud image download)
# - New-PveVm (VM creation)
# - Import-PveVmDisk (disk import from storage)
# - Set-PveVmConfig -AdditionalConfig (boot/agent/cloud-init config)
# - Set-PveCloudInitConfig (user/password/IP)
# - Start-PveVm (boot)
# - Test-PveVmGuestAgent (agent ping)
#
# SSH/SCP only for operations without API support:
# - pvesm set (enable snippets content type)
# - SCP snippet upload (no snippet upload API — PVE limitation, not even the web UI supports this)
#
# Usage: prepare-test-vm.sh <nested-pve-ip> <root-password> <vm-id> <node> <api-token>
#
# Outputs (to stdout, for capture by caller):
# LINUX_VMID=<vm-id>
set -euo pipefail
NESTED_IP="${1:?Usage: prepare-test-vm.sh <ip> <password> <vm-id> <node> <api-token>}"
ROOT_PASS="$2"
VMID="$3"
NODE="$4"
API_TOKEN="$5"
CLOUD_IMAGE_URL="https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
CLOUD_IMAGE_FILENAME="noble-server-cloudimg-amd64.img"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
SSH_CMD="sshpass -p ${ROOT_PASS} ssh ${SSH_OPTS} root@${NESTED_IP}"
SCP_CMD="sshpass -p ${ROOT_PASS} scp ${SSH_OPTS}"
CONNECT_CMD="Connect-PveServer -Server '${NESTED_IP}' -ApiToken '${API_TOKEN}' -SkipCertificateCheck"
echo "=== Preparing test Linux VM (VMID ${VMID}) on ${NESTED_IP} ==="
# ── Step 1: Upload cloud-init snippet (SSH — no API for snippets) ────
echo "Uploading cloud-init user-data snippet..."
USERDATA=$(mktemp)
cat > "${USERDATA}" <<'YAML'
#cloud-config
package_update: true
packages:
- qemu-guest-agent
runcmd:
- systemctl enable --now qemu-guest-agent
YAML
${SSH_CMD} "mkdir -p /var/lib/vz/snippets && pvesm set local --content iso,vztmpl,snippets,import"
${SCP_CMD} "${USERDATA}" "root@${NESTED_IP}:/var/lib/vz/snippets/test-vm-userdata.yml"
rm -f "${USERDATA}"
# ── Step 2: Download cloud image (module cmdlet) ─────────────────────
echo "Downloading Ubuntu cloud image via Invoke-PveStorageDownload..."
pwsh -NoProfile -Command "
Import-Module PSProxmoxVE; ${CONNECT_CMD}
Invoke-PveStorageDownload \
-Node '${NODE}' -Storage 'local' \
-Url '${CLOUD_IMAGE_URL}' -Filename '${CLOUD_IMAGE_FILENAME}' \
-ContentType 'import' -Wait
"
# ── Step 3: Create VM (module cmdlet) ────────────────────────────────
echo "Creating VM ${VMID} via New-PveVm..."
pwsh -NoProfile -Command "
Import-Module PSProxmoxVE; ${CONNECT_CMD}
New-PveVm -Node '${NODE}' -VmId ${VMID} -Name 'ubuntu-test' \
-Memory 512 -Cores 1 -OsType 'l26' -Wait
"
# ── Step 4: Import disk (module cmdlet) ────────────────────────────
echo "Importing disk image via Import-PveVmDisk..."
pwsh -NoProfile -Command "
Import-Module PSProxmoxVE; ${CONNECT_CMD}
Import-PveVmDisk -Node '${NODE}' -VmId ${VMID} -Disk 'scsi0' \
-TargetStorage 'local-lvm' \
-Source 'local:import/${CLOUD_IMAGE_FILENAME}' -Wait
"
# ── Step 5: Configure VM (module cmdlet — AdditionalConfig) ──────────
echo "Configuring VM via Set-PveVmConfig -AdditionalConfig..."
pwsh -NoProfile -Command "
Import-Module PSProxmoxVE; ${CONNECT_CMD}
Set-PveVmConfig -Node '${NODE}' -VmId ${VMID} -AdditionalConfig @{
scsihw = 'virtio-scsi-single'
boot = 'order=scsi0'
serial0 = 'socket'
agent = '1'
net0 = 'virtio,bridge=vmbr0'
ide2 = 'local-lvm:cloudinit'
cicustom = 'user=local:snippets/test-vm-userdata.yml'
}
"
# ── Step 6: Set cloud-init config (module cmdlet) ────────────────────
echo "Setting cloud-init config via Set-PveCloudInitConfig..."
pwsh -NoProfile -Command "
Import-Module PSProxmoxVE; ${CONNECT_CMD}
Set-PveCloudInitConfig -Node '${NODE}' -VmId ${VMID} \
-CiUser 'root' \
-Password (ConvertTo-SecureString '${ROOT_PASS}' -AsPlainText -Force) \
-IpConfig0 'ip=dhcp'
"
# ── Step 7: Start VM (module cmdlet) ─────────────────────────────────
echo "Starting VM ${VMID} via Start-PveVm..."
pwsh -NoProfile -Command "
Import-Module PSProxmoxVE; ${CONNECT_CMD}
Start-PveVm -Node '${NODE}' -VmId ${VMID} -Wait
"
# ── Step 8: Wait for guest agent (module cmdlet) ─────────────────────
echo "Waiting for guest agent on VM ${VMID} (cloud-init installing packages)..."
pwsh -NoProfile -Command "
Import-Module PSProxmoxVE; ${CONNECT_CMD}
\$timeout = 300; \$elapsed = 0
while (\$elapsed -lt \$timeout) {
if (Test-PveVmGuestAgent -Node '${NODE}' -VmId ${VMID}) {
Write-Host 'Guest agent responding on VM ${VMID}'
exit 0
}
Start-Sleep -Seconds 10
\$elapsed += 10
Write-Host \" Waiting... (\${elapsed}s / \${timeout}s)\"
}
throw 'Timeout waiting for guest agent on VM ${VMID}'
"
echo "LINUX_VMID=${VMID}"