Commit Graph

80 Commits

Author SHA1 Message Date
Clint Branham 89bead7a0e fix: add -Confirm:$false to Restart-PveContainer integration test (F080, F081)
Restart-PveContainer gained ConfirmImpact.High in the F062 fix, which
correctly prompts for confirmation on destructive operations. The
integration test was missing -Confirm:$false, causing a
NullReferenceException in non-interactive CI. The cascading failure also
broke the Copy-PveContainer test (F081).

Also updates CONTRIBUTING.md to reference .NET SDK 10.0+ (was 9.0+) to
match all CI workflows and test project TFM (F083).

Includes scan-6 review report and findings database update (F001-F083).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:48:11 -05:00
Clint Branham 68dadbfdc0 fix: remediate findings F045, F047, F048, F064, F070, F071, F076-F079
Phase 1 — Trivial fixes:
- F071: Add Uri.EscapeDataString to GetPveTemplateCmdlet node path
- F077: Add ValidateRange(100, 999999999) to GetPveTaskListCmdlet.VmId
- F076: Create .github/dependabot.yml (nuget + github-actions, weekly)
- F079: Fix unit-tests.yml dotnet SDK from 9.0.x to 10.0.x
- F048: Mark wont_fix — sync-over-async accepted for PS 5.1 compat

Phase 2 — Framework targeting (D009 compliance):
- F047: Reduce publishable csproj to netstandard2.0 only, remove all
  #if NET48/NETSTANDARD2_0 conditionals from PveHttpClient.cs,
  restructure build.yml for netstandard2.0 publish + net10.0/net48 tests
- F064: Resolved by F047 — SMA 7.5.0 ItemGroup removed with net10.0 TFM
- F070: Add PS 5.1 smoke-test job to publish.yml (windows-latest)

Phase 3 — IPveHttpClient interface extraction (F045):
- Extract IPveHttpClient interface from PveHttpClient
- Add constructor injection to all 14 service classes
- Services use injected client when available, create+dispose when not

Phase 4 — Service unit tests (F078):
- 196 new xUnit tests across 10 service test files
- All services tested via Moq-mocked IPveHttpClient
- Total test count: 382 (was 186)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:51:37 -05:00
Clint Branham 94424367bf fix: remediate scan-4 findings F058, F062, F063, F071, F072, F073, F074, F075
F058 (critical): Replace while(true) infinite-loop task polling with
TaskService.WaitForTask in 5 container snapshot and storage cmdlets.

F073+F047 (high): Migrate net9.0 → net10.0 across both source .csproj
files, build.yml, publish.yml, and test helper.

F071 (medium): Add Uri.EscapeDataString() to all inline URL path
segments in ~16 cmdlets that bypass service classes (D003).

F062+F063 (medium): Add ConfirmImpact.High to Restart-PveContainer
and Suspend-PveContainer (D006).

F075 (medium): Generate markdown help docs for 89 cmdlets that were
missing documentation (170 total, up from 81).

F072 (low): Remove unused System.Text.Json dependency from Core.csproj.
F074 (low): Raise publish smoke-test threshold from 60 to 150.
F065 (low): Add .github/ISSUE_TEMPLATE/config.yml.
F066 (low): Add CODEOWNERS.

Also fix _TestHelper.ps1 net9.0 → net10.0 framework reference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:54:28 -05:00
Clint Branham d99c3e5490 chore(test): update tests for parameter changes, target net10.0
- xUnit test project now targets net10.0 instead of net9.0 (EOL Nov 2026)
- Updated GuestAgentExtCmdlets tests for SecureString Password parameter
- Added Timeout parameter tests to GuestAgentCmdlets
- Added ConfirmImpact.High assertions for Suspend/Restart-PveVm
- Added -Confirm:$false to Suspend-PveVm integration test call

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 08:16:45 -05:00
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