Commit Graph

88 Commits

Author SHA1 Message Date
Clint Branham 3fd770d84c fix: percent-encode ';' in form values
PveHttpClient.EncodeFormValue encoded &, =, +, space, and % but left ';'
literal. PVE's application/x-www-form-urlencoded parser treats a raw ';'
as a field separator (the historical alternative to '&'), so a value like

    boot=order=scsi0;ide2

was split into 'boot=order=scsi0' plus an empty 'ide2' field, and PVE
rejected the PUT with "ide2: unable to parse drive options". This broke
any multi-device boot order set via Set-PveVmConfig -AdditionalConfig,
and any other value containing ';'.

Encode ';' as %3B. Safe under the existing minimal-encoding policy that
keeps ':' and '!' literal for cluster-join: cluster-join payloads never
contain ';', and PVE url-decodes config form values.

Tracked as F088. Closes #64.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:04:31 -05:00
Clint Branham 79c97ec211 docs: cut CHANGELOG into versioned entries, refresh ReleaseNotes
Doc hygiene catch-up surfaced by the PR #62 review:

- CHANGELOG.md: the [Unreleased] section had accumulated all post-preview
  work without ever being cut into release entries. Promote it into:
    - [0.1.3] - new fixes from #58 (DiskSize normalization) and #59
      (HttpClient -TimeoutSeconds + RequestTimeout surfacing)
    - [0.1.2] - #43/#44/#45 fixes from PR #46
    - [0.1.1] - the cmdlet expansion + OpenAPI validation that
      actually shipped to PSGallery as 0.1.1
  Reset [Unreleased] to empty.

- src/PSProxmoxVE/PSProxmoxVE.psd1: replace the stale "Initial preview
  release" ReleaseNotes (carried over since 0.1.0-preview) with actual
  0.1.3 notes. PSGallery shows this on the version page.

- CLAUDE.md: document the release process so future bumps update the
  psd1 version, psd1 ReleaseNotes, and CHANGELOG together before the
  tag is cut. Prevents this hygiene gap from recurring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:23:29 -05:00
Clint Branham ff728fc6d6 chore: bump version to 0.1.3
Releases #58 (LVM disk-size unit normalization) and #59 (HttpClient
timeout / -TimeoutSeconds) fixes to PSGallery.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:14:54 -05:00
Clint Branham cb97a86c9f fix: drop net48-incompatible TimeoutException filter in timeout catch
The when filter (ex.InnerException is TimeoutException) only matches on
.NET 5+. On .NET Framework 4.8 — which CI exercises via the test project's
net48 target — HttpClient.Timeout throws a bare TaskCanceledException
with no inner exception, so CI failed:

  Expected: typeof(PSProxmoxVE.Core.Exceptions.PveApiException)
  Actual:   typeof(System.Threading.Tasks.TaskCanceledException)
  ---- System.Threading.Tasks.TaskCanceledException : A task was canceled.

PveHttpClient.SendAsync never passes a CancellationToken to the inner
HttpClient.SendAsync, so the only way a TaskCanceledException can reach
this catch is HttpClient.Timeout firing — true on net48, .NET Core, and
.NET 5+. Drop the filter and wrap unconditionally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:22:47 -05:00
Clint Branham a1d9550d83 fix: surface HttpClient timeouts as PveApiException(RequestTimeout)
When HttpClient.Timeout elapses, .NET throws TaskCanceledException — not
HttpRequestException — so the existing catch in PveHttpClient.SendAsync
missed it and callers got a raw stack trace. With -TimeoutSeconds now
configurable and documented, this gap became user-visible.

In .NET 5+ HttpClient surfaces transport timeouts as TaskCanceledException
with a TimeoutException inner; user-driven token cancellation does not.
Catch by that inner-type signature and rethrow as PveApiException with
HttpStatusCode.RequestTimeout, the resource path, and a message that
reports the configured timeout.

Adds SendAsync_TimeoutFires_ThrowsPveApiExceptionWithRequestTimeout which
swaps in a delaying HttpMessageHandler with a 50ms timeout to exercise
the path deterministically. Drops the redundant
DefaultSessionTimeoutIs100Seconds test (covered by
PveSessionTests.Timeout_DefaultIs100Seconds and the existing flow-through
test).

Addresses PR #61 review feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:17:31 -05:00
Clint Branham ea2bcdc336 Merge main into fix/http-client-timeout
Resolves findings.json conflict — F086 (from #60, merged into main) and
F087 (this branch) both append to the trailing findings array. Kept both
entries, in numeric order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:17:20 -05:00
Clint Branham f3b06171b2 fix: add -TimeoutSeconds for long-running HTTP calls
PveHttpClient was constructed without setting HttpClient.Timeout, so
.NET's 100s default applied to every request. Multi-GB ISO uploads via
Send-PveFile on a real LAN reliably tripped this with TaskCanceledException
after 100 seconds, and there was no way to override it.

- PveSession gains a Timeout (TimeSpan) property, defaulting to 100s.
- PveHttpClient accepts an optional per-instance timeout override that
  takes precedence over the session timeout.
- Connect-PveServer exposes -TimeoutSeconds to set the session default.
- Send-PveFile and Invoke-PveStorageDownload expose -TimeoutSeconds with
  a 30-minute implicit default so large uploads/downloads do not trip
  the 100s default. -TimeoutSeconds 0 means Timeout.InfiniteTimeSpan.

Tracked as F087. Closes #59.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:08:26 -05:00
Clint Branham fa361a3691 fix: address PR #60 review feedback
- SizeParser: wrap TB-suffix overflow in try/catch so callers get
  ArgumentException with the parameter name rather than OverflowException.
- New-PveVm/New-PveContainer: validate -DiskSize/-RootFsSize before
  ShouldProcess so typos like "512M" are rejected even with -WhatIf and
  even when the matching -DiskStorage/-RootFsStorage is omitted.
- Add Pester tests for the new DiskSize and RootFsSize validation paths,
  including a new New-PveContainer.Tests.ps1.
- Add SizeParserTests coverage for the TB overflow path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:06:13 -05:00
Clint Branham 6181c8ce77 fix: normalize -DiskSize and -RootFsSize before sending to PVE
Disk and rootfs size strings were interpolated directly into the disk
spec as "<storage>:<size>", so "60G" produced "local-lvm:60G". On
LVM/LVM-thin storages PVE parses the value after the colon as a volume
name unless it is a bare integer, returning "unable to parse lvm volume
name '60G'". File-backed storages mask this by accepting either form.

SizeParser.NormalizeToGibibytes() now strips G/GB/T/TB suffixes and
returns a bare GiB integer string, so the documented "32G" call shape
works on every storage type. Sub-GB units are rejected with a clear
error rather than being silently truncated.

Tracked as F086. Closes #58.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:59:21 -05:00
dependabot[bot] 9a9830f2e1 Bump SharpCompress from 0.47.4 to 0.48.1
---
updated-dependencies:
- dependency-name: SharpCompress
  dependency-version: 0.48.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 15:08:29 +00:00
dependabot[bot] 97ff7701da Bump SharpCompress from 0.47.3 to 0.47.4
---
updated-dependencies:
- dependency-name: SharpCompress
  dependency-version: 0.47.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 14:14:39 +00:00
Clint Branham d3953c8c96 chore: bump version to 0.1.2
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:00:02 -05:00
Clint Branham 5dcbde45a6 fix: resolve issues #43, #44, #45
#44 Get-PveApiToken FullTokenId empty:
  - Make FullTokenId a computed property (UserId + "!" + TokenId)
  - RawFullTokenId captures the API's "full-tokenid" for creation responses

#43 Set-PvePermission token ACLs:
  - Add "token" to Type ValidateSet
  - Auto-detect tokens from "!" in UgId (user@realm!tokenid format)
  - Add tokens parameter to UserService.SetPermission

#45 Connect-PveServer return session by default:
  - Always output session (matches Connect-AzAccount pattern)
  - Add -Quiet switch to suppress output
  - Keep -PassThru as hidden deprecated param for backwards compat

Closes #43, closes #44, closes #45

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:22:37 -05:00
Clint Branham 6e953c1cd7 chore: bump version to 0.1.1 (stable release)
Remove prerelease tag — this is now a stable release.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:49:04 -05:00
Clint Branham d81c850b0b fix: validate API enum values against PVE OpenAPI spec
Add 70 xUnit tests that validate every ValidateSet in the module
against the PVE OpenAPI spec. Three bugs found and fixed:

- Storage: remove `glusterfs` (dropped in PVE 9), add `btrfs`, `esxi`
- Backup compression: `none` → `0` (PVE uses "0" not "none")
- Cluster resources: remove `lxc` filter (PVE uses `vm` for both)

The pve-api-enums.json fixture (199KB) is extracted from the full
OpenAPI spec and contains parameter enum values for 302 API paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:34:56 -05:00
copilot-swe-agent[bot] b1f93e655a fix: set task.Node from UPID + expand SetPveHaRule Type + extract GetNodeFromUpid helper
Co-authored-by: GoodOlClint <151449+GoodOlClint@users.noreply.github.com>
Agent-Logs-Url: https://github.com/GoodOlClint/PSProxmoxVE/sessions/42467843-de5e-4603-99e3-fb182eaf4251
2026-03-26 00:26:00 +00:00
Clint Branham 514ce3fc1b fix: Set-PveHaRule requires mandatory Type parameter
PVE 9 API requires 'type' on PUT /cluster/ha/rules/{rule} even for
updates. Added mandatory Type parameter with ValidateSet for
node-affinity and resource-affinity.

Updated integration test and Pester unit tests to pass -Type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:52:23 -05:00
Clint Branham 598e9edef2 fix: replace FormUrlEncodedContent with minimal form encoding
.NET's FormUrlEncodedContent over-encodes characters like : and !
(%3A, %21) in form values. PVE's internal API consumers (specifically
the cluster join process) do not URL-decode these values before using
them, causing fingerprint comparison failures and password mismatches.

Replace with BuildFormContent/EncodeFormValue that only encodes
characters that break form parsing (&, =, +, space, %). This matches
curl's -d behavior and fixes cluster join "Cluster join aborted!" errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:40:00 -05:00
Clint Branham 4277ad73b9 fix: retry re-auth during cluster join and always reconnect to node A
- Extract re-auth retry into ReauthenticateWithRetry helper (fixes
  cognitive complexity warning)
- Retry auth up to 10x with 3s delay — node B's auth services need
  time to restart after joining the cluster
- Wrap join in try/finally so the test always reconnects to node A,
  even if the join fails

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:40:00 -05:00
Clint Branham 8c131a0609 fix: re-authenticate during cluster join -Wait on 401
Cluster join restarts auth services on the joining node, which
invalidates the PVE ticket mid-poll. When WaitForTask gets a 401,
catch it and re-authenticate with the password already available
in the cmdlet, then retry the wait.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:40:00 -05:00
Clint Branham c7627ee4e1 test: add xUnit, Pester, and integration tests for cluster config + HA
xUnit (47 new tests, 429 total):
- ClusterConfigServiceTests: 25 tests covering all 14 service methods
  including URL encoding, null guards, and error responses
- HaServiceTests: 22 tests covering resources, groups, status, and rules
  with URI encoding verification (vm:100 → vm%3A100)

Pester (187 new tests, 1525 total):
- ClusterConfigCmdlets.Tests.ps1: 11 cmdlets tested
- HaCmdlets.Tests.ps1: 14 cmdlets tested

Integration (ClusterConfig.Integration.Tests.ps1):
- Full 2-node cluster lifecycle with -Wait for task completion
- Uses root@pam ticket auth for cluster create/join operations
- HA group tests skip on PVE 9.0+ (groups migrated to rules)
- JArray indexing uses .Item() for PowerShell compatibility

Cmdlet improvements:
- New-PveCluster, Add-PveClusterConfigNode, Add-PveClusterMember now
  support -Wait switch to block until task completes (via TaskService)
- GetClusterConfig returns JToken to handle array responses on standalone
- OutputType updated to PveTask for task-returning cmdlets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:40:00 -05:00
Clint Branham 2e126b96a3 fix: CloudInit regenerate calls PUT endpoint instead of GET dump
RegenerateCloudInitImage was calling GET /cloudinit/dump?type=user
which returns the cloud-init YAML content, not a task UPID. The
cmdlet then passed this YAML string to WaitForTask, causing a 501
error trying to poll a URI like "GET nodes/.../tasks/%23cloud-config..."

Fixed to call PUT /nodes/{node}/qemu/{vmid}/cloudinit which is the
correct regeneration endpoint that returns a UPID.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:06:28 -05:00
copilot-swe-agent[bot] 4a63055c10 chore: remove pre-correction D013-compliance.md and fix JsonHelper XML doc comment nullability
Co-authored-by: GoodOlClint <151449+GoodOlClint@users.noreply.github.com>
Agent-Logs-Url: https://github.com/GoodOlClint/PSProxmoxVE/sessions/2feecd30-2ba1-40bc-b1b5-c1fcc485a319
2026-03-25 16:42:29 +00:00
Clint Branham 10c277af10 fix: resolve F085 — PveClusterJoinInfo Nodelist/Totem D013 violations
D013 compliance scan found 2 remaining Newtonsoft type exposures:
- PveClusterJoinInfo.Nodelist: JArray → List<Dictionary<string, object?>>
- PveClusterJoinInfo.Totem: JObject → Dictionary<string, object?>

Both now use NativeListConverter/NativeDictionaryConverter for
deserialization. Full D013 compliance report added.

Updated findings.json: F085 status open with scan evidence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:31:05 -05:00
Clint Branham 3ce19a721c feat: emit native types instead of Newtonsoft JObject/JArray in public APIs
Replace all Newtonsoft.Json.Linq types (JObject, JArray, JToken) exposed
in public service methods, model properties, and cmdlet OutputTypes with
native .NET types (Dictionary<string, object?>, List<Dictionary<string,
object?>>, PSObject).

New utilities:
- JsonHelper: ToNative, ToDictionary, ToListOfDictionaries for recursive
  JToken→native conversion
- NativeListConverter / NativeDictionaryConverter: JsonConverters for
  model properties that deserialize JArray/JObject to native types

Services updated (8 methods):
- NodeService: GetNodeConfig, GetNodeDns → Dictionary<string, object?>
- BackupService: GetNotBackedUp → List<Dictionary<string, object?>>
- ClusterConfigService: GetClusterConfig, GetTotem, GetQdevice → Dictionary
- HaService: GetManagerStatus → Dictionary<string, object?>
- VmService: GetGuestExecStatus → Dictionary<string, object?>

Models updated (2 properties):
- PvePool.Members: JArray → List<Dictionary<string, object?>>
- PveHaRule.Properties: JObject → Dictionary<string, object?>

Cmdlets updated (5):
- GetPveClusterConfigCmdlet: OutputType JToken → Dictionary<string, object>
- GetPveNodeConfigCmdlet: iteration updated for Dictionary
- GetPveNodeDnsCmdlet: iteration updated for Dictionary
- GetPveBackupInfoCmdlet: iteration updated for List<Dictionary>
- InvokePveVmGuestExecCmdlet: polling updated for Dictionary

Added D013 to DECISIONS.md: cmdlets must emit only native or module-defined types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:12:57 -05:00
Clint Branham 51b51354ef fix: address second round of Copilot review feedback
- Replace null-forgiving operator on Marshal.PtrToStringUni with
  null-coalescing fallback (AddPveClusterMemberCmdlet.cs:64)
- GetNextId now throws InvalidOperationException instead of silently
  returning 0 when API response cannot be parsed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:47:43 -05:00
GoodOlClint e54f9fb749 Merge branch 'main' into feat/cluster-config-ha 2026-03-24 18:41:20 -05:00
Clint Branham f1a3676723 fix: address Copilot review feedback on PR #27
- Extract ParseLinks helper to PveCmdletBase for shared link parsing
  with WriteWarning on malformed entries (was duplicated in 3 cmdlets)
- Fix GetClusterConfig to return data payload, not full API envelope
- Fix OutputType on GetPveClusterConfigCmdlet to JObject
- Fix link doc comments to use correct key format (link0..link7)
- Add null-safe Properties hashtable conversion in HA rule cmdlets
- Use case-insensitive Mode comparison in MovePveHaResourceCmdlet
- Remove unused using directives

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:40:10 -05:00
copilot-swe-agent[bot] f400336803 fix: narrow exception catches in PingGuestAgent and ImportPveOvaCmdlet
Co-authored-by: GoodOlClint <151449+GoodOlClint@users.noreply.github.com>
Agent-Logs-Url: https://github.com/GoodOlClint/PSProxmoxVE/sessions/e7930807-5536-4c55-96f8-0712aee44e00
2026-03-24 23:35:27 +00:00
Clint Branham da2037d0c5 feat: add cluster config and HA management cmdlets (F060, F053)
Cluster Config (F060) — 11 new cmdlets:
- Get-PveClusterStatus, Get-PveClusterNextId
- Get/Set-PveClusterOption (datacenter settings)
- Get-PveClusterConfig, Get-PveClusterConfigNode
- Add/Remove-PveClusterConfigNode (cluster membership)
- Get-PveClusterJoinInfo, Add-PveClusterMember (join workflow)
- New-PveCluster (create cluster)

HA Management (F053) — 14 new cmdlets:
- Get/New/Set/Remove-PveHaResource (HA managed VMs/CTs)
- Move-PveHaResource (migrate/relocate via HA manager)
- Get/New/Set/Remove-PveHaGroup (node groups + priorities)
- Get-PveHaStatus (HA manager status)
- Get/New/Set/Remove-PveHaRule (PVE 9.0+ version-gated)

All cmdlets follow established conventions:
- sealed classes with [OutputType]
- ConfirmImpact.High on destructive operations (D006)
- SecureString for password parameter in Add-PveClusterMember (D002)
- Uri.EscapeDataString on all path segments (D003)
- Verb class constants (D011)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:28:11 -05:00
Clint Branham c0a37d5ef3 fix: resolve F039 bare catches and F084 session secret exposure
F039 (regressed): Replace bare catch blocks with filtered catches that
exclude OutOfMemoryException and StackOverflowException, per D004.
- VmService.PingGuestAgent: catch now filters fatal exceptions
- ImportPveOvaCmdlet: catch now filters fatal exceptions + WriteVerbose

F084 (new): Add PveSession format view to PSProxmoxVE.format.ps1xml
that shows only Hostname, Port, AuthMode, IsExpired, and ServerVersion
in default table output. Sensitive properties (Ticket, ApiToken,
CsrfToken) are hidden from default display but remain accessible
via Select-Object * or direct property access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:18:56 -05:00
copilot-swe-agent[bot] f76ff513e9 fix(storage): implement Portal defaulting for iSCSI, fix Pool/CephPool selection, add NFS format validation
Co-authored-by: GoodOlClint <151449+GoodOlClint@users.noreply.github.com>
Agent-Logs-Url: https://github.com/GoodOlClint/PSProxmoxVE/sessions/d84722e4-409e-4ced-bfbc-f323715b8bdc
2026-03-24 20:36:46 +00:00
Clint Branham 064cc372cf feat(storage): add -Target and -Portal parameters to New-PveStorage for iSCSI
New-PveStorage now supports configuring iSCSI storage backends natively:
  -Target: iSCSI target IQN (e.g. iqn.2024-01.com.example:storage)
  -Portal: iSCSI portal address (host:port, defaults to server:3260)

Also refactored ProcessRecord to use AddIfNotEmpty helper, reducing
cognitive complexity.

Added unit tests for all iSCSI/NFS parameter metadata and a new
SharedStorage.Tests.ps1 integration test file that tests NFS and iSCSI
storage create/verify/status/delete lifecycle against the Docker-based
storage containers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 14:43:33 -05:00
Clint Branham f6a2f843db docs: update cmdlet help via platyPS merge, fix generate-help.ps1
- generate-help.ps1: rewritten to use Update-MarkdownHelp (merge) by
  default, preserving hand-written descriptions and examples. Only
  New-MarkdownHelp on genuinely new cmdlets. -Force flag for full regen.
- docs/cmdlets: parameter metadata updated via platyPS merge, CRLF
  normalized to LF
- Removed stale Send-PveIso.md (cmdlet renamed to Send-PveFile)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 17:41:04 -05:00
Clint Branham 053fd4c06a 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>
2026-03-23 17:40:36 -05:00
Clint Branham d19c6f0c4b fix: upgrade SharpCompress from 0.38.0 to 0.47.3 (#12)
SharpCompress 0.47.3 renamed ReaderFactory.Open() to OpenReader() and
now requires an explicit ReaderOptions parameter. The synchronous API
is still available — no async conversion needed.

Closes #12

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:54:28 -05:00
Clint Branham 5c3dac9e27 refactor: seal all cmdlet classes, add missing OutputType and ConfirmImpact
Sealed 105 cmdlet classes that were not already sealed — cmdlets are leaf
classes and sealing enables compiler optimizations.

Added [OutputType(typeof(void))] to 53 cmdlets that were missing the
attribute. All 169 cmdlet files now declare OutputType.

Added ConfirmImpact.High to Suspend-PveVm and Restart-PveVm to match
the convention used by Stop-PveVm and Reset-PveVm.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 08:15:38 -05:00
Clint Branham 0532327d46 refactor: remove redundant System.Text.Json attributes from models
The codebase uses Newtonsoft.Json (JObject.Parse + ToObject<T>) exclusively.
Removed all [JsonPropertyName] attributes and [System.Text.Json.Serialization.
JsonIgnore] attributes from 41 model files, along with the now-unused
using System.Text.Json.Serialization imports. [JsonProperty] (Newtonsoft)
attributes remain as the sole serialization annotations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 08:15:23 -05:00
Clint Branham 5895e9abd2 fix(firewall): use nullable int for VmId parameter
Changed VmId from int to int? across all 18 firewall cmdlets. The parameter
is conditionally required (only when Level is Vm or Container) so using a
non-nullable int with 0 as sentinel was fragile. Now checks !VmId.HasValue
instead of VmId == 0, and passes the nullable directly to service methods.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 08:15:13 -05:00
Clint Branham 77849062ab fix: add Uri.EscapeDataString to service URL path interpolations
VmService, ContainerService, StorageService, and SnapshotService now wrap
user-controlled string parameters (node names, storage names) with
Uri.EscapeDataString() in URL path construction, preventing path traversal
or encoding issues. Integer parameters (vmid) are left unwrapped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 08:15:02 -05:00
Clint Branham ec8f3a86e2 fix: use specific exception types in catch blocks
Replaced 5 bare catch blocks with specific exception types:
- PveHttpClient: catch JsonException for JSON parse fallback
- PveCmdletBase: catch Exception with filter excluding OOM/SOE for polling
- VmService/ContainerService: catch PveApiException|HttpRequestException
  for offline node handling
- GetPveVmCmdlet: catch PveApiException for VM enrichment failures

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 08:14:51 -05:00
Clint Branham 379e6d2ada fix: use SecureString for guest password, validate Pool/CephPool conflict
Set-PveVmGuestPassword now accepts SecureString instead of plain string for
the -Password parameter, matching the established pattern in Set-PvePassword.
The password is converted via Marshal and zeroed from memory after use.

New-PveStorage now validates that -Pool and -CephPool cannot both be specified,
since both mapped to the same API key and CephPool silently overwrote Pool.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 08:14:39 -05:00
Clint Branham 001c93c45c fix: replace infinite WaitForTask loops with TaskService, add timeout to guest exec
Four cmdlets (Invoke-PveNetworkApply, New/Restore/Remove-PveSnapshot) had
private WaitForTask methods with bare while(true) loops and no timeout or
failure detection. Replaced with TaskService.WaitForTask which has a 10-minute
default timeout, exit status validation, and throws PveTaskTimeoutException
or PveTaskFailedException on failure.

Invoke-PveVmGuestExec had a similar unbounded do/while polling loop. Added a
-Timeout parameter (default 300s, range 1-3600) with Stopwatch enforcement.
Also fixed bare catch in DecodeBase64 to catch FormatException specifically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 08:14:28 -05:00
Clint Branham d3615c7df6 feat: add parameter-level version warnings for named params from newer PVE
Warn (not block) when users pass named parameters that were added in
later PVE versions:
- Send-PveFile: -Checksum/-ChecksumAlgorithm require PVE 7.1
- New/Set-PveSdnSubnet: -DhcpRange requires PVE 8.1

These are warnings, not hard blocks — the parameter is still sent to
the API, which will either accept it or return its own error. This
only applies to named cmdlet parameters, not AdditionalConfig
pass-through hashtables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:21:22 -05:00
Clint Branham 93e005f1a5 feat: add version gating from PVE API history, document version policy
Add RequireVersion calls based on pve_api endpoint-history.json data:
- Cloud-Init management: introduced PVE 7.2
- Storage URL download: introduced PVE 7.0
- Backup compliance info: introduced PVE 7.0
- Container interfaces: introduced PVE 8.1
- Pool update/delete: introduced PVE 8.1

Update README with version policy:
- PVE 9.x: primary target
- PVE 8.x: supported
- PVE 7.x (7.0+): best-effort with clear version errors
- PVE 6.x and older: hard blocked
- Document two-tier gating (introduced vs default) behavior

Parameter-level version changes are not gated — the API returns
clear errors for unknown parameters, which is acceptable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:15:25 -05:00
Clint Branham df4cae01bc feat: close API coverage gaps — 51 new cmdlets across 6 areas
Phase 1 — Missing Update/Set operations (10 cmdlets):
- SDN: Set-PveSdnZone/Vnet/Subnet/Controller/Ipam/Dns + Invoke-PveSdnApply
- Set-PveRole, Set-PveStorage, Set-PveApiToken

Phase 2 — Cluster operations (8 cmdlets):
- Get-PveClusterResource (cluster-wide inventory)
- Get-PveTaskList, Stop-PveTask
- Get/New/Set/Remove-PvePool
- Get-PveBackupInfo (unprotected VMs)

Phase 3 — VM & container gaps (14 cmdlets):
- Move-PveVmDisk, Remove-PveVmDisk
- Guest agent: Get-PveVmGuestOsInfo/FsInfo, Read/Write-PveVmGuestFile,
  Set-PveVmGuestPassword, Invoke-PveVmGuestFsTrim
- Suspend/Resume-PveContainer, Resize-PveContainerDisk,
  New-PveContainerTemplate, Move-PveContainerVolume,
  Get-PveContainerInterface

Phase 4 — Storage content management (4 cmdlets):
- Get-PveStorageStatus, Remove/Set-PveStorageContent, New-PveStorageDisk

Phase 5 — Node operations (6 cmdlets):
- Get/Set-PveNodeConfig, Get/Set-PveNodeDns, Start/Stop-PveNodeVms

Phase 6 — Access management (9 cmdlets):
- Get/New/Set/Remove-PveGroup, Get/New/Set/Remove-PveDomain, Set-PvePassword

Total: 169 cmdlets (was 118). Includes models, services, format views,
Pester unit tests, manifest updates, README, CHANGELOG, API coverage docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 09:55:19 -05:00
Clint Branham 4b503b08db fix: backup job creation requires vmid or -All, fix Enabled default
- Integration test: add -All to New-PveBackupJob call — PVE API
  requires either vmid or all when creating a backup job
- Fix New-PveBackupJob Enabled logic: jobs are enabled by default
  unless user explicitly passes -Enabled:$false (was broken because
  SwitchParameter default=true doesn't set IsPresent)
- Fix XML doc cref warning in NetworkService after RequireSdn removal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 09:07:39 -05:00
Clint Branham 27ecca6251 feat: add two-tier version gating (introduced vs default) for cmdlets
Replace the service-layer RequireSdn hard block with a cmdlet-level
RequireVersion helper in PveCmdletBase that supports two tiers:

- Introduced version: hard fail — the API endpoint doesn't exist
- Default version: warning only — feature exists but may not be
  enabled; users who manually enabled it can proceed

Applied to:
- SDN Zone/VNet/Subnet: introduced 6.2, default 8.0
- SDN IPAM/DNS/Controller: introduced 6.2, default 8.1
- Import-PveVmDisk, Import-PveOva: introduced 8.1

Also fixes integration tests:
- Remove duplicate OVA Import context
- Use systemd calendar format for backup schedule (PVE 9)
- Add SDN IPAM/DNS/Controller read-only integration tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 08:40:23 -05:00