Commit Graph

76 Commits

Author SHA1 Message Date
Clint Branham df4cae01bc feat: close API coverage gaps — 51 new cmdlets across 6 areas
Phase 1 — Missing Update/Set operations (10 cmdlets):
- SDN: Set-PveSdnZone/Vnet/Subnet/Controller/Ipam/Dns + Invoke-PveSdnApply
- Set-PveRole, Set-PveStorage, Set-PveApiToken

Phase 2 — Cluster operations (8 cmdlets):
- Get-PveClusterResource (cluster-wide inventory)
- Get-PveTaskList, Stop-PveTask
- Get/New/Set/Remove-PvePool
- Get-PveBackupInfo (unprotected VMs)

Phase 3 — VM & container gaps (14 cmdlets):
- Move-PveVmDisk, Remove-PveVmDisk
- Guest agent: Get-PveVmGuestOsInfo/FsInfo, Read/Write-PveVmGuestFile,
  Set-PveVmGuestPassword, Invoke-PveVmGuestFsTrim
- Suspend/Resume-PveContainer, Resize-PveContainerDisk,
  New-PveContainerTemplate, Move-PveContainerVolume,
  Get-PveContainerInterface

Phase 4 — Storage content management (4 cmdlets):
- Get-PveStorageStatus, Remove/Set-PveStorageContent, New-PveStorageDisk

Phase 5 — Node operations (6 cmdlets):
- Get/Set-PveNodeConfig, Get/Set-PveNodeDns, Start/Stop-PveNodeVms

Phase 6 — Access management (9 cmdlets):
- Get/New/Set/Remove-PveGroup, Get/New/Set/Remove-PveDomain, Set-PvePassword

Total: 169 cmdlets (was 118). Includes models, services, format views,
Pester unit tests, manifest updates, README, CHANGELOG, API coverage docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 09:55:19 -05:00
Clint Branham ca4585f134 fix(test): add missing Url and Key params to New-PveSdnDns no-session test
New-PveSdnDns requires mandatory Url and Key parameters. The no-session
test was failing with "missing mandatory parameters" instead of reaching
the session check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 09:31:40 -05:00
Clint Branham 4b503b08db fix: backup job creation requires vmid or -All, fix Enabled default
- Integration test: add -All to New-PveBackupJob call — PVE API
  requires either vmid or all when creating a backup job
- Fix New-PveBackupJob Enabled logic: jobs are enabled by default
  unless user explicitly passes -Enabled:$false (was broken because
  SwitchParameter default=true doesn't set IsPresent)
- Fix XML doc cref warning in NetworkService after RequireSdn removal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 09:07:39 -05:00
Clint Branham b11f41a5cc fix(test): fix 16 Pester unit test failures in firewall cmdlets
- Add -Level Cluster to "no session" tests for cmdlets that require
  the mandatory Level parameter (13 failures)
- Fix parameter name Pos → Position in Set/Remove-PveFirewallRule
  tests (2 failures)
- Change Set-PveFirewallAlias Cidr test from mandatory to optional
  assertion — Cidr is optional on update (1 failure)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 08:48:36 -05:00
Clint Branham 27ecca6251 feat: add two-tier version gating (introduced vs default) for cmdlets
Replace the service-layer RequireSdn hard block with a cmdlet-level
RequireVersion helper in PveCmdletBase that supports two tiers:

- Introduced version: hard fail — the API endpoint doesn't exist
- Default version: warning only — feature exists but may not be
  enabled; users who manually enabled it can proceed

Applied to:
- SDN Zone/VNet/Subnet: introduced 6.2, default 8.0
- SDN IPAM/DNS/Controller: introduced 6.2, default 8.1
- Import-PveVmDisk, Import-PveOva: introduced 8.1

Also fixes integration tests:
- Remove duplicate OVA Import context
- Use systemd calendar format for backup schedule (PVE 9)
- Add SDN IPAM/DNS/Controller read-only integration tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 08:40:23 -05:00
Clint Branham c6fd0d36fc fix(test): remove duplicate OVA context, use systemd calendar format for backup schedule
- Remove duplicate 'OVA Import' context that used wrong parameter name
  (NewVmId vs VmId) — original OVA test context at line 1227 is correct
- Fix backup job schedule from cron syntax ('0 3 * * 6') to systemd
  calendar event format ('sat 03:00') which PVE 9 requires

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 08:03:38 -05:00
Clint Branham a72642d203 feat: add firewall, backup, and SDN IPAM/DNS/controller cmdlets
Add 35 new cmdlets bringing the total to 118:
- Firewall (21): rules, groups, aliases, IP sets, options at
  cluster/node/VM/container levels
- Backup (5): ad-hoc vzdump and scheduled backup job CRUD
- SDN IPAM/DNS/Controller (9): plugin management for SDN subsystem

Also includes:
- Fix: Remove-PveRole now has ConfirmImpact.High
- Fix: URL-encode snapshot names in API paths
- Refactor: extract auth header strings to constants in PveHttpClient
- Add PSGallery version badge to README
- Full test coverage: 11 JSON fixtures, xUnit model tests, Pester
  unit tests, and integration tests for firewall/backup/OVA import
- Updated manifest, format file, CHANGELOG, README, API coverage docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 07:51:49 -05:00
Clint Branham 8d5f33f592 feat: add -Detailed switch to Get-PveVm for accurate pause detection
PVE list endpoint reports Status=running for paused VMs. The new
-Detailed switch queries status/current per VM to populate QmpStatus.

- PveVm.EffectiveStatus computed property: returns QmpStatus ?? Status
- PveVm.Status doc comment explains the limitation
- VmService.EnrichVmStatus() queries status/current endpoint
- Get-PveVm -Detailed enriches each VM with qmpstatus, pid, uptime
- format.ps1xml table view uses EffectiveStatus for the Status column
- Integration test asserts EffectiveStatus after suspend/resume

Verified locally: Get-PveVm -Detailed correctly shows 'paused' for
suspended VMs where Get-PveVm (without -Detailed) shows 'running'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 16:18:38 -05:00
Clint Branham 5e37737bc4 fix(test): remove Get-PveVm status assertions after suspend/resume
PVE list endpoint (GET /nodes/{node}/qemu) always reports Status=running
for paused VMs — qmpstatus is only available from the status/current
endpoint. The -Wait -Timeout polling uses status/current internally, so
if it returns without throwing, the state transition is confirmed.

Also fix restart recovery: use Resume-PveVm -Wait instead of checking
Status from list endpoint (which never shows 'paused').

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 16:08:20 -05:00
Clint Branham 0fc15d74ee fix: OVA import — scsi bus, boot order, NIC model from OVF, metadata verification
Locally verified against PVE 9.1 with Ubuntu 24.04 cloud OVA:

- Use scsi bus (not sata) for imported disks, matching PVE UI behavior
- Set boot=order=scsi0 for first disk
- Parse NIC ResourceSubType from OVF (VmxNet3, E1000, E1000e, etc.)
  and map to PVE network model instead of hardcoding virtio
- Add MapNicModel() to OvfMetadata with fallback to e1000 for unknown types
- Integration test verifies name, cores, memory, ostype, scsi0, net0, boot

qmpstatus fix (from previous local testing):
- WaitForStatusTransition polls status/current endpoint for qmpstatus
- PVE reports status=running but qmpstatus=paused for suspended VMs
- SDN subnet ID uses zone prefix: {zone}-{ip}-{prefix}

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 15:56:03 -05:00
Clint Branham a06cd3f2e5 fix: use qmpstatus for suspend state detection, fix SDN subnet ID format
Suspend/Resume:
- Poll status/current endpoint (not VM list) to get qmpstatus
- PVE reports status=running but qmpstatus=paused for suspended VMs
- Verified locally: Suspend-PveVm -Wait -Timeout 30 now works

SDN:
- Subnet IDs use zone prefix, not vnet: {zone}-{ip}-{prefix}
- Add 5s propagation delay before VNet deletion after subnet removal
- Switch to real Ubuntu 24.04 OVA for Import-PveOva testing

Integration tests:
- All VM/container lifecycle calls use -Wait -Timeout
- Remove manual Start-Sleep polling loops

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 15:37:38 -05:00
Clint Branham 2b772ebc65 feat: add -Timeout parameter with state polling to lifecycle cmdlets
All VM and container lifecycle cmdlets (Start, Stop, Restart, Suspend,
Resume, Reset) now poll VM/container status when -Wait is specified,
blocking until the expected state is reached or -Timeout (default 60s)
is exceeded.

Implementation:
- PveCmdletBase.WaitForStatusTransition() — shared helper that waits
  for PVE task completion then polls status via API
- 9 cmdlets updated: 6 VM (Start, Stop, Restart, Suspend, Resume,
  Reset) + 3 container (Start, Stop, Restart)
- -Timeout parameter with [ValidateRange(1, 3600)] on each

Integration tests:
- Replace all manual Start-Sleep + polling loops with -Wait -Timeout
- Switch to real Ubuntu cloud OVA (571 MB) for Import-PveOva testing
- OVA test verifies full import + VM start

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 15:15:30 -05:00
Clint Branham d1eb674c92 fix(test): fix suspend polling, SDN propagation delays, OVA resilience
- Suspend/Resume: replace fixed sleep with polling loop (up to 20s)
- Restart: ensure VM is running (not paused) before attempting restart
- SDN: add propagation delays between subnet/vnet/zone removal
- OVA: split into OVF parsing test (always works) and import test
  (tolerates synthetic VMDK failures gracefully)
- Dockerfile: add qemu-utils for proper VMDK generation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 15:05:40 -05:00
Clint Branham c5396707eb fix(test): fix suspend, SDN, container, and OVA integration test failures
- Suspend/Resume: move to Linux VM lifecycle context (empty VMs may
  not reliably transition to paused state)
- SDN subnet model: dhcp-range is an array, not string
- SDN subnet delete: use PVE-format ID (vnet-ip-prefix) not CIDR
- Container config: trim trailing newline from PVE description
- OVA: use qemu-img to create valid sparse VMDK (add qemu-utils to
  test container Dockerfile)
- Enable 'images' content type on local storage for OVA import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 14:49:06 -05:00
Clint Branham 531deafdb7 fix(test): fix integration test failures for SDN, containers, OVA, suspend
- SDN: zone ID cannot contain hyphens — rename 'pester-zone' to 'pesterz'
- SDN: subnet removal uses original CIDR, not transformed ID
- Containers: New-PveContainer now auto-assigns VM ID via /cluster/nextid
  when -VmId is omitted (was missing, unlike New-PveVm)
- OVA: enable 'images' content type on local storage for import-from
- Suspend: add delay before status check, use Should -Not -Throw pattern

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 14:35:53 -05:00
Clint Branham 1c4c112565 test: expand integration and unit test coverage to 84 test cases
Integration tests (27 contexts, 84 test cases, up from 48 cmdlets to 76):
- VM: Suspend/Resume/Resize
- Containers: full lifecycle (create, config, start, stop, restart, clone)
- Container Snapshots: create, list, restore, remove
- SDN: zone/vnet/subnet CRUD (PVE 8+ version-gated)
- Storage: create/remove directory storage, Invoke-PveStorageDownload
- OVA Import: Import-PveOva with generated test OVA
- Cloud-Init: Invoke-PveCloudInitRegenerate
- Templates: Remove-PveTemplate
- Disconnect-PveServer: verify disconnect + post-disconnect failure

Unit tests:
- Guest agent cmdlets (Test-PveVmGuestAgent, Get-PveVmGuestNetwork,
  Invoke-PveVmGuestExec) — parameter metadata, ShouldProcess, session

Test infrastructure:
- prepare-test-environment.sh generates minimal test OVA (OVF + 1MB VMDK)
- CI workflow passes PVETEST_OVA_PATH to integration tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 14:20:52 -05:00
Clint Branham 6a2b1df804 fix(test): rename cloud image to .qcow2 for PVE upload compatibility
PVE upload API validates file extensions per content type. The
content=import type does not accept .img extension. The Ubuntu cloud
image is qcow2 format, so rename from .img to .qcow2 during download.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 13:52:11 -05:00
Clint Branham f2280c1c88 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>
2026-03-20 13:39:06 -05:00
Clint Branham 16be330834 fix(test): download cloud image as content=import for disk import
Absolute filesystem paths are rejected by PVE API when using API token
auth (only root ticket auth allows arbitrary paths). Instead:
- Enable 'import' content type on local storage alongside snippets
- Download cloud image with content=import (not iso)
- Reference as local:import/filename in Import-PveVmDisk

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 13:17:14 -05:00
Clint Branham b19911ef06 fix(test): use absolute path for disk import in prepare-test-vm.sh
The cloud image is downloaded as content=iso, but import-from requires
content type 'images' or 'import'. Use the absolute node path
(/var/lib/vz/template/iso/) instead of the storage volume reference
(local:iso/) to work around this constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 13:08:47 -05:00
Clint Branham ca0cd945a0 feat: add Import-PveOva cmdlet for OVA appliance import
Parses OVF metadata client-side from the OVA TAR archive to extract
VM name, CPU, memory, disks, and network adapters. Then uploads the
OVA with content=import, creates the VM, and imports each disk.

- OvfMetadata parser with multi-target TAR support (System.Formats.Tar
  on net9.0, manual 512-byte header parsing on netstandard2.0/net48)
- VmService.UploadOva() for content=import uploads
- Import-PveOva cmdlet with -Name/-Memory/-Cores overrides and -Wait
- Pester unit tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 13:08:31 -05:00
Clint Branham 088221a5f1 feat: add Import-PveVmDisk cmdlet for disk image import
Imports disk images (qcow2, raw, vmdk) and OVA archives into VMs
via the PVE API using POST config with import-from syntax.

- VmService.ImportDisk() method using POST /nodes/{node}/qemu/{vmid}/config
- Import-PveVmDisk cmdlet with -Source, -Disk, -TargetStorage, -Format, -Wait
- Pester unit tests
- prepare-test-vm.sh updated to use Import-PveVmDisk instead of SSH qm importdisk
- Set-PveNetwork: add required Type parameter (fixes PVE API 400 error)
- Integration test fix: pass -Type bridge to Set-PveNetwork

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:55:36 -05:00
Clint Branham ae55dbf0d7 fix(test): add -Confirm:$false to all ConfirmImpact.High cmdlet calls in tests
ConfirmImpact.High causes interactive confirmation prompts in
non-interactive CI, blocking before ProcessRecord runs. Add
-Confirm:$false to all Stop-PveVm, Reset-PveVm, Remove-*,
Restore-*, and New-PveTemplate calls in both unit and integration tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:40:54 -05:00
Clint Branham de400e159c feat: add Move-PveContainer cmdlet for LXC migration
- New Move-PveContainer cmdlet with -Online and -Wait support
- Uses existing ContainerService.MigrateContainer
- Pester unit tests added to ContainerLifecycle.Tests.ps1
- README cmdlet reference updated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:31:46 -05:00
Clint Branham fac796ee43 test: add network CRUD integration tests
- Add bridge create/update/remove/apply test cases
- Add interface type filter test
- Tests exercise New-PveNetwork, Set-PveNetwork, Remove-PveNetwork,
  and Invoke-PveNetworkApply against live PVE

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:22:03 -05:00
Clint Branham ffec75b461 feat: add container snapshot and SDN subnet cmdlets
Container snapshots (4 cmdlets):
- Get-PveContainerSnapshot, New-PveContainerSnapshot
- Remove-PveContainerSnapshot, Restore-PveContainerSnapshot
- Service methods in ContainerService
- Pester unit tests

SDN subnet management (3 cmdlets):
- Get-PveSdnSubnet, New-PveSdnSubnet, Remove-PveSdnSubnet
- PveSdnSubnet model and NetworkService methods
- Pester unit tests

Also updates manifest (73 cmdlets), README cmdlet reference, and CHANGELOG.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:21:40 -05:00
Clint Branham b77f08533c fix(test): add delay after template conversion for PVE 9 propagation
PVE 9 may not immediately reflect the template flag in the VM list
after New-PveTemplate returns. Add a 3-second delay before verifying.
PVE 8 passed without the delay.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:58:11 -05:00
Clint Branham 518e5c838a feat(test): use new cmdlets in test VM setup and add guest agent tests
prepare-test-vm.sh now uses module cmdlets for 7 of 8 steps:
- Set-PveVmConfig -AdditionalConfig replaces SSH qm set (8 config keys)
- Test-PveVmGuestAgent replaces SSH qm agent ping
- Only SSH remaining: pvesm set, SCP snippet, qm importdisk

New integration tests for guest agent cmdlets:
- Test-PveVmGuestAgent: verify agent responds
- Get-PveVmGuestNetwork: verify interfaces with IPv4 addresses
- Invoke-PveVmGuestExec: run hostname command, verify exit code + output

Also force Node.js 24 for delete-package-versions (no v6 available yet).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 14:08:52 -05:00
Clint Branham b12ffc485d feat: add AdditionalConfig parameter and guest agent cmdlets (closes #2, #3)
Issue #3 — Set-PveVmConfig / Set-PveContainerConfig:
- Add -AdditionalConfig [Hashtable] for arbitrary PVE config keys
  (scsi0, boot, agent, net0, ide2, cicustom, etc.)
- Add -Delete [string] for removing config keys
- Unit tests for both new parameters

Issue #2 — QEMU Guest Agent cmdlets:
- Test-PveVmGuestAgent: pings guest agent, returns bool
- Get-PveVmGuestNetwork: returns guest network interfaces with IPs
- Invoke-PveVmGuestExec: executes command in guest, returns output
- New models: PveGuestNetworkInterface, PveGuestIpAddress
- Service methods in VmService for all agent operations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 14:06:55 -05:00
Clint Branham d995479c8e fix(test): add -Confirm:\$false to New-PveTemplate call in integration tests
ConfirmImpact.High causes an interactive prompt in CI without
-Confirm:\$false, leading to a null reference error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:31:17 -05:00
Clint Branham 6b213265de fix(ci): skip Invoke-PveCloudInitRegenerate (bug: returns config as UPID)
The cmdlet calls the /cloudinit/dump endpoint which returns YAML
content, not a task UPID. This gets passed to WaitForTask causing a
501 error. PVE regenerates cloud-init automatically on VM start, so
the explicit regenerate call is not needed for test VM setup.

Bug to fix: RegenerateCloudInitImage should PUT to /cloudinit instead
of GET from /cloudinit/dump.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:21:47 -05:00
Clint Branham 6f8389800f fix(ci): switch to Ubuntu cloud image (.img extension) for test VM
The Debian .qcow2 extension was rejected by PVE's download-url API.
Ubuntu's noble-server-cloudimg-amd64.img has a .img extension that
PVE accepts, and cloud-init + qemu-guest-agent work the same way.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:14:06 -05:00
Clint Branham d98bd40476 refactor(ci): use PSProxmoxVE cmdlets in test VM preparation
Replace raw qm/SSH commands with module cmdlets where possible:
- Invoke-PveStorageDownload for cloud image download
- New-PveVm for VM creation
- Set-PveCloudInitConfig for cloud-init user/password/IP
- Invoke-PveCloudInitRegenerate to apply config
- Start-PveVm to boot the VM

SSH/SCP retained only for operations without API support:
- Snippet upload (no snippet API)
- pvesm set for snippets content type
- qm importdisk (no importdisk cmdlet)
- qm set for --scsi0, --cicustom, --agent, --boot (not in Set-PveVmConfig)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:52:44 -05:00
Clint Branham f1f5f3f180 refactor(ci): use cloud-init user-data instead of virt-customize
Replace virt-customize with a cloud-init custom user-data snippet that
installs qemu-guest-agent on first boot. The snippet is SCP'd to the
nested PVE's /var/lib/vz/snippets/ and referenced via --cicustom.

This eliminates the need for libguestfs-tools entirely — no install
on nested PVE, no extra Docker image, no large dependencies.

Timeout increased to 300s to allow for cloud-init package installation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:49:52 -05:00
Clint Branham c4a1abf5b1 fix(ci): install libguestfs-tools on nested PVE and mask test password
- prepare-test-vm.sh: install libguestfs-tools if not present on the
  fresh nested PVE before running virt-customize
- Mask PVE_PASSWORD in logs via ::add-mask:: to prevent leaking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:46:21 -05:00
Clint Branham d6aa5274d4 refactor(ci): customize cloud image on nested PVE instead of container
Move virt-customize to run on the nested PVE host (which already has
libguestfs) instead of inside the CI container. This removes
libguestfs-tools, linux-image-generic, and qemu-utils from the
Dockerfile, shrinking the image from ~1.2GB to ~770MB.

- prepare-test-vm.sh: downloads Debian cloud image on nested PVE,
  runs virt-customize there to install qemu-guest-agent, then creates
  VM with imported disk + cloud-init
- Remove build-test-image.sh (no longer needed)
- Remove /opt/pve-images volume mount

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:37:35 -05:00
Clint Branham 3202bbccef fix(ci): add xorriso to container and expand tool verification
proxmox-auto-install-assistant requires xorriso for ISO manipulation.
Expand the verification step to check all tools the scripts depend on.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:09:36 -05:00
Clint Branham ff4853978d feat(ci): containerize integration tests and add Alpine guest-agent VM
Container infrastructure:
- Dockerfile with PowerShell, Pester, Terraform, sshpass,
  libguestfs-tools, and proxmox-auto-install-assistant
- build-test-container.yml workflow to build/push image to GHCR
- Image tagged ghcr.io/<repo>/integration-test:latest

Workflow restructure:
- New build job on GitHub-hosted ubuntu-latest (dotnet publish)
- Integration job runs inside Docker container on self-hosted runner
- Module artifact downloaded from build job (no .NET SDK in container)
- Host volumes mounted for PVE ISOs and cached images

Alpine guest-agent VM:
- build-test-image.sh: Downloads Alpine cloud image, installs
  qemu-guest-agent via virt-customize, caches on runner
- prepare-test-vm.sh: Deploys image to nested PVE via SCP + qm
  importdisk, configures cloud-init, waits for guest agent

Test fixes:
- Get-PveTask: use .Upid property instead of PveTask object ToString()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:26:43 -05:00
Clint Branham a9f1c066e0 feat(test): add Alpine guest-agent VM for ACPI and template tests
Infrastructure:
- build-test-image.sh: Downloads Alpine cloud image, uses virt-customize
  to install qemu-guest-agent, caches at /opt/pve-images/
- prepare-test-vm.sh: SCPs image to nested PVE, creates VM via
  qm importdisk with cloud-init and agent enabled, waits for agent

Workflow:
- Build Alpine image step (cached on runner, only built once)
- Deploy test VM step after provisioning, passes LINUX_VMID to tests

Tests:
- Guest Agent VM lifecycle: verify running, ACPI restart, ACPI stop
- Templates: convert Linux VM to template, clone from template
  (New-PveTemplate, New-PveVmFromTemplate)
- Fix Get-PveTask: use .Upid property instead of whole PveTask object

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:01:05 -05:00
Clint Branham 84029d8871 fix(infra): detect Debian suite from os-release for PVE 8 compatibility
PVE 8 (Bookworm) may not have /etc/apt/sources.list.d/debian.sources,
causing the grep fallback to use 'trixie' which is invalid for PVE 8.
Use /etc/os-release VERSION_CODENAME which works on both PVE 8 and 9.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 10:46:44 -05:00
Clint Branham 8924d521b6 fix(test): correct integration test parameter mismatches
- New-PveRole: Privileges is a comma-separated string, not an array
- Set-PvePermission: uses UgId+Type, not Users
- Get-PveVmConfig: no Name property — test config get/set together
- Restart-PveVm: replace with Reset-PveVm (ACPI timeout on no-OS VM)
- Remove resize disk test (Set-PveVmConfig lacks Settings param)
- Copy-PveVm: must provide NewVmId explicitly
- Get-PveTask: requires Upid — test via Start-PveVm task chain

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 10:46:08 -05:00
Clint Branham 5e1864a0b0 feat(test): add PVE 8+9 matrix and expand integration test coverage
Workflow changes:
- Convert to matrix strategy testing PVE 8.4 and 9.1 sequentially
- Each version gets unique VMID (99908/99909) and VM name
- Pass PVETEST_PVE_VERSION env var for version-specific assertions
- Version-specific artifact names for test results

New integration tests:
- User CRUD: create, list, update (Set-PveUser), remove
- API Token CRUD: create, list, remove (on dedicated test user)
- Role CRUD: create with privileges, list, remove
- Permissions: list and set (Get/Set-PvePermission)
- VM config: get and set config, resize disk
- VM lifecycle: restart, explicit remove
- Snapshots: full lifecycle (create, list, restore, remove)
- Storage: list content (Get-PveStorageContent)
- Tasks: list recent tasks (Get-PveTask)
- Version assertion: verify PVE major version matches expected

Coverage improved from 15 to ~30 cmdlets tested against live API.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 10:33:07 -05:00
Clint Branham cc344a3070 fix(test): replace null-coalescing operator for PS 5.1 compatibility
PS 5.1 cannot parse the ?? operator (PS 7.0+), causing a discovery
error even though integration tests are excluded by tag. Replace with
if/else that works on all PS versions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 10:00:18 -05:00
Clint Branham 6cab18779c fix(test): remove mandatory-param invocation tests that hang PS 5.1
PS 5.1 prompts interactively for mandatory parameters instead of
throwing, causing CI to hang indefinitely. Replace invocation-based
tests with attribute-based IsMandatory checks where duplicates exist,
or convert to attribute checks where they don't.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:52:43 -05:00
Clint Branham b8af36a7fc fix(build): add netstandard2.0 target to eliminate System.Runtime version mismatch
The net9.0 DLL references System.Runtime 9.0.0.0 which is unavailable
on GitHub runners where pwsh ships with .NET 8.x. Adding netstandard2.0
as a target produces a DLL compatible with any .NET version (PS 5.1 on
.NET Framework through PS 7.x on .NET 8/9).

- Add netstandard2.0 to TargetFrameworks in Core and module csproj
- Update #if NET48 conditionals to include NETSTANDARD2_0
- Switch CI workflows from net9.0 to netstandard2.0 for Pester tests
- Update _TestHelper.ps1 framework search order

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:34:40 -05:00
Clint Branham 35805959f4 fix(ci): remove all deps/runtimeconfig json and unset DOTNET_ROOT in-process
Previous approach of env: DOTNET_ROOT='' at the step level wasn't
sufficient — the .NET runtime may have already cached the original
DOTNET_ROOT before pwsh started. Now:

- Remove *.deps.json AND *.runtimeconfig.json from publish output
- Unset DOTNET_ROOT via $env:DOTNET_ROOT = $null inside the pwsh script
- _TestHelper.ps1 also unsets DOTNET_ROOT and cleans deps/runtimeconfig

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 09:06:24 -05:00
Clint Branham 558053dc74 chore(ci): move unit tests and build to Docker containers on self-hosted runner
All CI now runs on the self-hosted runner using Docker containers,
eliminating GitHub-hosted runner minute consumption:

- Unit tests: PS 7.5 container (mcr.microsoft.com/powershell:7.5-ubuntu-24.04)
  with .NET SDK 9.0 installed at build time
- Build/xUnit: .NET SDK 9.0 container (mcr.microsoft.com/dotnet/sdk:9.0)
- Integration tests: unchanged (runs directly on self-hosted runner)

Drops net48/PS 5.1 CI matrix (Windows-only, can't run in Linux
containers). Local Docker testing available via tests/Dockerfile.test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 08:56:12 -05:00
Clint Branham 7962a549f0 fix(test): load module via .psd1 manifest instead of raw DLL
Loading via the module manifest (.psd1) ensures PowerShell sets up
proper assembly resolution context. Loading via raw DLL path bypasses
PS's module loading infrastructure and fails on CI runners where the
.NET SDK's assembly resolution conflicts with PS's bundled runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 08:45:23 -05:00
Clint Branham 8d268aee61 fix(ci): remove PSProxmoxVE.deps.json to fix assembly loading in PS 7.x
The deps.json file causes .NET's AssemblyLoadContext to conflict with
PowerShell's own runtime assembly resolution, resulting in
FileNotFoundException for System.Runtime 9.0.0.0 on all PS 7.x
platforms. Binary PS modules should not include deps.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 08:36:49 -05:00
Clint Branham d187b0de52 fix: resolve net48-only nullable warnings (CS8601, CS8602, CS8604)
- CS8601: Add null-forgiving operator on guarded dictionary assignments
  in cmdlets where IsNullOrEmpty check precedes the assignment
- CS8602: Add Assert.NotNull after JObject["data"] in xUnit model tests
  (JToken indexer returns nullable on net48)
- CS8604: Add null-forgiving on guarded arguments in NodeService,
  WaitPveTaskCmdlet, GetPveTemplateCmdlet, and auth test parameters

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 08:32:57 -05:00