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
+8 -9
View File
@@ -275,19 +275,17 @@ jobs:
# ── Prepare test Linux VM ────────────────────────────────────────
- name: Deploy test Linux VM to nested PVE
- name: Prepare test environment on nested PVE
if: inputs.skip_provision != true
id: linux_vm
id: test_env
shell: bash
run: |
OUTPUT=$(bash ${SCRIPTS_DIR}/prepare-test-vm.sh \
OUTPUT=$(bash ${SCRIPTS_DIR}/prepare-test-environment.sh \
"${{ steps.provision.outputs.host }}" \
"${PVE_PASSWORD}" \
200 \
"${{ steps.target.outputs.node }}" \
"${{ steps.provision.outputs.token }}")
LINUX_VMID=$(echo "$OUTPUT" | grep "^LINUX_VMID=" | cut -d= -f2)
echo "linux_vmid=${LINUX_VMID}" >> "$GITHUB_OUTPUT"
"${RUNNER_TEMP}")
CLOUD_IMAGE_PATH=$(echo "$OUTPUT" | grep "^CLOUD_IMAGE_PATH=" | cut -d= -f2)
echo "cloud_image_path=${CLOUD_IMAGE_PATH}" >> "$GITHUB_OUTPUT"
# ── Run tests ─────────────────────────────────────────────────────
@@ -304,7 +302,8 @@ jobs:
PVETEST_NODE: ${{ steps.target.outputs.node }}
PVETEST_STORAGE: ${{ steps.target.outputs.storage }}
PVETEST_PVE_VERSION: ${{ matrix.pve_version }}
PVETEST_LINUX_VMID: ${{ steps.linux_vm.outputs.linux_vmid }}
PVETEST_PASSWORD: ${{ env.PVE_PASSWORD }}
PVETEST_CLOUD_IMAGE_PATH: ${{ steps.test_env.outputs.cloud_image_path }}
run: |
$env:PVETEST_ISO_PATH = Join-Path $env:RUNNER_TEMP "pvetest.iso"
Import-Module Pester -MinimumVersion 5.0
+7 -4
View File
@@ -82,14 +82,17 @@ Get-PveVm -VmId 100 | Copy-PveVm -NewVmId 200 -NewName 'my-clone' -Full -Wait
Get-PveVm -VmId 100 | Get-PveVmConfig
```
### Upload an ISO
### Upload Files
```powershell
# Upload a local ISO file to Proxmox storage
Send-PveIso -Node 'pve1' -Storage 'local' -Path './ubuntu-24.04-live-server-amd64.iso' -Wait
Send-PveFile -Node 'pve1' -Storage 'local' -Path './ubuntu-24.04-live-server-amd64.iso' -Wait
# Upload a disk image for VM import
Send-PveFile -Node 'pve1' -Storage 'local' -Path './disk.qcow2' -ContentType 'import' -Wait
```
> **Note:** `Send-PveIso` implements a workaround for a long-standing Proxmox API multipart parsing bug
> **Note:** `Send-PveFile` implements a workaround for a long-standing Proxmox API multipart parsing bug
> ([bugzilla 7389](https://bugzilla.proxmox.com/show_bug.cgi?id=7389)). Standard multipart HTTP libraries
> (including .NET's `MultipartFormDataContent`) add sub-headers that Proxmox's `pveproxy` mishandles,
> resulting in corrupt uploads. This cmdlet constructs the multipart body manually to ensure correct uploads
@@ -253,7 +256,7 @@ SDN management requires Proxmox VE 8.0 or later. Connected server is version 7.4
|---|---|
| `Get-PveStorage` | List storage pools |
| `Get-PveStorageContent` | List storage content (ISOs, images, etc.) |
| `Send-PveIso` | Upload a local ISO file |
| `Send-PveFile` | Upload a file (ISO, disk image, template) to storage |
| `Invoke-PveStorageDownload` | Download a URL to storage (server-side) |
| `New-PveStorage` | Create a storage pool |
| `Remove-PveStorage` | Remove a storage pool |
@@ -8,32 +8,42 @@ using PSProxmoxVE.Core.Models.Vms;
namespace PSProxmoxVE.Cmdlets.Storage
{
/// <summary>
/// <para type="synopsis">Uploads a local ISO file to a Proxmox VE storage.</para>
/// <para type="synopsis">Uploads a local file to a Proxmox VE storage.</para>
/// <para type="description">
/// Uploads an ISO image from the local filesystem to the specified node/storage using
/// the Proxmox VE upload API. Streams the file in 4 MB chunks and reports progress
/// Uploads a file from the local filesystem to the specified node/storage using
/// the Proxmox VE upload API. Supports ISO images, container templates, and
/// disk images for import. Streams the file in chunks and reports progress
/// via Write-Progress. Returns a PveTask representing the upload job.
/// </para>
/// </summary>
[Cmdlet(VerbsCommunications.Send, "PveIso", SupportsShouldProcess = true)]
[Cmdlet(VerbsCommunications.Send, "PveFile", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public class SendPveIsoCmdlet : PveCmdletBase
public class SendPveFileCmdlet : PveCmdletBase
{
/// <summary>The Proxmox VE node to upload to.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>The target storage identifier (must support "iso" content).</summary>
/// <summary>The target storage identifier.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The storage pool name.")]
public string Storage { get; set; } = string.Empty;
/// <summary>
/// The full local path to the ISO file to upload. The file must exist.
/// The full local path to the file to upload. The file must exist.
/// </summary>
[Parameter(Mandatory = true, Position = 2, HelpMessage = "Local path to the ISO file to upload.")]
[Parameter(Mandatory = true, Position = 2, HelpMessage = "Local path to the file to upload.")]
[FileExistsValidation]
public string Path { get; set; } = string.Empty;
/// <summary>
/// The PVE storage content type. Determines where the file is stored:
/// "iso" for ISO images, "vztmpl" for container templates,
/// "import" for disk images and OVA files to be imported into VMs.
/// </summary>
[Parameter(Mandatory = false, Position = 3, HelpMessage = "Content type: iso, vztmpl, or import.")]
[ValidateSet("iso", "vztmpl", "import", IgnoreCase = true)]
public string ContentType { get; set; } = "iso";
/// <summary>Optional checksum value to verify the uploaded file.</summary>
[Parameter(Mandatory = false, HelpMessage = "Checksum value to verify the upload.")]
public string? Checksum { get; set; }
@@ -52,19 +62,19 @@ namespace PSProxmoxVE.Cmdlets.Storage
protected override void ProcessRecord()
{
var fileName = System.IO.Path.GetFileName(Path);
if (!ShouldProcess($"{Node}/{Storage}/{fileName}", "Upload ISO"))
if (!ShouldProcess($"{Node}/{Storage}/{fileName}", $"Upload file (content={ContentType})"))
return;
var session = GetSession();
using var client = new PveHttpClient(session);
WriteVerbose($"Uploading ISO to {Node}/{Storage}...");
WriteVerbose($"Uploading {fileName} to {Node}/{Storage} (content={ContentType})...");
var resource = $"nodes/{Node}/storage/{Storage}/upload";
var totalBytes = new System.IO.FileInfo(Path).Length;
var activityId = 1;
var progressRecord = new ProgressRecord(activityId,
$"Uploading ISO to {Node}/{Storage}",
$"Uploading to {Node}/{Storage}",
$"Uploading {fileName}...");
// Track progress via an atomic counter updated from the upload thread.
@@ -78,7 +88,7 @@ namespace PSProxmoxVE.Cmdlets.Storage
Path,
formFields: new System.Collections.Generic.Dictionary<string, string>
{
["content"] = "iso"
["content"] = ContentType
},
checksum: Checksum,
checksumAlgorithm: ChecksumAlgorithm,
+1 -1
View File
@@ -105,7 +105,7 @@
# Storage
'Get-PveStorage',
'Get-PveStorageContent',
'Send-PveIso',
'Send-PveFile',
'Invoke-PveStorageDownload',
'New-PveStorage',
'Remove-PveStorage',
@@ -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}"