fix(security): add Uri.EscapeDataString to all URL path parameters

20 instances across 5 service files were missing URL encoding on
dynamic path segments (node, iface, zone, vnet). Fixes D003 regression.

- NodeService: 7 methods
- NetworkService: 7 methods (5 node-network + 2 SDN remove)
- CloudInitService: 3 methods
- TaskService: 2 methods (GetTask, GetTaskLog)
- TemplateService: 1 method (CreateTemplate)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-23 17:40:36 -05:00
parent 625ea0fa28
commit 053fd4c06a
11 changed files with 20 additions and 1437 deletions
-878
View File
@@ -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 (CQ1CQ4) now use `TaskService.WaitForTask`. Guest exec timeout added (CQ5). OutputType on all cmdlets (CQ9). Firewall VmId nullable (CQ10). Bare catches replaced with filtered exceptions (CQ11CQ14). 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 (S1S3). 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**: `<Nullable>enable</Nullable>` 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 |
|---|---|---|
| CQ1CQ4 | 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 `<PVE_HOST>`, `<USER>@<REALM>!<TOKENID>=<TOKEN_UUID>` |
| 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 CQ1CQ4 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%).
-3
View File
@@ -1,3 +0,0 @@
*.flow
_analyze.py
run_analyze.py
-192
View File
@@ -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://<PVE_HOST>:8006/api2/json/nodes/pve/storage/local/upload'
$AuthHeader = 'PVEAPIToken=<USER>@<REALM>!<TOKENID>=<TOKEN_UUID>'
$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 '<PVE_HOST>' -Port 8006 `
-ApiToken '<USER>@<REALM>!<TOKENID>=<TOKEN_UUID>' `
-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
-83
View File
@@ -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
-198
View File
@@ -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] <String> [-Storage] <String> [-Path] <String> [-Checksum <String>]
[-ChecksumAlgorithm <String>] [-Wait] [-Session <PveSession>] [-ProgressAction <ActionPreference>] [-WhatIf]
[-Confirm] [<CommonParameters>]
```
## 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
@@ -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;
@@ -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<PveNetwork>() ?? 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
{
+7 -7
View File
@@ -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<PveNodeStatus>() ?? 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
+2 -2
View File
@@ -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<PveTask>() ?? 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<PveTaskLog[]>() ?? Array.Empty<PveTaskLog>();
@@ -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);
}
-63
View File
@@ -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",
]
}