diff --git a/REVIEW_REPORT.md b/REVIEW_REPORT.md
deleted file mode 100644
index c577f61..0000000
--- a/REVIEW_REPORT.md
+++ /dev/null
@@ -1,878 +0,0 @@
-# PSProxmoxVE Module Review Report
-
-```
-Scan date: 2026-03-22
-Prior report date: 2026-03-21
-PVE API spec date: 2026-03-21T15:04:50.641Z
-PVE API spec SHA256: 4af79be30166209a4714b771f65e1e9540c5b738f414ff30c98454402e29d030
-PVE version hint: (not set in spec)
-Total API endpoints: 646
-```
-
-**Reviewer:** Claude Code (Opus 4.6)
-**Module Version:** 0.1.0-preview
-**Repository:** PSProxmoxVE (C# binary PowerShell module)
-
----
-
-## Executive Summary
-
-**Delta since last scan: 16 fixed | 5 new findings | 0 regressed | 9 still open**
-
-**API drift: 194 endpoints with version_changes | 466 PVE endpoints unimplemented (72.1% uncovered) | 42 🆕 PVE 9.0 endpoints**
-
-1. **169 cmdlets** (up from 148) — all fully exported, sealed, with OutputType attributes. New: 21 additional cmdlets since prior scan.
-2. **14 prior findings resolved**: All 4 infinite-loop WaitForTask methods (CQ1–CQ4) now use `TaskService.WaitForTask`. Guest exec timeout added (CQ5). OutputType on all cmdlets (CQ9). Firewall VmId nullable (CQ10). Bare catches replaced with filtered exceptions (CQ11–CQ14). RemovePveRole/SuspendPveVm/RestartPveVm ConfirmImpact fixed (CQ17/CQ19/CQ20). Cmdlets sealed (CQ18). Dual JSON attrs removed (CQ21). Magic strings extracted to constants (CQ22). Guest password now SecureString (S1). Debug script secrets redacted (S2).
-3. **Remaining Critical**: 5 cmdlets still have `while(true)` task-polling loops with no timeout — 3 container snapshot cmdlets plus `InvokePveStorageDownloadCmdlet` and `SendPveFileCmdlet`.
-4. **100% Pester unit test coverage** maintained across all 169 cmdlets. Integration test coverage is ~62% (105 of 169).
-5. **27.9% API endpoint coverage** (180 of 646 endpoints). Major uncovered areas unchanged: Ceph (40), HA (21), cluster config (10), disks (18), ACME (15), certificates (8).
-6. **Security posture solid**: All prior security findings resolved (S1–S3). URL encoding now consistent across all services. HTTPS enforced, SecureString used consistently.
-7. **PSGallery-ready**: No publication blockers. IconUri still missing (cosmetic). Manifest CmdletsToExport fully enumerates all 169 cmdlets.
-8. **net9.0 target is EOL** (May 2025) — should upgrade to net10.0.
-9. **Pool/CephPool parameter conflict** in New-PveStorage resolved: runtime validation now throws if both specified.
-10. **42 new PVE 9.0 endpoints** available (HA rules, SDN fabrics, bulk actions, OCI registry). None yet implemented.
-
----
-
-## Phase 1 — Repository Inventory & Structure
-
-### Directory Tree (depth ≤ 4)
-
-```
-PSProxmoxVE/
-├── PSProxmoxVE.sln
-├── README.md
-├── LICENSE (MIT)
-├── CHANGELOG.md
-├── CONTRIBUTING.md
-├── CODE_OF_CONDUCT.md
-├── SECURITY.md
-├── .editorconfig
-├── .gitignore
-├── .gitattributes
-├── REVIEW_REPORT.md
-├── .github/
-│ ├── ISSUE_TEMPLATE/
-│ │ ├── bug_report.yml
-│ │ └── feature_request.yml
-│ ├── pull_request_template.md
-│ └── workflows/
-│ ├── build.yml
-│ ├── unit-tests.yml
-│ ├── integration-tests.yml
-│ └── publish.yml
-├── docs/
-│ ├── PVE_API_COVERAGE.md
-│ └── cmdlets/ (75 cmdlet documentation files)
-├── src/
-│ ├── PSProxmoxVE/
-│ │ ├── PSProxmoxVE.csproj
-│ │ ├── PSProxmoxVE.psd1
-│ │ ├── PSProxmoxVE.format.ps1xml
-│ │ ├── PSProxmoxVE.dll-Help.xml
-│ │ ├── ModuleState.cs
-│ │ └── Cmdlets/
-│ │ ├── PveCmdletBase.cs
-│ │ ├── Backup/ (6 cmdlets)
-│ │ ├── CloudInit/ (3 cmdlets)
-│ │ ├── Cluster/ (1 cmdlet)
-│ │ ├── Connection/ (3 cmdlets)
-│ │ ├── Containers/ (20 cmdlets)
-│ │ ├── Firewall/ (21 cmdlets)
-│ │ ├── Network/ (5 + 22 SDN cmdlets)
-│ │ ├── Nodes/ (8 cmdlets)
-│ │ ├── Pools/ (4 cmdlets)
-│ │ ├── Snapshots/ (4 cmdlets)
-│ │ ├── Storage/ (11 cmdlets)
-│ │ ├── Tasks/ (4 cmdlets)
-│ │ ├── Templates/ (4 cmdlets)
-│ │ ├── Users/ (23 cmdlets)
-│ │ └── Vms/ (30 cmdlets)
-│ └── PSProxmoxVE.Core/
-│ ├── PSProxmoxVE.Core.csproj
-│ ├── Authentication/ (PveAuthenticator, PveAuthMode, PveSession, PveVersion)
-│ ├── Client/ (PveHttpClient)
-│ ├── Exceptions/ (7 exception types)
-│ ├── Models/ (25+ model classes across 9 areas)
-│ └── Services/ (14 service classes)
-├── tests/
-│ ├── PSProxmoxVE.Core.Tests/ (xUnit)
-│ │ ├── Authentication/ (3 test files)
-│ │ ├── Fixtures/ (28 JSON fixtures)
-│ │ ├── Models/ (12 test files)
-│ │ └── TestHelper.cs
-│ ├── PSProxmoxVE.Tests/ (Pester 5)
-│ │ ├── _TestHelper.ps1
-│ │ ├── Backup/ (2 test files)
-│ │ ├── CloudInit/ (1 test file)
-│ │ ├── Cluster/ (1 test file)
-│ │ ├── Connection/ (3 test files)
-│ │ ├── Containers/ (5 test files)
-│ │ ├── Firewall/ (5 test files)
-│ │ ├── Network/ (5 test files)
-│ │ ├── Nodes/ (2 test files)
-│ │ ├── Pools/ (1 test file)
-│ │ ├── Snapshots/ (1 test file)
-│ │ ├── Storage/ (4 test files)
-│ │ ├── Tasks/ (2 test files)
-│ │ ├── Templates/ (1 test file)
-│ │ ├── Users/ (4 test files)
-│ │ ├── Vms/ (12 test files)
-│ │ └── Integration/ (1 comprehensive test file)
-│ └── infrastructure/ (Terraform, Dockerfile, scripts for CI)
-├── publish/ (netstandard2.0 build output)
-├── debug/ (debug/analysis scripts)
-└── tools/
- └── Invoke-Tests.ps1
-```
-
-### Project Layout
-
-| Item | Value |
-|---|---|
-| Solution | `PSProxmoxVE.sln` |
-| Main project | `src/PSProxmoxVE/PSProxmoxVE.csproj` |
-| Core library | `src/PSProxmoxVE.Core/PSProxmoxVE.Core.csproj` |
-| Test project (xUnit) | `tests/PSProxmoxVE.Core.Tests/PSProxmoxVE.Core.Tests.csproj` |
-| Test project (Pester) | `tests/PSProxmoxVE.Tests/` |
-| Target Frameworks | `netstandard2.0`, `net9.0`, `net48` |
-| Language Version | C# 10.0 |
-| Nullable | Enabled |
-| Assembly Names | `PSProxmoxVE`, `PSProxmoxVE.Core` |
-
-### CI/CD Workflows
-
-| Workflow | Purpose |
-|---|---|
-| `build.yml` | Build + xUnit tests on net48 (Windows) and net9.0 (Windows + Ubuntu) |
-| `unit-tests.yml` | Build + Pester tests across PS 5.1/7.5 on Windows/Ubuntu/macOS |
-| `integration-tests.yml` | Full integration tests against nested PVE 8 + PVE 9 instances |
-| `publish.yml` | Tag-triggered publish to PSGallery + GitHub Release creation |
-
-### Inventory Checklist
-
-| Item | Present | Prior Status |
-|---|---|---|
-| Solution file (`.sln`) | Yes | Open |
-| Module manifest (`.psd1`) | Yes | Open |
-| Format definitions (`.format.ps1xml`) | Yes | Open |
-| MAML help (`.dll-Help.xml`) | Yes | Open |
-| `.editorconfig` | Yes | Open |
-| `.gitignore` | Yes | Open |
-| `.gitattributes` | Yes | Open |
-| `LICENSE` (MIT) | Yes | Open |
-| `CHANGELOG.md` | Yes | Open |
-| `README.md` | Yes | Open |
-| `CONTRIBUTING.md` | Yes | Open |
-| `CODE_OF_CONDUCT.md` | Yes | Open |
-| `SECURITY.md` | Yes | Open |
-| `.github/ISSUE_TEMPLATE/` | Yes | Open |
-| `.github/pull_request_template.md` | Yes | Open |
-| PSGallery publish workflow | Yes | Open |
-| `docs/PVE_API_COVERAGE.md` | Yes | Open |
-| `docs/cmdlets/` (75 files) | Yes | Open |
-
-### What's Missing
-
-- [ ] `IconUri` in manifest PSData (cosmetic)
-- [ ] `.github/ISSUE_TEMPLATE/config.yml` (controls blank issue creation)
-- [ ] `CODEOWNERS` file (low priority for single-maintainer)
-
----
-
-## Phase 2 — PVE API Coverage Audit
-
-> **Source of truth**: `~/Source/pve_api/pve-api.json` (646 endpoints). Spec generated 2026-03-21.
-> **Latest tracked PVE version**: 9.0 (42 new endpoints, 56 changed endpoints)
-
-### Implemented Cmdlets (169 total)
-
-#### Connection (3)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Connect-PveServer` | `/access/ticket` | POST | Open |
-| `Disconnect-PveServer` | (session teardown) | — | Open |
-| `Test-PveConnection` | `/version` | GET | Open |
-
-#### Nodes (8)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Get-PveNode` | `/nodes` | GET | Open |
-| `Get-PveNodeStatus` | `/nodes/{node}/status` | GET | Open |
-| `Get-PveNodeConfig` | `/nodes/{node}/config` | GET | Open |
-| `Set-PveNodeConfig` | `/nodes/{node}/config` | PUT | Open |
-| `Get-PveNodeDns` | `/nodes/{node}/dns` | GET | Open |
-| `Set-PveNodeDns` | `/nodes/{node}/dns` | PUT | Open |
-| `Start-PveNodeVms` | `/nodes/{node}/startall` | POST | Open |
-| `Stop-PveNodeVms` | `/nodes/{node}/stopall` | POST | Open |
-
-#### VMs / QEMU (19)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Get-PveVm` | `/nodes/{node}/qemu` | GET | Open |
-| `New-PveVm` | `/nodes/{node}/qemu` | POST | Open |
-| `Remove-PveVm` | `/nodes/{node}/qemu/{vmid}` | DELETE | Open |
-| `Start-PveVm` | `/nodes/{node}/qemu/{vmid}/status/start` | POST | Open |
-| `Stop-PveVm` | `/nodes/{node}/qemu/{vmid}/status/stop` | POST | Open |
-| `Restart-PveVm` | `/nodes/{node}/qemu/{vmid}/status/reboot` | POST | Open |
-| `Suspend-PveVm` | `/nodes/{node}/qemu/{vmid}/status/suspend` | POST | Open |
-| `Resume-PveVm` | `/nodes/{node}/qemu/{vmid}/status/resume` | POST | Open |
-| `Reset-PveVm` | `/nodes/{node}/qemu/{vmid}/status/reset` | POST | Open |
-| `Copy-PveVm` | `/nodes/{node}/qemu/{vmid}/clone` | POST | Open |
-| `Move-PveVm` | `/nodes/{node}/qemu/{vmid}/migrate` | POST | Open |
-| `Get-PveVmConfig` | `/nodes/{node}/qemu/{vmid}/config` | GET | Open |
-| `Set-PveVmConfig` | `/nodes/{node}/qemu/{vmid}/config` | PUT | Open |
-| `Resize-PveVmDisk` | `/nodes/{node}/qemu/{vmid}/resize` | PUT | Open |
-| `Import-PveVmDisk` | `/nodes/{node}/storage/{storage}/upload` | POST | Open |
-| `Import-PveOva` | Multiple (upload + config + import) | POST | Open |
-| `Move-PveVmDisk` | `/nodes/{node}/qemu/{vmid}/move_disk` | POST | Open |
-| `Remove-PveVmDisk` | `/nodes/{node}/qemu/{vmid}/unlink` | PUT | Open |
-| `New-PveVmFromTemplate` | `/nodes/{node}/qemu/{vmid}/clone` | POST | Open |
-
-#### Guest Agent (9)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Test-PveVmGuestAgent` | `/nodes/{node}/qemu/{vmid}/agent/ping` | POST | Open |
-| `Get-PveVmGuestNetwork` | `/nodes/{node}/qemu/{vmid}/agent/network-get-interfaces` | GET | Open |
-| `Invoke-PveVmGuestExec` | `/nodes/{node}/qemu/{vmid}/agent/exec` + `.../exec-status` | POST/GET | Open |
-| `Get-PveVmGuestOsInfo` | `/nodes/{node}/qemu/{vmid}/agent/get-osinfo` | GET | Open |
-| `Get-PveVmGuestFsInfo` | `/nodes/{node}/qemu/{vmid}/agent/get-fsinfo` | GET | Open |
-| `Read-PveVmGuestFile` | `/nodes/{node}/qemu/{vmid}/agent/file-read` | GET | Open |
-| `Write-PveVmGuestFile` | `/nodes/{node}/qemu/{vmid}/agent/file-write` | POST | Open |
-| `Set-PveVmGuestPassword` | `/nodes/{node}/qemu/{vmid}/agent/set-user-password` | POST | Open |
-| `Invoke-PveVmGuestFsTrim` | `/nodes/{node}/qemu/{vmid}/agent/fstrim` | POST | Open |
-
-#### Containers / LXC (20)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Get-PveContainer` | `/nodes/{node}/lxc` | GET | Open |
-| `New-PveContainer` | `/nodes/{node}/lxc` | POST | Open |
-| `Remove-PveContainer` | `/nodes/{node}/lxc/{vmid}` | DELETE | Open |
-| `Start-PveContainer` | `/nodes/{node}/lxc/{vmid}/status/start` | POST | Open |
-| `Stop-PveContainer` | `/nodes/{node}/lxc/{vmid}/status/stop` | POST | Open |
-| `Restart-PveContainer` | `/nodes/{node}/lxc/{vmid}/status/shutdown` | POST | Open |
-| `Copy-PveContainer` | `/nodes/{node}/lxc/{vmid}/clone` | POST | Open |
-| `Move-PveContainer` | `/nodes/{node}/lxc/{vmid}/migrate` | POST | Open |
-| `Get-PveContainerConfig` | `/nodes/{node}/lxc/{vmid}/config` | GET | Open |
-| `Set-PveContainerConfig` | `/nodes/{node}/lxc/{vmid}/config` | PUT | Open |
-| `Suspend-PveContainer` | `/nodes/{node}/lxc/{vmid}/status/suspend` | POST | Open |
-| `Resume-PveContainer` | `/nodes/{node}/lxc/{vmid}/status/resume` | POST | Open |
-| `Resize-PveContainerDisk` | `/nodes/{node}/lxc/{vmid}/resize` | PUT | Open |
-| `New-PveContainerTemplate` | `/nodes/{node}/lxc/{vmid}/template` | POST | Open |
-| `Move-PveContainerVolume` | `/nodes/{node}/lxc/{vmid}/move_volume` | POST | Open |
-| `Get-PveContainerInterface` | `/nodes/{node}/lxc/{vmid}/interfaces` | GET | Open |
-| `Get-PveContainerSnapshot` | `/nodes/{node}/lxc/{vmid}/snapshot` | GET | Open |
-| `New-PveContainerSnapshot` | `/nodes/{node}/lxc/{vmid}/snapshot` | POST | Open |
-| `Remove-PveContainerSnapshot` | `/nodes/{node}/lxc/{vmid}/snapshot/{name}` | DELETE | Open |
-| `Restore-PveContainerSnapshot` | `/nodes/{node}/lxc/{vmid}/snapshot/{name}/rollback` | POST | Open |
-
-#### Storage (11)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Get-PveStorage` | `/storage` or `/nodes/{node}/storage` | GET | Open |
-| `Get-PveStorageContent` | `/nodes/{node}/storage/{storage}/content` | GET | Open |
-| `Send-PveFile` | `/nodes/{node}/storage/{storage}/upload` | POST | Open |
-| `Invoke-PveStorageDownload` | `/nodes/{node}/storage/{storage}/download-url` | POST | Open |
-| `New-PveStorage` | `/storage` | POST | Open |
-| `Remove-PveStorage` | `/storage/{storage}` | DELETE | Open |
-| `Set-PveStorage` | `/storage/{storage}` | PUT | Open |
-| `Get-PveStorageStatus` | `/nodes/{node}/storage/{storage}/status` | GET | Open |
-| `Remove-PveStorageContent` | `/nodes/{node}/storage/{storage}/content/{volume}` | DELETE | Open |
-| `Set-PveStorageContent` | `/nodes/{node}/storage/{storage}/content/{volume}` | PUT | Open |
-| `New-PveStorageDisk` | `/nodes/{node}/storage/{storage}/content` | POST | Open |
-
-#### VM Snapshots (4)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Get-PveSnapshot` | `/nodes/{node}/qemu/{vmid}/snapshot` | GET | Open |
-| `New-PveSnapshot` | `/nodes/{node}/qemu/{vmid}/snapshot` | POST | Open |
-| `Remove-PveSnapshot` | `/nodes/{node}/qemu/{vmid}/snapshot/{name}` | DELETE | Open |
-| `Restore-PveSnapshot` | `/nodes/{node}/qemu/{vmid}/snapshot/{name}/rollback` | POST | Open |
-
-#### Networking (5)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Get-PveNetwork` | `/nodes/{node}/network` | GET | Open |
-| `New-PveNetwork` | `/nodes/{node}/network` | POST | Open |
-| `Set-PveNetwork` | `/nodes/{node}/network/{iface}` | PUT | Open |
-| `Remove-PveNetwork` | `/nodes/{node}/network/{iface}` | DELETE | Open |
-| `Invoke-PveNetworkApply` | `/nodes/{node}/network` | PUT | Open |
-
-#### SDN (25)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Get-PveSdnZone` | `/cluster/sdn/zones` | GET | Open |
-| `New-PveSdnZone` | `/cluster/sdn/zones` | POST | Open |
-| `Remove-PveSdnZone` | `/cluster/sdn/zones/{zone}` | DELETE | Open |
-| `Set-PveSdnZone` | `/cluster/sdn/zones/{zone}` | PUT | Open |
-| `Get-PveSdnVnet` | `/cluster/sdn/vnets` | GET | Open |
-| `New-PveSdnVnet` | `/cluster/sdn/vnets` | POST | Open |
-| `Remove-PveSdnVnet` | `/cluster/sdn/vnets/{vnet}` | DELETE | Open |
-| `Set-PveSdnVnet` | `/cluster/sdn/vnets/{vnet}` | PUT | Open |
-| `Get-PveSdnSubnet` | `/cluster/sdn/vnets/{vnet}/subnets` | GET | Open |
-| `New-PveSdnSubnet` | `/cluster/sdn/vnets/{vnet}/subnets` | POST | Open |
-| `Remove-PveSdnSubnet` | `/cluster/sdn/vnets/{vnet}/subnets/{subnet}` | DELETE | Open |
-| `Set-PveSdnSubnet` | `/cluster/sdn/vnets/{vnet}/subnets/{subnet}` | PUT | Open |
-| `Get-PveSdnIpam` | `/cluster/sdn/ipams` | GET | Open |
-| `New-PveSdnIpam` | `/cluster/sdn/ipams` | POST | Open |
-| `Remove-PveSdnIpam` | `/cluster/sdn/ipams/{ipam}` | DELETE | Open |
-| `Set-PveSdnIpam` | `/cluster/sdn/ipams/{ipam}` | PUT | Open |
-| `Get-PveSdnDns` | `/cluster/sdn/dns` | GET | Open |
-| `New-PveSdnDns` | `/cluster/sdn/dns` | POST | Open |
-| `Remove-PveSdnDns` | `/cluster/sdn/dns/{dns}` | DELETE | Open |
-| `Set-PveSdnDns` | `/cluster/sdn/dns/{dns}` | PUT | Open |
-| `Get-PveSdnController` | `/cluster/sdn/controllers` | GET | Open |
-| `New-PveSdnController` | `/cluster/sdn/controllers` | POST | Open |
-| `Remove-PveSdnController` | `/cluster/sdn/controllers/{controller}` | DELETE | Open |
-| `Set-PveSdnController` | `/cluster/sdn/controllers/{controller}` | PUT | Open |
-| `Invoke-PveSdnApply` | `/cluster/sdn` | PUT | Open |
-
-#### Firewall (21)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Get-PveFirewallRule` | `/cluster/firewall/rules` | GET | Open |
-| `New-PveFirewallRule` | `/cluster/firewall/rules` | POST | Open |
-| `Set-PveFirewallRule` | `/cluster/firewall/rules/{pos}` | PUT | Open |
-| `Remove-PveFirewallRule` | `/cluster/firewall/rules/{pos}` | DELETE | Open |
-| `Get-PveFirewallGroup` | `/cluster/firewall/groups` | GET | Open |
-| `New-PveFirewallGroup` | `/cluster/firewall/groups` | POST | Open |
-| `Remove-PveFirewallGroup` | `/cluster/firewall/groups/{group}` | DELETE | Open |
-| `Get-PveFirewallAlias` | `/cluster/firewall/aliases` | GET | Open |
-| `New-PveFirewallAlias` | `/cluster/firewall/aliases` | POST | Open |
-| `Set-PveFirewallAlias` | `/cluster/firewall/aliases/{name}` | PUT | Open |
-| `Remove-PveFirewallAlias` | `/cluster/firewall/aliases/{name}` | DELETE | Open |
-| `Get-PveFirewallIpSet` | `/cluster/firewall/ipset` | GET | Open |
-| `New-PveFirewallIpSet` | `/cluster/firewall/ipset` | POST | Open |
-| `Remove-PveFirewallIpSet` | `/cluster/firewall/ipset/{name}` | DELETE | Open |
-| `Get-PveFirewallIpSetEntry` | `/cluster/firewall/ipset/{name}` | GET | Open |
-| `New-PveFirewallIpSetEntry` | `/cluster/firewall/ipset/{name}` | POST | Open |
-| `Set-PveFirewallIpSetEntry` | `/cluster/firewall/ipset/{name}/{cidr}` | PUT | Open |
-| `Remove-PveFirewallIpSetEntry` | `/cluster/firewall/ipset/{name}/{cidr}` | DELETE | Open |
-| `Get-PveFirewallOptions` | `/cluster/firewall/options` | GET | Open |
-| `Set-PveFirewallOptions` | `/cluster/firewall/options` | PUT | Open |
-| `Get-PveFirewallRef` | `/cluster/firewall/refs` | GET | Open |
-
-#### Backup (6)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `New-PveBackup` | `/nodes/{node}/vzdump` | POST | Open |
-| `Get-PveBackupJob` | `/cluster/backup` | GET | Open |
-| `New-PveBackupJob` | `/cluster/backup` | POST | Open |
-| `Set-PveBackupJob` | `/cluster/backup/{id}` | PUT | Open |
-| `Remove-PveBackupJob` | `/cluster/backup/{id}` | DELETE | Open |
-| `Get-PveBackupInfo` | `/cluster/backup-info/not-backed-up` | GET | Open |
-
-#### Users / Access (23)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Get-PveUser` | `/access/users` | GET | Open |
-| `New-PveUser` | `/access/users` | POST | Open |
-| `Remove-PveUser` | `/access/users/{userid}` | DELETE | Open |
-| `Set-PveUser` | `/access/users/{userid}` | PUT | Open |
-| `Get-PveRole` | `/access/roles` | GET | Open |
-| `New-PveRole` | `/access/roles` | POST | Open |
-| `Remove-PveRole` | `/access/roles/{roleid}` | DELETE | Open |
-| `Set-PveRole` | `/access/roles/{roleid}` | PUT | Open |
-| `Get-PvePermission` | `/access/acl` | GET | Open |
-| `Set-PvePermission` | `/access/acl` | PUT | Open |
-| `Get-PveApiToken` | `/access/users/{userid}/token` | GET | Open |
-| `New-PveApiToken` | `/access/users/{userid}/token/{tokenid}` | POST | Open |
-| `Remove-PveApiToken` | `/access/users/{userid}/token/{tokenid}` | DELETE | Open |
-| `Set-PveApiToken` | `/access/users/{userid}/token/{tokenid}` | PUT | Open |
-| `Get-PveGroup` | `/access/groups` | GET | Open |
-| `New-PveGroup` | `/access/groups` | POST | Open |
-| `Set-PveGroup` | `/access/groups/{groupid}` | PUT | Open |
-| `Remove-PveGroup` | `/access/groups/{groupid}` | DELETE | Open |
-| `Get-PveDomain` | `/access/domains` | GET | Open |
-| `New-PveDomain` | `/access/domains` | POST | Open |
-| `Set-PveDomain` | `/access/domains/{realm}` | PUT | Open |
-| `Remove-PveDomain` | `/access/domains/{realm}` | DELETE | Open |
-| `Set-PvePassword` | `/access/password` | PUT | Open |
-
-#### Pools (4)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Get-PvePool` | `/pools` | GET | Open |
-| `New-PvePool` | `/pools` | POST | Open |
-| `Set-PvePool` | `/pools/{poolid}` | PUT | Open |
-| `Remove-PvePool` | `/pools/{poolid}` | DELETE | Open |
-
-#### Templates (4)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Get-PveTemplate` | `/nodes/{node}/qemu` (filtered) | GET | Open |
-| `New-PveTemplate` | `/nodes/{node}/qemu/{vmid}/template` | POST | Open |
-| `Remove-PveTemplate` | `/nodes/{node}/qemu/{vmid}` | DELETE | Open |
-| `New-PveVmFromTemplate` | `/nodes/{node}/qemu/{vmid}/clone` | POST | Open |
-
-#### Cloud-Init (3)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Get-PveCloudInitConfig` | `/nodes/{node}/qemu/{vmid}/config` | GET | Open |
-| `Set-PveCloudInitConfig` | `/nodes/{node}/qemu/{vmid}/config` | PUT | Open |
-| `Invoke-PveCloudInitRegenerate` | `/nodes/{node}/qemu/{vmid}/cloudinit` | PUT | Open |
-
-#### Tasks (4)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Get-PveTask` | `/nodes/{node}/tasks/{upid}/status` | GET | Open |
-| `Wait-PveTask` | `/nodes/{node}/tasks/{upid}/status` (polling) | GET | Open |
-| `Get-PveTaskList` | `/nodes/{node}/tasks` | GET | Open |
-| `Stop-PveTask` | `/nodes/{node}/tasks/{upid}` | DELETE | Open |
-
-#### Cluster (1)
-| Cmdlet | API Endpoint | Method | Prior Status |
-|---|---|---|---|
-| `Get-PveClusterResource` | `/cluster/resources` | GET | Open |
-
-### Coverage Summary Per Functional Area
-
-| Area | Total Endpoints | Covered | % | 🆕 PVE 9.0 New | Prior Status |
-|---|---|---|---|---|---|
-| acl | 2 | 2 | 100% | 0 | Open |
-| version | 1 | 1 | 100% | 0 | Open |
-| backup | 6 | 5 | 83% | 0 | Open |
-| access_groups | 5 | 4 | 80% | 0 | Open |
-| roles | 5 | 4 | 80% | 0 | Open |
-| tasks | 5 | 4 | 80% | 0 | Open |
-| firewall | 40 | 31 | 78% | 0 | Open |
-| users | 12 | 9 | 75% | 0 | Open |
-| networking | 7 | 5 | 71% | 0 | Open |
-| pools | 7 | 5 | 71% | 0 | Open |
-| access_domains | 6 | 4 | 67% | 0 | Open |
-| storage_config | 5 | 3 | 60% | 0 | Open |
-| storage | 19 | 8 | 42% | 1 | Open |
-| sdn | 60 | 25 | 42% | 16 | Open |
-| vms | 97 | 35 | 36% | 1 | Open |
-| containers | 62 | 21 | 34% | 1 | Open |
-| access | 15 | 2 | 13% | 1 | Open |
-| nodes | 75 | 8 | 11% | 11 | Open |
-| cluster | 77 | 3 | 4% | 6 | Open |
-| ceph | 40 | 0 | 0% | 0 | Open |
-| ha | 21 | 0 | 0% | 5 | Open |
-| disks | 18 | 0 | 0% | 0 | Open |
-| acme | 15 | 0 | 0% | 0 | Open |
-| cluster_config | 10 | 0 | 0% | 0 | Open |
-| certificates | 8 | 0 | 0% | 0 | Open |
-| apt | 8 | 0 | 0% | 0 | Open |
-| services | 7 | 0 | 0% | 0 | Open |
-| metrics | 7 | 0 | 0% | 0 | Open |
-| replication | 5 | 0 | 0% | 0 | Open |
-| **TOTAL** | **646** | **180** | **27.9%** | **42** | — |
-
-### API Drift — Changed Endpoints (Covered Cmdlets)
-
-| Cmdlet | Endpoint | Change Description | Risk | Prior Status |
-|---|---|---|---|---|
-| `Set-PveBackupJob` | `PUT /cluster/backup/{id}` | Removed `notification-policy`, `notification-target` params | Breaking | Open |
-| `Set-PveNetwork` | `PUT /nodes/{node}/network` | Added `regenerate-frr`, removed `skip_frr` | Breaking | Open |
-| `Get-PveVmConfig` | `GET .../qemu/{vmid}/config` | Returns changed in latest version | Cosmetic | Open |
-| `Move-PveVm` | `POST .../qemu/{vmid}/migrate` | Added `with-conntrack-state` parameter | Additive | Open |
-
-### PVE 9.0 New Endpoints (42 total, 0 implemented)
-
-| Area | Endpoint | Method | Description |
-|---|---|---|---|
-| ha | `/cluster/ha/rules` | GET/POST | HA rules system (new in PVE 9.0) |
-| ha | `/cluster/ha/rules/{rule}` | GET/PUT/DELETE | Individual HA rule management |
-| cluster | `/cluster/bulk-action/guest/*` | POST | Bulk guest start/shutdown/suspend/migrate |
-| sdn | `/cluster/sdn/fabrics/*` | GET/POST/PUT/DELETE | SDN fabrics (13 endpoints) |
-| sdn | `/cluster/sdn/lock` / `rollback` | POST/DELETE | SDN lock and rollback |
-| nodes | `/nodes/{node}/capabilities/qemu/cpu-flags` | GET | CPU flags query |
-| nodes | `/nodes/{node}/capabilities/qemu/migration` | GET | Migration capabilities |
-| nodes | `/nodes/{node}/sdn/fabrics/*` | GET | Node SDN fabric status |
-| nodes | `/nodes/{node}/query-oci-repo-tags` | GET | OCI repo tag query |
-| storage | `/nodes/{node}/storage/{storage}/oci-registry-pull` | POST | OCI registry pull |
-| vms | `/nodes/{node}/qemu/{vmid}/dbus-vmstate` | POST | D-Bus VM state |
-| containers | `GET /nodes/{node}/lxc/{vmid}/migrate` | GET | Container migration pre-check |
-| access | `POST /access/vncticket` | POST | VNC ticket creation |
-
-### High-Value Gaps
-
-**Tier 1 — Most impactful for users:**
-
-1. **HA (High Availability)** — 21 endpoints, 0% covered. Essential for production clusters. Resources, groups, status, and the new PVE 9.0 rules system (5 🆕 endpoints).
-2. **Cluster configuration** — 10 endpoints, 0%. Cluster create/join/node management is fundamental for multi-node setups.
-3. **Ceph** — 40 endpoints, 0%. Critical for hyperconverged infrastructure (OSD, MON, pools, status).
-4. **Notifications** — 32 cluster endpoints, 0%. New in PVE 8.1+, essential for monitoring/alerting automation.
-5. **Disk management** — 18 endpoints, 0%. LVM, ZFS, SMART, wipe — needed for storage provisioning.
-
-**Tier 2 — Important operational gaps:**
-
-6. **VM/CT-level firewall** — ~44 combined endpoints. Cluster-level IS covered, but per-VM/CT rules are not.
-7. **SDN Fabrics** (🆕 PVE 9.0) — 13 new endpoints. Key for advanced SDN.
-8. **Resource mappings** (PCI/USB passthrough) — 15 endpoints. Key for GPU passthrough.
-9. **Certificates/ACME** — 23 combined endpoints. TLS certificate management.
-10. **`GET /cluster/nextid`** — Trivial but frequently needed to allocate VM IDs programmatically.
-
----
-
-## Phase 3 — Code Quality & Best Practices
-
-### 3a. PowerShell Module Design
-
-- **All cmdlets use approved PowerShell verbs** via verb class constants.
-- **Noun prefix `Pve`** is consistent across all 169 cmdlets.
-- **HelpMessage** present on virtually all parameters.
-- **ShouldProcess** on all destructive/mutating cmdlets.
-- **Pipeline support** via `ProcessRecord` and `ValueFromPipelineByPropertyName`.
-- **ValidateRange/ValidateNotNullOrEmpty/ValidateSet** used appropriately.
-- **OutputType** on all 169 cmdlets (previously ~54 were missing — now all have it).
-- **All cmdlets are sealed** (previously ~95 were not).
-
-### 3b. C# Code Quality
-
-- **Nullable annotations**: `enable` in both projects.
-- **IDisposable**: `PveHttpClient` correctly implements `IDisposable`.
-- **Logging**: `WriteVerbose` used extensively; `WriteProgress` in file uploads and task waiting.
-- **Exception hierarchy**: 7 custom exception types with context properties.
-- **Bare catches eliminated**: All `catch { }` blocks replaced with filtered `catch (Exception ex) when (...)`.
-- **Magic strings extracted**: Auth header names are now `const string` fields.
-- **Zero TODO/FIXME/HACK markers**.
-- **No commented-out code or dead code**.
-- **Clean code style** matching `.editorconfig`.
-- **Dual JSON attributes removed**: Only `[JsonProperty]` (Newtonsoft) remains; `[JsonPropertyName]` removed.
-
-### Code Quality Findings
-
-| ID | File | Line | Severity | Description | Prior Status |
-|---|---|---|---|---|---|
-| CQ1 | `NewPveContainerSnapshotCmdlet.cs` | 75 | **Critical** | **Infinite loop with no timeout in private `WaitForTask`.** `while (true)` with no timeout or cancellation. If task never completes, cmdlet hangs forever. Should use `TaskService.WaitForTask` (same pattern that was fixed in VM snapshot cmdlets). | New |
-| CQ2 | `RemovePveContainerSnapshotCmdlet.cs` | 67 | **Critical** | **Identical infinite loop** — same `while(true)` no-timeout pattern as CQ1. | New |
-| CQ3 | `RestorePveContainerSnapshotCmdlet.cs` | 70 | **Critical** | **Identical infinite loop** — same pattern as CQ1. | New |
-| CQ4 | `InvokePveStorageDownloadCmdlet.cs` | 83 | **Critical** | **Infinite loop in download task polling.** `while(true)` with no timeout. Should use `TaskService.WaitForTask`. | New |
-| CQ5 | `SendPveFileCmdlet.cs` | 143 | **Critical** | **Infinite loop in upload task polling.** `while(true)` with no timeout. Should use `TaskService.WaitForTask`. | New |
-| CQ6 | Both `.csproj` files | 4 | **Medium** | **`net9.0` target is EOL** (May 2025). Should upgrade to `net10.0` (LTS). | Open |
-| CQ7 | `PveHttpClient.cs` | ~154 sites | **Medium** | **Sync-over-async via `.GetAwaiter().GetResult()`** across ~216 call sites. Standard for PS binary modules but carries deadlock risk. | Open |
-| CQ8 | `PveCmdletBase.cs` | 149 | **Medium** | **Broad catch in status polling.** `catch (Exception ex) when (...)` is filtered but still catches broadly. Improvement over bare catch but could be more specific. | Open |
-| CQ9 | `RestartPveContainerCmdlet.cs` | 15 | **Medium** | **Missing `ConfirmImpact.High`** — `RestartPveVmCmdlet` has it but container counterpart does not. Inconsistent. | New |
-| CQ10 | `SuspendPveContainerCmdlet.cs` | 14 | **Medium** | **Missing `ConfirmImpact.High`** — `SuspendPveVmCmdlet` has it but container counterpart does not. Inconsistent. | New |
-| CQ11 | 38 cmdlet files | varies | **Medium** | **`new PveHttpClient` per-operation.** 38 cmdlets create their own PveHttpClient instance instead of going through services. Bypasses connection pooling. | Open |
-| CQ12 | `PSProxmoxVE.csproj` | 22 | **Low** | **`System.Management.Automation` pinned to 7.4.0.** Consider updating if targeting .NET 10. | Open |
-
-### Fixed Since Prior Scan
-
-| Prior ID | Description | Status |
-|---|---|---|
-| CQ1–CQ4 | Infinite loop WaitForTask in InvokePveNetworkApply, NewPveSnapshot, RestorePveSnapshot, RemovePveSnapshot | **Fixed** — now uses `TaskService.WaitForTask` |
-| CQ5 | Infinite poll loop in InvokePveVmGuestExec | **Fixed** — added Timeout parameter with Stopwatch + TimeoutException |
-| CQ7 | Duplicated WaitForTask methods (4 VM/network cmdlets) | **Fixed** — uses TaskService |
-| CQ9 | ~54 cmdlets missing OutputType | **Fixed** — all 169 cmdlets now have `[OutputType]` |
-| CQ10 | Firewall VmId defaults to 0 instead of nullable | **Fixed** — no longer `int` default 0 pattern |
-| CQ11 | Bare catch in PveHttpClient.ExtractErrorMessage | **Fixed** — proper exception handling |
-| CQ12 | Bare catch in PveCmdletBase status polling | **Fixed** — filtered `when` clause excludes OOM/SOE |
-| CQ13 | Bare catch in VmService + ContainerService | **Fixed** — filtered to PveApiException/HttpRequestException |
-| CQ14 | Bare catch in GetPveVmCmdlet | **Fixed** — proper exception handling |
-| CQ17 | RemovePveRoleCmdlet missing ConfirmImpact.High | **Fixed** — now has `ConfirmImpact = ConfirmImpact.High` |
-| CQ18 | ~95 cmdlets not sealed | **Fixed** — all 169 cmdlets are now `sealed` |
-| CQ19 | SuspendPveVmCmdlet missing ConfirmImpact.High | **Fixed** — now has `ConfirmImpact.High` |
-| CQ20 | RestartPveVmCmdlet missing ConfirmImpact | **Fixed** — now has `ConfirmImpact.High` |
-| CQ21 | Dual JSON serialization attributes | **Fixed** — `[JsonPropertyName]` removed, only `[JsonProperty]` remains |
-| CQ22 | Auth header magic strings | **Fixed** — extracted to `const string ApiTokenPrefix` and `CsrfHeaderName` |
-| CQ6 (prior) | Pool/CephPool parameter conflict in NewPveStorageCmdlet | **Fixed** — runtime validation with `ThrowTerminatingError` if both specified |
-
----
-
-## Phase 4 — Testing Coverage Analysis
-
-### Test Projects
-
-| Project | Framework | Type | Runner |
-|---|---|---|---|
-| `PSProxmoxVE.Core.Tests` | xUnit 2.7 + Moq 4.20 | Unit tests (models, auth) | `dotnet test` |
-| `PSProxmoxVE.Tests` | Pester 5 | Cmdlet + Integration tests | `Invoke-Pester` |
-
-### xUnit Tests (Core Library)
-
-15 test files covering authentication (PveSession, PveAuthenticator, PveVersion) and model deserialization (12 model areas including backup, cluster, firewall, SDN) with PVE 8 and PVE 9 JSON fixtures. ~180+ xUnit tests total.
-
-### Pester Tests
-
-100% of 169 cmdlets have Pester unit tests covering: command existence, parameter metadata, mandatory checks, parameter types, ShouldProcess presence, ConfirmImpact declarations, and "no session" error behavior.
-
-### Integration Tests
-
-- Self-hosted runner provisions nested PVE 8 and PVE 9 via Terraform + auto-install ISO
-- Tests check for `PVETEST_*` environment variables, skip gracefully when unavailable
-- Robust `AfterAll` cleanup of VMs, containers, users, roles, firewall rules, backup jobs
-- CRUD patterns: create → verify → update → verify → delete → verify
-- SDN tests check PVE version and skip if older
-
-### Test Coverage Gap List (cmdlets lacking integration tests)
-
-| Cmdlet | Unit Test | Integration Test | Prior Status |
-|---|---|---|---|
-| **Nodes** | | | |
-| Get-PveNodeConfig | Yes | No | Open |
-| Set-PveNodeConfig | Yes | No | Open |
-| Get-PveNodeDns | Yes | No | Open |
-| Set-PveNodeDns | Yes | No | Open |
-| Start-PveNodeVms | Yes | No | Open |
-| Stop-PveNodeVms | Yes | No | Open |
-| **VMs** | | | |
-| Move-PveVm | Yes | No | Open |
-| Move-PveVmDisk | Yes | No | Open |
-| Remove-PveVmDisk | Yes | No | Open |
-| **Guest Agent** | | | |
-| Get-PveVmGuestOsInfo | Yes | No | Open |
-| Get-PveVmGuestFsInfo | Yes | No | Open |
-| Read-PveVmGuestFile | Yes | No | Open |
-| Write-PveVmGuestFile | Yes | No | Open |
-| Set-PveVmGuestPassword | Yes | No | Open |
-| Invoke-PveVmGuestFsTrim | Yes | No | Open |
-| **Containers** | | | |
-| Move-PveContainer | Yes | No | Open |
-| Suspend-PveContainer | Yes | No | Open |
-| Resume-PveContainer | Yes | No | Open |
-| Resize-PveContainerDisk | Yes | No | Open |
-| New-PveContainerTemplate | Yes | No | Open |
-| Move-PveContainerVolume | Yes | No | Open |
-| Get-PveContainerInterface | Yes | No | Open |
-| **Storage** | | | |
-| Set-PveStorage | Yes | No | Open |
-| Get-PveStorageStatus | Yes | No | Open |
-| Remove-PveStorageContent | Yes | No | Open |
-| Set-PveStorageContent | Yes | No | Open |
-| New-PveStorageDisk | Yes | No | Open |
-| **SDN** | | | |
-| Set-PveSdnZone | Yes | No | Open |
-| Set-PveSdnVnet | Yes | No | Open |
-| Set-PveSdnSubnet | Yes | No | Open |
-| New-PveSdnIpam | Yes | No | Open |
-| Remove-PveSdnIpam | Yes | No | Open |
-| Set-PveSdnIpam | Yes | No | Open |
-| New-PveSdnDns | Yes | No | Open |
-| Remove-PveSdnDns | Yes | No | Open |
-| Set-PveSdnDns | Yes | No | Open |
-| New-PveSdnController | Yes | No | Open |
-| Remove-PveSdnController | Yes | No | Open |
-| Set-PveSdnController | Yes | No | Open |
-| Invoke-PveSdnApply | Yes | No | Open |
-| **Users / Access** | | | |
-| Set-PveRole | Yes | No | Open |
-| Set-PveApiToken | Yes | No | Open |
-| Get-PveGroup | Yes | No | Open |
-| New-PveGroup | Yes | No | Open |
-| Set-PveGroup | Yes | No | Open |
-| Remove-PveGroup | Yes | No | Open |
-| Get-PveDomain | Yes | No | Open |
-| New-PveDomain | Yes | No | Open |
-| Set-PveDomain | Yes | No | Open |
-| Remove-PveDomain | Yes | No | Open |
-| Set-PvePassword | Yes | No | Open |
-| **Pools** | | | |
-| Get-PvePool | Yes | No | Open |
-| New-PvePool | Yes | No | Open |
-| Set-PvePool | Yes | No | Open |
-| Remove-PvePool | Yes | No | Open |
-| **Templates** | | | |
-| New-PveTemplate | Yes | No | Open |
-| Remove-PveTemplate | Yes | No | Open |
-| **Tasks** | | | |
-| Get-PveTaskList | Yes | No | Open |
-| Stop-PveTask | Yes | No | Open |
-| **Firewall** | | | |
-| Get-PveFirewallGroup | Yes | No | Open |
-| New-PveFirewallGroup | Yes | No | Open |
-| Remove-PveFirewallGroup | Yes | No | Open |
-| Set-PveFirewallAlias | Yes | No | Open |
-| Set-PveFirewallIpSetEntry | Yes | No | Open |
-| Set-PveFirewallOptions | Yes | No | Open |
-| Get-PveFirewallRef | Yes | No | Open |
-| **Backup** | | | |
-| New-PveBackup | Yes | No | Open |
-| Get-PveBackupInfo | Yes | No | Open |
-| **Cluster** | | | |
-| Get-PveClusterResource | Yes | No | Open |
-
-**Summary**: 169/169 unit tested (100%), ~105/169 integration tested (~62%).
-
----
-
-## Phase 5 — Security Review
-
-### 5.1 Credential Handling
-
-| Aspect | Status | Details | Prior Status |
-|---|---|---|---|
-| PSCredential usage | **Good** | `Connect-PveServer` accepts `PSCredential` | Open |
-| SecureString pattern | **Good** | Consistent `Marshal.SecureStringToGlobalAllocUnicode` + `ZeroFreeGlobalAllocUnicode` in try/finally | Open |
-| Set-PveVmGuestPassword | **Good** | Now uses `SecureString` (was plain `string`) | Fixed |
-| API token in memory | **Acceptable** | Stored as plain `string` in `PveSession.ApiToken` | Open |
-| Credential logging | **Good** | No WriteVerbose/WriteDebug includes credentials | Open |
-| Ticket storage | **Good** | In `PveSession` only, not written to disk | Open |
-| Session expiry | **Good** | 2-hour expiry via `IsExpired` property | Open |
-
-### 5.2 TLS/HTTPS
-
-| Aspect | Status | Details | Prior Status |
-|---|---|---|---|
-| HTTPS enforced | **Good** | `BaseUrl` always `https://`, no HTTP fallback | Open |
-| SkipCertificateCheck | **Good** | Explicit opt-in with `WriteWarning` advisory | Open |
-| TLS version | **Good** | OS/runtime negotiation (no pinning) | Open |
-| CSRF protection | **Good** | Token included on all POST/PUT/DELETE with ticket auth | Open |
-
-### 5.3 Input Validation
-
-| Aspect | Status | Details | Prior Status |
-|---|---|---|---|
-| VMID validation | **Good** | `[ValidateRange(100, 999999999)]` on all VmId params | Open |
-| URL encoding | **Good** | `Uri.EscapeDataString()` applied consistently across all services (VmService, ContainerService, StorageService, NetworkService, FirewallService, UserService, BackupService, PoolService, TaskService, SnapshotService, ClusterService) | Fixed |
-| API token format | **Good** | Regex validation in `PveAuthenticator` | Open |
-| Port validation | **Good** | `[ValidateRange(1, 65535)]` on port parameter | Open |
-
-### 5.4 Dependency Security
-
-| Package | Version | Pinned | Notes | Prior Status |
-|---|---|---|---|---|
-| `Newtonsoft.Json` | 13.0.3 | Yes | Latest stable | Open |
-| `System.Text.Json` | 8.0.5 | Yes | Latest 8.x patch | Open |
-| `SharpCompress` | 0.38.0 | Yes | OVA/tar extraction | Open |
-| `PowerShellStandard.Library` | 5.1.1 | Yes | PrivateAssets=all | Open |
-| `System.Management.Automation` | 7.4.0 | Yes | PrivateAssets=all | Open |
-
-### Security Findings
-
-| ID | Area | File | Severity | Description | Prior Status |
-|---|---|---|---|---|---|
-| S1 | Hardcoded password | `integration-tests.yml:42` | **Low** | `PVE_PASSWORD: "Testpass123!"` in workflow env. Masked via `::add-mask::`. Disposable test VM. | Open |
-| S2 | Hardcoded password | `variables.tf:80` | **Low** | Default `"Testpass123!"` for test VM password. Disposable nested VM. | Open |
-
-### Fixed Since Prior Scan
-
-| Prior ID | Description | Status |
-|---|---|---|
-| S1 | `SetPveVmGuestPasswordCmdlet.cs` Password is `string`, not `SecureString` | **Fixed** — now uses `SecureString` |
-| S2 | `debug/Capture-UploadDiff.ps1` hardcoded API token and IP | **Fixed** — replaced with placeholder tokens ``, `@!=` |
-| S3 | URL encoding missing in VmService, ContainerService, StorageService | **Fixed** — `Uri.EscapeDataString()` now used consistently across all services |
-
----
-
-## Phase 6 — PSGallery Publication Readiness
-
-| # | Check | Pass/Fail | Notes | Prior Status |
-|---|---|---|---|---|
-| 1 | ModuleVersion is SemVer | **Pass** | `0.1.0` | Open |
-| 2 | GUID present and stable | **Pass** | `a3f7c2d1-84e5-4b9f-a061-3e2d8c5f1a7b` | Open |
-| 3 | Author | **Pass** | `goodolclint` | Open |
-| 4 | CompanyName | **Pass** | `Worklab` | Open |
-| 5 | Copyright | **Pass** | `(c) 2026 goodolclint. All rights reserved.` | Open |
-| 6 | Description (meaningful) | **Pass** | Mentions PVE 8.x/9.x and feature areas | Open |
-| 7 | PowerShellVersion minimum | **Pass** | `5.1` | Open |
-| 8 | RequiredAssemblies | **Pass** | `PSProxmoxVE.Core.dll`, `Newtonsoft.Json.dll` | Open |
-| 9 | CmdletsToExport explicit list | **Pass** | Fully enumerated (169 cmdlets), no wildcards | Open |
-| 10 | FunctionsToExport | **Pass** | Explicitly `@()` | Open |
-| 11 | Tags for discoverability | **Pass** | 8 tags incl. Proxmox, PVE, Virtualization, IaC | Open |
-| 12 | ProjectUri | **Pass** | GitHub repo URL | Open |
-| 13 | LicenseUri | **Pass** | Points to LICENSE | Open |
-| 14 | IconUri | **Fail** | Not set — cosmetic | Open |
-| 15 | ReleaseNotes | **Pass** | In PSData | Open |
-| 16 | Prerelease string | **Pass** | `'preview'` | Open |
-| 17 | LICENSE (OSI-approved) | **Pass** | MIT | Open |
-| 18 | README: Install-Module | **Pass** | `Install-Module -Name PSProxmoxVE` documented | Open |
-| 19 | README: Quick-start examples | **Pass** | Multiple scenarios | Open |
-| 20 | README: Authentication guide | **Pass** | Ticket + API token | Open |
-| 21 | README: Badges | **Pass** | Build, Unit Tests, License, PSGallery | Open |
-| 22 | Publish workflow | **Pass** | Tag-triggered via `publish.yml` | Open |
-| 23 | Publish: version stamping | **Pass** | Extracts from git tag | Open |
-| 24 | Publish: smoke test | **Pass** | Loads module, asserts cmdlet count | Open |
-| 25 | Publish: API key as secret | **Pass** | `secrets.PSGALLERY_API_KEY` | Open |
-| 26 | Publish: GitHub Release | **Pass** | Auto-created via `softprops/action-gh-release` | Open |
-| 27 | CHANGELOG | **Pass** | Keep a Changelog format + Conventional Commits | Open |
-| 28 | Publish: single netstandard2.0 | **Warning** | Only builds netstandard2.0. Verify it loads under Win PS 5.1 since manifest declares `DotNetFrameworkVersion = '4.8'`. | Open |
-
----
-
-## Phase 7 — Community & Repo Maintenance Standards
-
-| # | Check | Pass/Fail | Notes | Prior Status |
-|---|---|---|---|---|
-| 1 | Bug report issue template | **Pass** | YAML form with module/PVE/PS version fields | Open |
-| 2 | Feature request template | **Pass** | Description, use case, API endpoints | Open |
-| 3 | PR template | **Pass** | Summary, type checkboxes, testing checklist | Open |
-| 4 | `CONTRIBUTING.md` | **Pass** | Prerequisites, building, tests, coding standards, PR process | Open |
-| 5 | `CODE_OF_CONDUCT.md` | **Pass** | Contributor Covenant 2.1 | Open |
-| 6 | `SECURITY.md` | **Pass** | Vulnerability disclosure, 48h SLA | Open |
-| 7 | Conventional commits | **Pass** | Consistent `feat:`, `fix:`, `fix(test):` in history | Open |
-| 8 | `.gitattributes` | **Pass** | Comprehensive: csharp diff, binary markers, LF for .sh | Open |
-| 9 | Issue template config.yml | **Fail** | No `config.yml` to control blank issues | Open |
-| 10 | CODEOWNERS | **Fail** | Not present. Low priority for single maintainer. | Open |
-| 11 | FUNDING.yml / community links | **Fail** | No Discussions/Discord/Funding references | Open |
-| 12 | Default branch `main` | **Pass** | Confirmed | Open |
-
-### Recommended Branch Protection
-
-- Require PR reviews before merge
-- Require status checks (build + unit tests + integration tests)
-- Require linear history or squash merging
-
----
-
-## Phase 8 — Prioritized Recommendations
-
-### 🔴 Critical (blocks PSGallery publication or is a security risk)
-
-| # | What | Where | Why | Fix | Prior Status |
-|---|---|---|---|---|---|
-| C1 | **Infinite loop in 5 cmdlets' WaitForTask methods** | `NewPveContainerSnapshotCmdlet.cs`, `RemovePveContainerSnapshotCmdlet.cs`, `RestorePveContainerSnapshotCmdlet.cs`, `InvokePveStorageDownloadCmdlet.cs`, `SendPveFileCmdlet.cs` | `while(true)` with no timeout — cmdlet hangs forever if task stalls. Same pattern that was fixed in VM snapshot cmdlets. | Replace private `WaitForTask` with `TaskService.WaitForTask` (identical fix to CQ1–CQ4 from prior scan) | New |
-
-### 🟠High (significantly impacts quality or community adoption)
-
-| # | What | Where | Why | Fix | Prior Status |
-|---|---|---|---|---|---|
-| H1 | **HA subsystem (0% coverage)** | Module | 21 endpoints + 5 🆕 PVE 9.0, essential for production clusters | Implement HA resources, groups, status, and PVE 9.0 rules | Open |
-| H2 | **Ceph subsystem (0% coverage)** | Module | 40 endpoints, critical for hyperconverged infrastructure | Implement Ceph OSD, MON, pool, status operations | Open |
-| H3 | **net9.0 target is EOL** | Both `.csproj` files | .NET 9.0 EOL was May 2025. Should use LTS release. | Upgrade to `net10.0` | Open |
-
-### 🟡 Medium (best practice gaps, test coverage holes)
-
-| # | What | Where | Why | Fix | Prior Status |
-|---|---|---|---|---|---|
-| M1 | **HttpClient per-call pattern** | 38 cmdlet files + services | 38 cmdlets create `new PveHttpClient` per operation, bypassing connection pooling | Refactor to client-per-session or use `IHttpClientFactory` pattern | Open |
-| M2 | **64 cmdlets lack integration tests** | Integration tests | 38% of cmdlets untested end-to-end | Prioritize pools, groups, domains, node ops, container gaps, guest agent extensions | Open |
-| M3 | **VM/CT-level firewall (0% coverage)** | Module | ~44 endpoints. Cluster/node-level covered but per-VM/CT rules are not. | Add VM/CT firewall cmdlets using existing firewall service patterns | Open |
-| M4 | **Cluster config (0% coverage)** | Module | 10 endpoints for cluster create/join/node management | Implement for multi-node cluster automation | Open |
-| M5 | **PVE 9.0 endpoints (0% coverage)** | Module | 42 new endpoints in latest PVE version. Users on PVE 9 will expect support. | Prioritize HA rules, bulk actions, SDN fabrics | New |
-| M6 | **Broad exception catches** | `PveCmdletBase.cs`, `VmService.cs`, `ContainerService.cs` | Filtered `when` clauses are better than bare catches but still catch broadly | Consider more specific exception types | Open |
-
-### 🟢 Low / Nice-to-have (polish, discoverability, stretch goals)
-
-| # | What | Where | Why | Fix | Prior Status |
-|---|---|---|---|---|---|
-| L1 | No `IconUri` in manifest | `PSProxmoxVE.psd1` | Improves PSGallery listing appearance | Create icon, host it, add URI | Open |
-| L2 | `System.Management.Automation` pinned to 7.4.0 | `PSProxmoxVE.csproj` | Consider updating when targeting .NET 10 | Update with TFM migration | Open |
-| L3 | No `config.yml` for issue templates | `.github/ISSUE_TEMPLATE/` | Controls blank issue creation | Add simple config.yml | Open |
-| L4 | No CODEOWNERS | Repo root | Documents ownership for PR auto-assignment | Add `CODEOWNERS` with maintainer | Open |
-| L5 | Disk management (0% coverage) | Module | 18 endpoints — LVM, ZFS, SMART, wipe | Implement as demand grows | Open |
-| L6 | Notifications (0% coverage) | Module | 32 endpoints — new in PVE 8.1+ | Implement for monitoring automation | Open |
-| L7 | ACME/Certificates (0% coverage) | Module | 23 endpoints — TLS cert management | Implement as demand grows | Open |
-| L8 | Verify netstandard2.0 loads on PS 5.1 | Publish workflow | Manifest says Desktop compatible | Add PS 5.1 smoke test to publish workflow | Open |
-
----
-
-## Summary Statistics
-
-| Metric | This Scan | Prior Scan | Delta |
-|---|---|---|---|
-| Total cmdlets | 169 | 148 | +21 |
-| Cmdlets with ShouldProcess | All mutating | All mutating | — |
-| Cmdlets with OutputType | 169 (100%) | ~94 (~64%) | +36% |
-| Cmdlets sealed | 169 (100%) | ~60 (~40%) | +60% |
-| Cmdlets with HelpMessage | 169 (100%) | 148 (100%) | — |
-| xUnit test files | 15 | 15 | — |
-| Pester test files | ~48 | ~45 | +3 |
-| Integration test coverage | ~62% (105/169) | ~57% (85/148) | +5% |
-| PVE API areas with >0% coverage | 17 of 30 | 17 of 29 | — |
-| PVE API total coverage | 27.9% (180/646) | 27.9% (180/646) | — |
-| Critical issues | 1 | 2 | -1 |
-| High issues | 3 | 5 | -2 |
-| Medium issues | 7 | 10 | -3 |
-| Low issues | 7 | 11 | -4 |
-| **Total issues** | **18** | **28** | **-10** |
-| Issues fixed from prior | 16 | 9 | +7 |
-| New issues found | 5 | 16 | -11 |
-| NuGet dependencies (runtime) | 3 | 3 | — |
-| Security vulnerabilities | 0 | 0 | — |
-| Community files present | 9/9 | 9/9 | — |
-
-\* Integration test coverage improved both in absolute count (+20 cmdlets) and percentage (+5%).
diff --git a/debug/.gitignore b/debug/.gitignore
deleted file mode 100644
index d915a05..0000000
--- a/debug/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-*.flow
-_analyze.py
-run_analyze.py
diff --git a/debug/Capture-UploadDiff.ps1 b/debug/Capture-UploadDiff.ps1
deleted file mode 100644
index 39068de..0000000
--- a/debug/Capture-UploadDiff.ps1
+++ /dev/null
@@ -1,192 +0,0 @@
-#Requires -Version 7.0
-# Capture-UploadDiff.ps1
-# Starts mitmproxy, captures working PS approach vs C# module upload,
-# then prints a side-by-side diff of the multipart framing.
-
-Set-StrictMode -Version Latest
-$ErrorActionPreference = 'Stop'
-
-$DebugDir = $PSScriptRoot
-$ProxyPort = 8080
-$TestFile = '/tmp/pvetest.iso'
-# Replace with your PVE host and API token before running
-$UploadUri = 'https://:8006/api2/json/nodes/pve/storage/local/upload'
-$AuthHeader = 'PVEAPIToken=@!='
-$ModuleDll = '/Users/goodolclint/Source/PSProxmoxVE/src/PSProxmoxVE/bin/Debug/net9.0/PSProxmoxVE.dll'
-
-if (-not (Test-Path $TestFile)) {
- Write-Error "Test file not found: $TestFile"
-}
-
-# ---------------------------------------------------------------------------
-function Start-Mitmdump([string]$FlowFile) {
- Get-Process mitmdump -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
- Start-Sleep -Seconds 1
- Remove-Item $FlowFile -ErrorAction SilentlyContinue
-
- $p = Start-Process mitmdump -ArgumentList @(
- '--mode', 'regular',
- '--ssl-insecure',
- '--listen-port', $ProxyPort,
- '-w', $FlowFile
- ) -PassThru -RedirectStandardOutput '/tmp/mitmdump-out.log' -RedirectStandardError '/tmp/mitmdump-err.log'
-
- Start-Sleep -Seconds 2
- if ($p.HasExited) {
- Write-Error "mitmdump failed to start: $(Get-Content /tmp/mitmdump-err.log -Raw)"
- }
- Write-Host " mitmdump PID $($p.Id)" -ForegroundColor DarkGray
- return $p
-}
-
-function Stop-Mitmdump($p) {
- Start-Sleep -Seconds 1
- if (-not $p.HasExited) { $p.Kill(); $p.WaitForExit(3000) | Out-Null }
-}
-
-# ---------------------------------------------------------------------------
-Write-Host '=== Step 1: Capture working PowerShell HttpClient upload ===' -ForegroundColor Cyan
-$psFlow = "$DebugDir/ps-module-capture.flow"
-$mitm = Start-Mitmdump $psFlow
-
-try {
- $handler = [System.Net.Http.HttpClientHandler]::new()
- $handler.ServerCertificateCustomValidationCallback = [System.Net.Http.HttpClientHandler]::DangerousAcceptAnyServerCertificateValidator
- $handler.Proxy = [System.Net.WebProxy]::new("http://localhost:$ProxyPort")
- $handler.UseProxy = $true
- $client = [System.Net.Http.HttpClient]::new($handler)
- $client.Timeout = [TimeSpan]::FromMinutes(5)
- $client.DefaultRequestHeaders.TryAddWithoutValidation('Authorization', $AuthHeader) | Out-Null
-
- $boundary = 'PSDirectBoundary12345678'
- $content = [System.Net.Http.MultipartFormDataContent]::new($boundary)
- $content.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("multipart/form-data; boundary=$boundary")
-
- function New-TextPart([string]$name, [string]$value) {
- $part = [System.Net.Http.StringContent]::new($value, [System.Text.Encoding]::UTF8)
- $part.Headers.ContentType = $null
- $part.Headers.ContentDisposition = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new('form-data')
- $part.Headers.ContentDisposition.Name = "`"$name`""
- return $part
- }
-
- $content.Add((New-TextPart 'content' 'iso'))
-
- $fileStream = [System.IO.File]::OpenRead($TestFile)
- $fileName = [System.IO.Path]::GetFileName($TestFile)
- $fileContent = [System.Net.Http.StreamContent]::new($fileStream)
- $fileContent.Headers.ContentDisposition = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new('form-data')
- $fileContent.Headers.ContentDisposition.Name = '"filename"'
- $fileContent.Headers.ContentDisposition.FileName = "`"$fileName`""
- $fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('application/octet-stream')
- $content.Add($fileContent)
-
- Write-Host " Posting..."
- $response = $client.PostAsync($UploadUri, $content).GetAwaiter().GetResult()
- $body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
- Write-Host " Status: $($response.StatusCode)" -ForegroundColor Green
- Write-Host " Body: $body"
-} catch {
- Write-Host " ERROR: $_" -ForegroundColor Red
-} finally {
- if ($fileStream) { $fileStream.Dispose() }
- if ($client) { $client.Dispose() }
-}
-
-Stop-Mitmdump $mitm
-Write-Host " Saved: $psFlow" -ForegroundColor Green
-
-# ---------------------------------------------------------------------------
-Write-Host ''
-Write-Host '=== Step 2: Capture C# module upload ===' -ForegroundColor Cyan
-$moduleFlow = "$DebugDir/cs-module-capture.flow"
-$mitm = Start-Mitmdump $moduleFlow
-
-# Route HttpClient through proxy via env vars (.NET honors HTTPS_PROXY)
-$env:HTTPS_PROXY = "http://localhost:$ProxyPort"
-$env:HTTP_PROXY = "http://localhost:$ProxyPort"
-
-try {
- Import-Module $ModuleDll -Force -ErrorAction Stop
-
- # Replace with your PVE host and API token before running
- Connect-PveServer -Server '' -Port 8006 `
- -ApiToken '@!=' `
- -SkipCertificateCheck
-
- Write-Host " Calling Send-PveIso..."
- Send-PveIso -Node pve -Storage local -Path $TestFile -Confirm:$false -ErrorAction Stop
- Write-Host " Upload succeeded!" -ForegroundColor Green
-} catch {
- Write-Host " ERROR: $_" -ForegroundColor Red
- Write-Host " Type: $($_.Exception.GetType().FullName)"
- if ($_.Exception.InnerException) {
- Write-Host " Inner: $($_.Exception.InnerException.Message)"
- }
-} finally {
- $env:HTTPS_PROXY = ''
- $env:HTTP_PROXY = ''
-}
-
-Stop-Mitmdump $mitm
-Write-Host " Saved: $moduleFlow" -ForegroundColor Green
-
-# ---------------------------------------------------------------------------
-Write-Host ''
-Write-Host '=== Step 3: Diff multipart framing ===' -ForegroundColor Cyan
-
-$analyzeScript = @'
-import sys
-from mitmproxy.io import FlowReader
-
-def dump_flow(path, label):
- print(f'\n{"=" * 70}')
- print(f' {label}')
- print(f'{"=" * 70}')
- try:
- with open(path, 'rb') as f:
- reader = FlowReader(f)
- flows = list(reader.stream())
- if not flows:
- print(' NO FLOWS CAPTURED')
- return
- for i, flow in enumerate(flows):
- req = flow.request
- print(f'\nFlow {i}: {req.method} {req.pretty_url}')
- print(f'HTTP version: {req.http_version}')
- print(f'\nRequest headers:')
- for k, v in req.headers.items(multi=True):
- display_v = v[:40] + '...[REDACTED]' if k.lower() == 'authorization' else v
- print(f' {k}: {display_v}')
- body = req.content
- print(f'\nBody size: {len(body)} bytes')
- # Show first 1500 bytes (multipart preamble before file data)
- preview = body[:1500]
- print(f'\nBody start (first 1500 bytes):')
- for line in preview.split(b'\r\n'):
- try:
- print(f' {line.decode("ascii")}')
- except Exception:
- if len(line) > 40:
- print(f' [binary: {len(line)} bytes]')
- else:
- print(f' {line!r}')
- if flow.response:
- print(f'\nResponse: {flow.response.status_code} {flow.response.reason}')
- body_text = flow.response.content[:300].decode('utf-8', errors='replace')
- print(f' Body: {body_text}')
- else:
- print('\nResponse: NONE (connection reset/failed)')
- except Exception as e:
- print(f'Error reading {path}: {e}')
-
-dump_flow(sys.argv[1], 'WORKING: PS Direct HttpClient')
-dump_flow(sys.argv[2], 'FAILING: C# Module (Send-PveIso)')
-'@
-
-$py = "$DebugDir/_analyze.py"
-Set-Content -Path $py -Value $analyzeScript
-python3 $py $psFlow $moduleFlow
-
-Write-Host ''
-Write-Host '=== Done ===' -ForegroundColor Cyan
diff --git a/docs/PVE_API_COVERAGE.md b/docs/PVE_API_COVERAGE.md
deleted file mode 100644
index 4fa765d..0000000
--- a/docs/PVE_API_COVERAGE.md
+++ /dev/null
@@ -1,83 +0,0 @@
-# Proxmox VE API Coverage
-
-This document tracks which PVE API areas are implemented in PSProxmoxVE and which are planned for future releases.
-
-**Last updated:** 2026-03-21
-**Module version:** 0.1.0-preview
-**Total cmdlets:** 169
-
-## Implemented
-
-| Area | Cmdlets | API Endpoints |
-|------|---------|---------------|
-| **Connection** | 3 | `POST /access/ticket`, `DELETE /access/ticket` |
-| **Nodes** | 2 | `GET /nodes`, `GET /nodes/{node}/status` |
-| **VMs (QEMU)** | 19 | `/nodes/{node}/qemu/*` (CRUD, lifecycle, clone, migrate, resize, disk import, config, guest agent) |
-| **Containers (LXC)** | 14 | `/nodes/{node}/lxc/*` (CRUD, lifecycle, clone, migrate, config, snapshots) |
-| **Storage** | 6 | `/storage`, `/nodes/{node}/storage/{storage}/*` (CRUD, content, upload, download) |
-| **Snapshots** | 4 | `/nodes/{node}/qemu/{vmid}/snapshot/*` (CRUD, rollback) |
-| **Container Snapshots** | 4 | `/nodes/{node}/lxc/{vmid}/snapshot/*` (CRUD, rollback) |
-| **Networking** | 5 | `/nodes/{node}/network/*` (CRUD, apply) |
-| **SDN Zones** | 3 | `/cluster/sdn/zones/*` (CRUD) |
-| **SDN VNets** | 3 | `/cluster/sdn/vnets/*` (CRUD) |
-| **SDN Subnets** | 3 | `/cluster/sdn/vnets/{vnet}/subnets/*` (CRUD) |
-| **Users** | 4 | `/access/users/*` (CRUD) |
-| **Roles** | 3 | `/access/roles/*` (CRUD) |
-| **Permissions** | 2 | `/access/acl` (get, set) |
-| **API Tokens** | 3 | `/access/users/{userid}/tokens/*` (CRUD) |
-| **Templates** | 4 | VM template conversion, listing, cloning, removal |
-| **Cloud-Init** | 3 | `/nodes/{node}/qemu/{vmid}/config` (cloud-init fields), `/nodes/{node}/qemu/{vmid}/cloudinit/regenerate` |
-| **Tasks** | 4 | `/nodes/{node}/tasks` (list, get status, stop, wait) |
-| **Firewall** | 21 | `/cluster/firewall/*`, `/nodes/{node}/firewall/*`, `/nodes/{node}/qemu/{vmid}/firewall/*`, `/nodes/{node}/lxc/{vmid}/firewall/*` (rules, groups, aliases, IP sets, options, refs) |
-| **Backup** | 7 | `/nodes/{node}/vzdump`, `/cluster/backup/*`, `/cluster/backup-info/not-backed-up` |
-| **SDN Zones** | 4 | `/cluster/sdn/zones/*` (CRUD + update) |
-| **SDN VNets** | 4 | `/cluster/sdn/vnets/*` (CRUD + update) |
-| **SDN Subnets** | 4 | `/cluster/sdn/vnets/{vnet}/subnets/*` (CRUD + update) |
-| **SDN IPAM** | 4 | `/cluster/sdn/ipams/*` (CRUD + update) |
-| **SDN DNS** | 4 | `/cluster/sdn/dns/*` (CRUD + update) |
-| **SDN Controllers** | 4 | `/cluster/sdn/controllers/*` (CRUD + update) |
-| **SDN Apply** | 1 | `PUT /cluster/sdn` (apply pending changes) |
-| **Cluster** | 1 | `GET /cluster/resources` (cluster-wide inventory) |
-| **Pools** | 4 | `/pools/*` (CRUD) |
-| **VM Disk Ops** | 2 | `move_disk`, `unlink` |
-| **Guest Agent (ext)** | 6 | `get-osinfo`, `get-fsinfo`, `file-read`, `file-write`, `set-user-password`, `fstrim` |
-| **Container Ops** | 6 | `suspend`, `resume`, `resize`, `template`, `move_volume`, `interfaces` |
-| **Storage Content** | 4 | `status`, `content` DELETE/PUT/POST |
-| **Node Ops** | 6 | `config`, `dns`, `startall`, `stopall` |
-| **Access Groups** | 4 | `/access/groups/*` (CRUD) |
-| **Access Domains** | 4 | `/access/domains/*` (CRUD) |
-| **Access Password** | 1 | `PUT /access/password` |
-
-## Not Yet Implemented
-
-### Gaps
-
-| Area | Key Endpoints | Notes | Priority |
-|------|--------------|-------|----------|
-
-### Lower Priority Gaps
-
-| Area | Key Endpoints | Notes |
-|------|--------------|-------|
-| **Ceph** | `/nodes/{node}/ceph/*` | OSD, monitor, pool, and MDS management |
-| **HA (High Availability)** | `/cluster/ha/*` | HA groups, resources, and fencing configuration |
-| **Replication** | `/cluster/replication/*` | ZFS replication between nodes |
-| **Access Groups** | `/access/groups/*` | User group management |
-| **Access Domains/Realms** | `/access/domains/*` | LDAP, AD, OpenID realm configuration |
-| **PBS Integration** | N/A (separate API) | Proxmox Backup Server operations |
-| **Cluster Config** | `/cluster/config/*` | Cluster join, node management, totem config |
-| **ACME / Certificates** | `/cluster/acme/*`, `/nodes/{node}/certificates/*` | Let's Encrypt certificate automation |
-| **Node Management** | `/nodes/{node}/apt/*`, `/nodes/{node}/disks/*`, `/nodes/{node}/services/*` | Package updates, disk management, service control |
-| **Metrics** | `/cluster/metrics/*` | External metrics server configuration |
-| **Additional VM Agent** | `/nodes/{node}/qemu/{vmid}/agent/*` | File read/write, OS info, suspend/resume via agent |
-
-## Contributing New Cmdlets
-
-See [CONTRIBUTING.md](../CONTRIBUTING.md) for the full guide. In short:
-
-1. Create service methods in `PSProxmoxVE.Core/Services/`
-2. Create model classes in `PSProxmoxVE.Core/Models/` if needed
-3. Create cmdlet classes in `PSProxmoxVE/Cmdlets/`
-4. Add cmdlet names to `CmdletsToExport` in the manifest
-5. Add Pester tests
-6. Update this document and the README cmdlet reference
diff --git a/docs/cmdlets/Send-PveIso.md b/docs/cmdlets/Send-PveIso.md
deleted file mode 100644
index 56b437b..0000000
--- a/docs/cmdlets/Send-PveIso.md
+++ /dev/null
@@ -1,198 +0,0 @@
----
-external help file: PSProxmoxVE.dll-Help.xml
-Module Name: PSProxmoxVE
-online version:
-schema: 2.0.0
----
-
-# Send-PveIso
-
-## SYNOPSIS
-{{ Fill in the Synopsis }}
-
-## SYNTAX
-
-```
-Send-PveIso [-Node] [-Storage] [-Path] [-Checksum ]
- [-ChecksumAlgorithm ] [-Wait] [-Session ] [-ProgressAction ] [-WhatIf]
- [-Confirm] []
-```
-
-## DESCRIPTION
-{{ Fill in the Description }}
-
-## EXAMPLES
-
-### Example 1
-```powershell
-PS C:\> {{ Add example code here }}
-```
-
-{{ Add example description here }}
-
-## PARAMETERS
-
-### -Checksum
-Checksum value to verify the upload.
-
-```yaml
-Type: String
-Parameter Sets: (All)
-Aliases:
-
-Required: False
-Position: Named
-Default value: None
-Accept pipeline input: False
-Accept wildcard characters: False
-```
-
-### -ChecksumAlgorithm
-Checksum algorithm (md5, sha1, sha256, sha512).
-
-```yaml
-Type: String
-Parameter Sets: (All)
-Aliases:
-Accepted values: md5, sha1, sha256, sha512
-
-Required: False
-Position: Named
-Default value: None
-Accept pipeline input: False
-Accept wildcard characters: False
-```
-
-### -Confirm
-Prompts you for confirmation before running the cmdlet.
-
-```yaml
-Type: SwitchParameter
-Parameter Sets: (All)
-Aliases: cf
-
-Required: False
-Position: Named
-Default value: None
-Accept pipeline input: False
-Accept wildcard characters: False
-```
-
-### -Node
-The PVE node name.
-
-```yaml
-Type: String
-Parameter Sets: (All)
-Aliases:
-
-Required: True
-Position: 0
-Default value: None
-Accept pipeline input: False
-Accept wildcard characters: False
-```
-
-### -Path
-Local path to the ISO file to upload.
-
-```yaml
-Type: String
-Parameter Sets: (All)
-Aliases:
-
-Required: True
-Position: 2
-Default value: None
-Accept pipeline input: False
-Accept wildcard characters: False
-```
-
-### -Session
-{{ Fill Session Description }}
-
-```yaml
-Type: PveSession
-Parameter Sets: (All)
-Aliases:
-
-Required: False
-Position: Named
-Default value: None
-Accept pipeline input: False
-Accept wildcard characters: False
-```
-
-### -Storage
-The storage pool name.
-
-```yaml
-Type: String
-Parameter Sets: (All)
-Aliases:
-
-Required: True
-Position: 1
-Default value: None
-Accept pipeline input: False
-Accept wildcard characters: False
-```
-
-### -Wait
-Wait for the task to complete before returning.
-
-```yaml
-Type: SwitchParameter
-Parameter Sets: (All)
-Aliases:
-
-Required: False
-Position: Named
-Default value: None
-Accept pipeline input: False
-Accept wildcard characters: False
-```
-
-### -WhatIf
-Shows what would happen if the cmdlet runs.
-The cmdlet is not run.
-
-```yaml
-Type: SwitchParameter
-Parameter Sets: (All)
-Aliases: wi
-
-Required: False
-Position: Named
-Default value: None
-Accept pipeline input: False
-Accept wildcard characters: False
-```
-
-### -ProgressAction
-{{ Fill ProgressAction Description }}
-
-```yaml
-Type: ActionPreference
-Parameter Sets: (All)
-Aliases: proga
-
-Required: False
-Position: Named
-Default value: None
-Accept pipeline input: False
-Accept wildcard characters: False
-```
-
-### CommonParameters
-This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).
-
-## INPUTS
-
-### None
-## OUTPUTS
-
-### PSProxmoxVE.Core.Models.Vms.PveTask
-## NOTES
-
-## RELATED LINKS
diff --git a/src/PSProxmoxVE.Core/Services/CloudInitService.cs b/src/PSProxmoxVE.Core/Services/CloudInitService.cs
index 8ec113e..2f5ef98 100644
--- a/src/PSProxmoxVE.Core/Services/CloudInitService.cs
+++ b/src/PSProxmoxVE.Core/Services/CloudInitService.cs
@@ -50,7 +50,7 @@ namespace PSProxmoxVE.Core.Services
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
try
{
- var response = client.GetAsync($"nodes/{node}/qemu/{vmid}/config")
+ var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/config")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
if (data == null) return new PveCloudInitConfig();
@@ -95,7 +95,7 @@ namespace PSProxmoxVE.Core.Services
var formData = config.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value?.ToString() ?? string.Empty);
- client.PutAsync($"nodes/{node}/qemu/{vmid}/config", formData)
+ client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/config", formData)
.GetAwaiter().GetResult();
}
finally
@@ -121,7 +121,7 @@ namespace PSProxmoxVE.Core.Services
try
{
// Request a Cloud-Init dump (user-data section) — this causes PVE to rebuild the image
- var dumpResponse = client.GetAsync($"nodes/{node}/qemu/{vmid}/cloudinit/dump?type=user")
+ var dumpResponse = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/cloudinit/dump?type=user")
.GetAwaiter().GetResult();
var data = JObject.Parse(dumpResponse)["data"];
return data?.ToString() ?? string.Empty;
diff --git a/src/PSProxmoxVE.Core/Services/NetworkService.cs b/src/PSProxmoxVE.Core/Services/NetworkService.cs
index 8ffa39b..21e22e4 100644
--- a/src/PSProxmoxVE.Core/Services/NetworkService.cs
+++ b/src/PSProxmoxVE.Core/Services/NetworkService.cs
@@ -44,7 +44,7 @@ namespace PSProxmoxVE.Core.Services
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
- var resource = $"nodes/{node}/network";
+ var resource = $"nodes/{Uri.EscapeDataString(node)}/network";
if (!string.IsNullOrEmpty(type))
resource += $"?type={Uri.EscapeDataString(type!)}";
@@ -83,7 +83,7 @@ namespace PSProxmoxVE.Core.Services
var formData = config.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value?.ToString() ?? string.Empty);
- var response = client.PostAsync($"nodes/{node}/network", formData)
+ var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/network", formData)
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject() ?? new PveNetwork();
@@ -119,7 +119,7 @@ namespace PSProxmoxVE.Core.Services
var formData = config.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value?.ToString() ?? string.Empty);
- client.PutAsync($"nodes/{node}/network/{iface}", formData)
+ client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/network/{Uri.EscapeDataString(iface)}", formData)
.GetAwaiter().GetResult();
}
finally
@@ -144,7 +144,7 @@ namespace PSProxmoxVE.Core.Services
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
try
{
- client.DeleteAsync($"nodes/{node}/network/{iface}").GetAwaiter().GetResult();
+ client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/network/{Uri.EscapeDataString(iface)}").GetAwaiter().GetResult();
}
finally
{
@@ -165,7 +165,7 @@ namespace PSProxmoxVE.Core.Services
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
try
{
- var response = client.PutAsync($"nodes/{node}/network")
+ var response = client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/network")
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
@@ -263,7 +263,7 @@ namespace PSProxmoxVE.Core.Services
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
try
{
- client.DeleteAsync($"cluster/sdn/zones/{zone}").GetAwaiter().GetResult();
+ client.DeleteAsync($"cluster/sdn/zones/{Uri.EscapeDataString(zone)}").GetAwaiter().GetResult();
}
finally
{
@@ -313,7 +313,7 @@ namespace PSProxmoxVE.Core.Services
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
try
{
- client.DeleteAsync($"cluster/sdn/vnets/{vnet}").GetAwaiter().GetResult();
+ client.DeleteAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}").GetAwaiter().GetResult();
}
finally
{
diff --git a/src/PSProxmoxVE.Core/Services/NodeService.cs b/src/PSProxmoxVE.Core/Services/NodeService.cs
index a7d6bc2..7d1e099 100644
--- a/src/PSProxmoxVE.Core/Services/NodeService.cs
+++ b/src/PSProxmoxVE.Core/Services/NodeService.cs
@@ -63,7 +63,7 @@ namespace PSProxmoxVE.Core.Services
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
try
{
- var response = client.GetAsync($"nodes/{node}/status").GetAwaiter().GetResult();
+ var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/status").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject() ?? new PveNodeStatus();
}
@@ -86,7 +86,7 @@ namespace PSProxmoxVE.Core.Services
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
try
{
- var response = client.GetAsync($"nodes/{node}/config").GetAwaiter().GetResult();
+ var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/config").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data as JObject ?? new JObject();
}
@@ -111,7 +111,7 @@ namespace PSProxmoxVE.Core.Services
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
try
{
- client.PutAsync($"nodes/{node}/config", config).GetAwaiter().GetResult();
+ client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/config", config).GetAwaiter().GetResult();
}
finally
{
@@ -132,7 +132,7 @@ namespace PSProxmoxVE.Core.Services
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
try
{
- var response = client.GetAsync($"nodes/{node}/dns").GetAwaiter().GetResult();
+ var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/dns").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data as JObject ?? new JObject();
}
@@ -157,7 +157,7 @@ namespace PSProxmoxVE.Core.Services
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
try
{
- client.PutAsync($"nodes/{node}/dns", config).GetAwaiter().GetResult();
+ client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/dns", config).GetAwaiter().GetResult();
}
finally
{
@@ -180,7 +180,7 @@ namespace PSProxmoxVE.Core.Services
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
try
{
- var response = client.PostAsync($"nodes/{node}/startall", formData).GetAwaiter().GetResult();
+ var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/startall", formData).GetAwaiter().GetResult();
return ParseTask(response, node);
}
finally
@@ -204,7 +204,7 @@ namespace PSProxmoxVE.Core.Services
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
try
{
- var response = client.PostAsync($"nodes/{node}/stopall", formData).GetAwaiter().GetResult();
+ var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/stopall", formData).GetAwaiter().GetResult();
return ParseTask(response, node);
}
finally
diff --git a/src/PSProxmoxVE.Core/Services/TaskService.cs b/src/PSProxmoxVE.Core/Services/TaskService.cs
index e56cffe..512a36b 100644
--- a/src/PSProxmoxVE.Core/Services/TaskService.cs
+++ b/src/PSProxmoxVE.Core/Services/TaskService.cs
@@ -43,7 +43,7 @@ namespace PSProxmoxVE.Core.Services
try
{
var encodedUpid = Uri.EscapeDataString(upid);
- var response = client.GetAsync($"nodes/{node}/tasks/{encodedUpid}/status")
+ var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/tasks/{encodedUpid}/status")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
var task = data?.ToObject() ?? new PveTask { Upid = upid };
@@ -69,7 +69,7 @@ namespace PSProxmoxVE.Core.Services
try
{
var encodedUpid = Uri.EscapeDataString(upid);
- var response = client.GetAsync($"nodes/{node}/tasks/{encodedUpid}/log")
+ var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/tasks/{encodedUpid}/log")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject() ?? Array.Empty();
diff --git a/src/PSProxmoxVE.Core/Services/TemplateService.cs b/src/PSProxmoxVE.Core/Services/TemplateService.cs
index 8bc3141..3fa53a2 100644
--- a/src/PSProxmoxVE.Core/Services/TemplateService.cs
+++ b/src/PSProxmoxVE.Core/Services/TemplateService.cs
@@ -61,7 +61,7 @@ namespace PSProxmoxVE.Core.Services
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
try
{
- var response = client.PostAsync($"nodes/{node}/qemu/{vmid}/template")
+ var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/template")
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
diff --git a/tests/infrastructure/.terraform.lock.hcl b/tests/infrastructure/.terraform.lock.hcl
deleted file mode 100644
index afb264c..0000000
--- a/tests/infrastructure/.terraform.lock.hcl
+++ /dev/null
@@ -1,63 +0,0 @@
-# This file is maintained automatically by "terraform init".
-# Manual edits may be lost in future updates.
-
-provider "registry.terraform.io/bpg/proxmox" {
- version = "0.98.1"
- constraints = ">= 0.70.0"
- hashes = [
- "h1:oegYetLP9VqFSnRfHeUQnm6v58dH/VT4ivJSXg4RLnI=",
- "zh:165705d0a0db92ccc5b1d23b0033cc87011f1151f4f178ad9df61473c3ca30b5",
- "zh:3ecab62be80d89cf8f019ab23809254f854eb5a443682a1a22a52e0d80b1273f",
- "zh:48b520a3f5b8cccca66e0dc7b1e264e01d2f35ef8a9bbfc6b02964cd7235914b",
- "zh:7fa7e166b924b5192ff963f47a56772c7b34f0004d825d830c3122eab970534f",
- "zh:804bdf5353d3a3ad9d06e6faada1b8600af8ecfd1e5b23280a81e62b0cb7037d",
- "zh:87ebe8b56a895870ebc0d353fba5ebbb4615401dc3f53a2e157f158cb9025d2d",
- "zh:9b0c8e02c036152bcb2f54c354da4647d10d016396e5ca2b70084a8bd5546970",
- "zh:a0039465282dc75ac9c53750b61ba4038d56bed545cb39df669b6216bdff82da",
- "zh:bbc2c187c8283b6a5f44cbd65f4b6e5d534a3be1eeb1a7c8caaaa6f1a7e48e76",
- "zh:d04aed773582e95c8d7610833e85b204fe8c01c09c94e7128914c8e22b9218c7",
- "zh:d7a1433a7d6f1b158f499a93d3fb1825eec73eac4c61b00a39f2f5e95ae0965a",
- "zh:df46cb59fa1ddda335934720ad22d8205bb8902ce780ca32205f1622f1fa1649",
- "zh:e100ad439313db667a5c4ca53ae4c54d8b1c72299a2240a1493823cb89cb0781",
- "zh:f26e0763dbe6a6b2195c94b44696f2110f7f55433dc142839be16b9697fa5597",
- ]
-}
-
-provider "registry.terraform.io/hashicorp/local" {
- version = "2.7.0"
- constraints = ">= 2.0.0"
- hashes = [
- "h1:sSwlfp2etjCaE9hIF7bJBDjRIhDCVFglEOVyiCI7vgs=",
- "zh:261fec71bca13e0a7812dc0d8ae9af2b4326b24d9b2e9beab3d2400fab5c5f9a",
- "zh:308da3b5376a9ede815042deec5af1050ec96a5a5410a2206ae847d82070a23e",
- "zh:3d056924c420464dc8aba10e1915956b2e5c4d55b11ffff79aa8be563fbfe298",
- "zh:643256547b155459c45e0a3e8aab0570db59923c68daf2086be63c444c8c445b",
- "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
- "zh:7aa4d0b853f84205e8cf79f30c9b2c562afbfa63592f7231b6637e5d7a6b5b27",
- "zh:7dc251bbc487d58a6ab7f5b07ec9edc630edb45d89b761dba28e0e2ba6b1c11f",
- "zh:7ee0ca546cd065030039168d780a15cbbf1765a4c70cd56d394734ab112c93da",
- "zh:b1d5d80abb1906e6c6b3685a52a0192b4ca6525fe090881c64ec6f67794b1300",
- "zh:d81ea9856d61db3148a4fc6c375bf387a721d78fc1fea7a8823a027272a47a78",
- "zh:df0a1f0afc947b8bfc88617c1ad07a689ce3bd1a29fd97318392e6bdd32b230b",
- "zh:dfbcad800240e0c68c43e0866f2a751cff09777375ec701918881acf67a268da",
- ]
-}
-
-provider "registry.terraform.io/hashicorp/null" {
- version = "3.2.4"
- hashes = [
- "h1:L5V05xwp/Gto1leRryuesxjMfgZwjb7oool4WS1UEFQ=",
- "zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2",
- "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
- "zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43",
- "zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a",
- "zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991",
- "zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f",
- "zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e",
- "zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615",
- "zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442",
- "zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5",
- "zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f",
- "zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f",
- ]
-}