diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index 6bfc0b4..976a341 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -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
diff --git a/README.md b/README.md
index bf3c267..06618ed 100644
--- a/README.md
+++ b/README.md
@@ -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 |
diff --git a/src/PSProxmoxVE/Cmdlets/Storage/SendPveIsoCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/SendPveFileCmdlet.cs
similarity index 80%
rename from src/PSProxmoxVE/Cmdlets/Storage/SendPveIsoCmdlet.cs
rename to src/PSProxmoxVE/Cmdlets/Storage/SendPveFileCmdlet.cs
index 3281e5e..29c9a86 100644
--- a/src/PSProxmoxVE/Cmdlets/Storage/SendPveIsoCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Storage/SendPveFileCmdlet.cs
@@ -8,32 +8,42 @@ using PSProxmoxVE.Core.Models.Vms;
namespace PSProxmoxVE.Cmdlets.Storage
{
///
- /// Uploads a local ISO file to a Proxmox VE storage.
+ /// Uploads a local file to a Proxmox VE storage.
///
- /// 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.
///
///
- [Cmdlet(VerbsCommunications.Send, "PveIso", SupportsShouldProcess = true)]
+ [Cmdlet(VerbsCommunications.Send, "PveFile", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
- public class SendPveIsoCmdlet : PveCmdletBase
+ public class SendPveFileCmdlet : PveCmdletBase
{
/// The Proxmox VE node to upload to.
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
- /// The target storage identifier (must support "iso" content).
+ /// The target storage identifier.
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The storage pool name.")]
public string Storage { get; set; } = string.Empty;
///
- /// 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.
///
- [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;
+ ///
+ /// 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.
+ ///
+ [Parameter(Mandatory = false, Position = 3, HelpMessage = "Content type: iso, vztmpl, or import.")]
+ [ValidateSet("iso", "vztmpl", "import", IgnoreCase = true)]
+ public string ContentType { get; set; } = "iso";
+
/// Optional checksum value to verify the uploaded file.
[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
{
- ["content"] = "iso"
+ ["content"] = ContentType
},
checksum: Checksum,
checksumAlgorithm: ChecksumAlgorithm,
diff --git a/src/PSProxmoxVE/PSProxmoxVE.psd1 b/src/PSProxmoxVE/PSProxmoxVE.psd1
index 0da34e8..3d43515 100644
--- a/src/PSProxmoxVE/PSProxmoxVE.psd1
+++ b/src/PSProxmoxVE/PSProxmoxVE.psd1
@@ -105,7 +105,7 @@
# Storage
'Get-PveStorage',
'Get-PveStorageContent',
- 'Send-PveIso',
+ 'Send-PveFile',
'Invoke-PveStorageDownload',
'New-PveStorage',
'Remove-PveStorage',
diff --git a/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 b/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1
index a8fd631..70456b1 100644
--- a/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1
+++ b/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1
@@ -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)' {
diff --git a/tests/PSProxmoxVE.Tests/Integration/README.md b/tests/PSProxmoxVE.Tests/Integration/README.md
index 9c63c72..d743150 100644
--- a/tests/PSProxmoxVE.Tests/Integration/README.md
+++ b/tests/PSProxmoxVE.Tests/Integration/README.md
@@ -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.
diff --git a/tests/PSProxmoxVE.Tests/Storage/Send-PveIso.Tests.ps1 b/tests/PSProxmoxVE.Tests/Storage/Send-PveFile.Tests.ps1
similarity index 77%
rename from tests/PSProxmoxVE.Tests/Storage/Send-PveIso.Tests.ps1
rename to tests/PSProxmoxVE.Tests/Storage/Send-PveFile.Tests.ps1
index 67a1623..fbe7984 100644
--- a/tests/PSProxmoxVE.Tests/Storage/Send-PveIso.Tests.ps1
+++ b/tests/PSProxmoxVE.Tests/Storage/Send-PveFile.Tests.ps1
@@ -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 }
}
diff --git a/tests/infrastructure/scripts/prepare-test-environment.sh b/tests/infrastructure/scripts/prepare-test-environment.sh
new file mode 100755
index 0000000..7c5d5cc
--- /dev/null
+++ b/tests/infrastructure/scripts/prepare-test-environment.sh
@@ -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
+#
+# 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 for upload tests
+
+set -euo pipefail
+
+NESTED_IP="${1:?Usage: prepare-test-environment.sh }"
+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."
diff --git a/tests/infrastructure/scripts/prepare-test-vm.sh b/tests/infrastructure/scripts/prepare-test-vm.sh
deleted file mode 100755
index 638fd68..0000000
--- a/tests/infrastructure/scripts/prepare-test-vm.sh
+++ /dev/null
@@ -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
-#
-# Outputs (to stdout, for capture by caller):
-# LINUX_VMID=
-
-set -euo pipefail
-
-NESTED_IP="${1:?Usage: prepare-test-vm.sh }"
-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}"