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}"