mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-08-12 15:16:52 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
+22
-12
@@ -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,
|
||||
@@ -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)' {
|
||||
|
||||
@@ -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.
|
||||
|
||||
+31
-10
@@ -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 }
|
||||
}
|
||||
@@ -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}"
|
||||
Reference in New Issue
Block a user