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