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>
Add scan-4 full review: 1 resolved (F040), 6 new findings (F071-F076).
27 open findings, 49 resolved. No regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
Replaced hardcoded PVE host IP and API token in Capture-UploadDiff.ps1
with placeholder values that must be filled in before running.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sealed 105 cmdlet classes that were not already sealed — cmdlets are leaf
classes and sealing enables compiler optimizations.
Added [OutputType(typeof(void))] to 53 cmdlets that were missing the
attribute. All 169 cmdlet files now declare OutputType.
Added ConfirmImpact.High to Suspend-PveVm and Restart-PveVm to match
the convention used by Stop-PveVm and Reset-PveVm.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The codebase uses Newtonsoft.Json (JObject.Parse + ToObject<T>) exclusively.
Removed all [JsonPropertyName] attributes and [System.Text.Json.Serialization.
JsonIgnore] attributes from 41 model files, along with the now-unused
using System.Text.Json.Serialization imports. [JsonProperty] (Newtonsoft)
attributes remain as the sole serialization annotations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changed VmId from int to int? across all 18 firewall cmdlets. The parameter
is conditionally required (only when Level is Vm or Container) so using a
non-nullable int with 0 as sentinel was fragile. Now checks !VmId.HasValue
instead of VmId == 0, and passes the nullable directly to service methods.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VmService, ContainerService, StorageService, and SnapshotService now wrap
user-controlled string parameters (node names, storage names) with
Uri.EscapeDataString() in URL path construction, preventing path traversal
or encoding issues. Integer parameters (vmid) are left unwrapped.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaced 5 bare catch blocks with specific exception types:
- PveHttpClient: catch JsonException for JSON parse fallback
- PveCmdletBase: catch Exception with filter excluding OOM/SOE for polling
- VmService/ContainerService: catch PveApiException|HttpRequestException
for offline node handling
- GetPveVmCmdlet: catch PveApiException for VM enrichment failures
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Set-PveVmGuestPassword now accepts SecureString instead of plain string for
the -Password parameter, matching the established pattern in Set-PvePassword.
The password is converted via Marshal and zeroed from memory after use.
New-PveStorage now validates that -Pool and -CephPool cannot both be specified,
since both mapped to the same API key and CephPool silently overwrote Pool.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Four cmdlets (Invoke-PveNetworkApply, New/Restore/Remove-PveSnapshot) had
private WaitForTask methods with bare while(true) loops and no timeout or
failure detection. Replaced with TaskService.WaitForTask which has a 10-minute
default timeout, exit status validation, and throws PveTaskTimeoutException
or PveTaskFailedException on failure.
Invoke-PveVmGuestExec had a similar unbounded do/while polling loop. Added a
-Timeout parameter (default 300s, range 1-3600) with Stopwatch enforcement.
Also fixed bare catch in DecodeBase64 to catch FormatException specifically.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Warn (not block) when users pass named parameters that were added in
later PVE versions:
- Send-PveFile: -Checksum/-ChecksumAlgorithm require PVE 7.1
- New/Set-PveSdnSubnet: -DhcpRange requires PVE 8.1
These are warnings, not hard blocks — the parameter is still sent to
the API, which will either accept it or return its own error. This
only applies to named cmdlet parameters, not AdditionalConfig
pass-through hashtables.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
- 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>
- 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>
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>
- 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>
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>
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>
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>
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>
PVE handles OVA disk extraction + import as part of qmcreate when
import-from is specified in the create parameters. The previous
approach (create VM, then POST config with import-from) caused lock
timeouts because the config update conflicted with the VM lock.
Now Import-PveOva passes disk import-from and network config directly
in the POST /nodes/{node}/qemu create call, matching how the PVE UI
handles OVA imports.
Verified locally against PVE 9.1 with Ubuntu 24.04 cloud OVA (571 MB).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
- 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>
- 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>
- 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>
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>
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>
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>
System.Formats.Tar is not available as a standalone NuGet backport for
netstandard2.0/net48. Use SharpCompress 0.38.0 on all targets for a
single, well-tested code path that handles GNU long filenames, PAX
extended headers, and other TAR variants correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
Full audit covering repository structure, API coverage, code quality,
testing, security, PSGallery readiness, and community standards.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- generate-help.ps1 script to build markdown stubs and MAML XML
- 81 cmdlet markdown help files in docs/cmdlets/
- PSProxmoxVE.dll-Help.xml (898 KB) for Get-Help support
- PVE_API_COVERAGE.md documenting implemented and missing API areas
- .csproj updated to include help XML in build output
- HelpInfoUri added to module manifest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
Replace `new Random()` with `RandomNumberGenerator` in
PveHttpClient.GenerateBoundary(). Uses the static Fill() method on
.NET Core and the disposable Create() pattern on net48/netstandard2.0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
- Add missing XML doc comments to all public types and members in
PSProxmoxVE.Core (Authentication, Exceptions, Models, Client)
- Remove CS1591 suppression from PSProxmoxVE.Core.csproj
- Build succeeds with 0 warnings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
- Add HelpMessage to every [Parameter] attribute
- Add [ValidateRange(100, 999999999)] to all VmId parameters
- Add ConfirmImpact.High to Stop-PveVm and Reset-PveVm
- Add ShouldProcess to Invoke-PveVmGuestExec
- Add WriteVerbose before every API call
- Add WriteWarning on -SkipCertificateCheck usage in Connect-PveServer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
The script uses PSProxmoxVE cmdlets but the module was only copied to
the module path after it ran. Move the install step before the test VM
deployment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>