mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-09-04 03:05:32 +00:00
283dc4765888b2b41f05bd5c45754b2590b4243a
470 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
283dc47658 |
refactor: wire Group level onto the firewall rule cmdlets (#126) (#202)
FirewallService.GetGroupRules/CreateGroupRule/UpdateGroupRule/RemoveGroupRule already existed with zero callers. Get/New/Set/Remove-PveFirewallRule gain a Group value in their -Level ValidateSet and a -Group parameter, required and validated the same way Node/VmId are validated for the other levels, and dispatch to those service methods instead of the generic BuildBasePath-driven ones. No cmdlet in this area built its own client, so unlike the other #126 areas there is no inline request to strip. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
87fa7a7a9c |
refactor: route the network and SDN cmdlets through NetworkService (#126) (#201)
Wires the 12 direct-PveHttpClient Network/SDN cmdlets (Get/New/Set/Remove-PveNetwork, Invoke-PveNetworkApply, Get/New-PveSdnZone, Get/New-PveSdnVnet, Get/New-PveSdnSubnet, Remove-PveSdnSubnet) to the existing NetworkService methods, which previously had zero callers for the New/Set/Get paths. NetworkService.ParseTask now stamps Status = "running" on the UPID-string branch, matching SnapshotService's ParseTask from PR #196 (the pattern PR). Offline tests for every wired service method are added to NetworkServiceTests.cs. Fold-in from issue #126's own comment: ValidatePattern identifier validation, already present on Remove-PveSdnZone/Vnet and Remove-PveStorage from PR #161, is added to the Zone/Vnet identifier parameters on the New-PveSdnZone/Vnet/Subnet and Remove-PveSdnSubnet cmdlets converted here. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
3f7f69b250 |
refactor: route the CloudInit/Templates cmdlets through their service seams (#126) (#200)
Get-PveCloudInitConfig and Get-PveTemplate each built their own PveHttpClient inline. They now go through CloudInitService and TemplateService, which is offline-testable per ADR 0021. Get-PveCloudInitConfig's shipped output is the full PveVmConfig (per its own synopsis), not the CI-key-filtered PveCloudInitConfig that CloudInitService.GetCloudInitConfig already returns for a different purpose. CloudInitService gains GetFullVmConfig, a thin delegation to the existing VmService.GetVmConfig (already used by Get-PveVmConfig) rather than a parallel HTTP implementation. Get-PveTemplate's TemplateService.GetTemplates now forwards VmService.GetVms's onNodeSkipped callback, matching Get-PveVm/Get-PveContainer, so an unreachable node during the all-nodes listing produces a warning instead of a silently short result. Two review-found regressions were fixed before commit: an empty -Node value used to mean "query all nodes" and now does again (it had started reaching the API literally as nodes//qemu), and the per-node warning above. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
3fa7eeb023 |
refactor: route the container cmdlets through ContainerService (#126) (#199)
New-PveContainerSnapshot, Remove-PveContainerSnapshot and Restore-PveContainerSnapshot each built their own PveHttpClient and parsed the response inline while ContainerService.CreateContainerSnapshot / RemoveContainerSnapshot / RollbackContainerSnapshot carried the same three requests with no callers. The cmdlets now call the service, so the path and form each sends is asserted offline (ADR 0021). New-PveContainer already called ContainerService.CreateContainer but allocated its container ID with a private PveHttpClient hitting cluster/nextid; it now goes through ClusterConfigService.GetNextId, the seam Copy-PveVm, Copy-PveContainer, New-PveVm and Import-PveOva already use. ContainerService.ParseTask's UPID-string branch now stamps Status = "running", matching SnapshotService.ParseTask from the #126 pattern PR (#196). ParseTask is shared by every ContainerService lifecycle method, so this also changes the non-Wait Status output of ten cmdlets beyond the four converted here, from null to "running" — pinned with a dedicated test. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
48bab3e4cb |
refactor: route the Users cmdlets through UserService (#126) (#198)
Get-PveRole, Get-PveUser, New-PveRole and Set-PvePermission each built their own PveHttpClient and hand-rolled requests, while UserService's GetRoles/GetUsers/CreateRole/SetPermission carried the same requests with zero callers. Wire the cmdlets to the service and change SetPermission's propagate/delete parameters from bool with defaults to nullable bool so the omitted-when-unset shipped behaviour of Set-PvePermission survives the move. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
9af67f78e6 |
refactor: route the Nodes cmdlets through NodeService (#126) (#197)
Get-PveNode and Get-PveNodeStatus built their own PveHttpClient and parsed the response inline. Both now call the existing NodeService methods (GetNodes, GetNodeStatus), which already carried matching requests. NodeService.GetNodeStatus gained the node-name stamp the cmdlet used to apply after deserializing, and both GetNodes and GetNodeStatus regained the missing-data guard the cmdlets had, which the pre-existing service implementation lacked. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
17bb2987b2 |
refactor: route the snapshot cmdlets through SnapshotService (#126, pattern) (#196)
New-PveSnapshot, Remove-PveSnapshot and Restore-PveSnapshot built their own PveHttpClient and parsed the response inline while SnapshotService carried the same three requests with no callers. The cmdlets now call the service, so the path and form each sends is asserted offline (ADR 0021). Reconciled toward the shipped cmdlet behaviour: CreateSnapshot omits vmstate unless it is true, and ParseTask stamps Status = "running" on a UPID-string response. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
907b2aa1f2 |
refactor: share one transport per host and poll tasks with backoff (#151) (#193)
Every service method built and disposed its own PveHttpClient, and each client owned a fresh HttpClientHandler, so every API call was a TCP connect plus a TLS handshake and WaitForTask paid that 300 times over a ten-minute wait. PveServiceBase now owns the injected-or-fresh client lifetime behind one Invoke helper; the 201 hand-written try/finally blocks across the 16 services collapse to calls on it, and the nested NodeService/VmService instances receive the injected client. PveHttpClient takes its handler from a process-wide PveHandlerCache keyed on (host, port, skipCertificateCheck) and never disposes it, so the connection pool outlives any one client. WaitForTask holds one client for the whole wait and, when no pollInterval is supplied, backs off from 1 s toward a 10 s cap, never sleeping past the deadline. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
40f4eae9e2 |
docs: record that changelog entries are batched, and that the up-to-date rule is off (#185)
* docs: record that changelog entries are batched, and that the up-to-date rule is off The automated reviewer on #182 requested a CHANGELOG.md edit on a fix PR because the batching convention lived only in the remediation plan. The reviewer reads CLAUDE.md from main on every run, so the rule goes here. Also records the branch-protection change from 2026-09-02. Closes #177 * docs: drop the remediation reference from the branch-protection note --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
18f5fe6841 |
docs: changelog entries for the wave 2 fixes (#183)
* docs: changelog entries for the wave 2 fixes Lifts the Changelog section of #178, #179, #180, #181 and #182 into [Unreleased]. * docs: do not call the Import-PveOva fallback documented; it was not --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
4f9ee05edf |
refactor: centralize build config and pin the SDK (#156) (#182)
Adds global.json (10.0 SDK, rollForward latestFeature) so local dev resolves the same SDK line CI pins in five workflows, instead of floating to whatever is installed. Adds Directory.Build.props for LangVersion/Nullable, the two properties identical across all three csproj (TargetFramework stays per-project since the test project multi-targets net10.0;net48). Adds Directory.Packages.props with central package management, moving every PackageReference version out of the three csproj into one file. Newtonsoft.Json bumped 13.0.3 -> 13.0.4 in the single place instead of two independent declarations that could drift. xunit.runner.visualstudio stays at 4.0.0 (it runs xUnit v1/v2/v3 per its own description; nothing in the issue's stated xunit 2.9.3 pairing required a downgrade). Removes the self-referential $(NoWarn) from the Core csproj, a no-op. Adds an explicit System.Memory 4.6.3 PackageReference to the net48 leg of the test project, which resolves the MSB3277 conflict between System.Memory 4.0.1.2 and 4.0.5.0 (110 warnings -> 0). Scoped to net48 only per the issue, which names only the net48 test target. Adds PackageVersionCentralizationTests, an xUnit test asserting none of the three csproj declare a PackageReference Version attribute outside Directory.Packages.props. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
716ecd100d |
fix: Import-PveOva's dead not-found catch and missing upload timeout (#180)
* fix: retarget Import-PveOva's not-found catch and add upload timeouts VmService.GetVm throws InvalidOperationException when the VM is not yet listed on the node, never PveApiException(NotFound) — the API call itself returns 200. Import-PveOva's tail catch was for the exception GetVm never throws, so a successful import without -Wait raised the InvalidOperationException unhandled instead of falling back to a basic PveVm. Retarget the catch, and narrow its try region to the GetVm call only so the fallback can no longer fire for a WriteObject failure on an already-retrieved VM. VmService.UploadOva and StorageService.UploadIso built their PveHttpClient with no timeout override, so large OVA/ISO uploads inherited the session's 100s default and aborted mid-transfer. Both now take a TimeSpan? timeout (default 30 minutes), matching Send-PveFile. Import-PveOva gains -TimeoutSeconds mirroring Send-PveFileCmdlet's parameter. Also drops the trailing null, null, null on WaitForTask calls in ImportPveOvaCmdlet.cs and NewPveVmCmdlet.cs, left over from #140. Closes #139 * fix: give UploadOva/UploadIso a timeout override and add coverage Completes the #139 fix: VmService.UploadOva and StorageService.UploadIso now take a TimeSpan? timeout (default 30 minutes) instead of always using the session's 100s default. Adds a GetVm not-found regression test and a timeout-propagation test suite for both upload methods. * test: release the upload before deleting its temp file The two default-timeout tests left the upload in flight and then deleted the file it still had open. Windows refuses that, so both build-and-test legs failed on windows-latest. --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
0ec75a02e7 |
fix: surface swallowed errors in status polling and per-node listing (#181)
* fix: surface swallowed errors in status polling and per-node listing WaitForStatusTransition's poll loop caught every exception except OOM/ StackOverflow and discarded it silently. An expired ticket, a deleted VM, or a wrong node name under -Wait -Timeout spun for the full timeout and then raised PveTaskTimeoutException instead of the real 401/403/404. The loop now catches only PveApiException (excluding 401/403/404, which propagate) and HttpRequestException, and WriteVerbose's what it swallows. VmService.GetVms and ContainerService.GetContainers caught PveApiException of any status per node and continued, so a permission problem or an unreachable node looked identical to "no VMs". The per-node catch now narrows to a 5xx/408/connectivity failure (IsNodeUnreachable) and takes an optional onNodeSkipped callback; Get-PveVm and Get-PveContainer wire it to WriteWarning. Any other status (401/403/404 included) propagates. Adds xUnit coverage for the per-node aggregation path (403 propagates, 500/ 408/connectivity failures are skipped and reported, the other nodes' results still come back), reaching NodeService's internal client via reflection since it is not otherwise constructor-injectable from VmService/ ContainerService. Closes #142 * test: add per-node aggregation coverage and wire onNodeSkipped Adds the remaining changes: VmService/ContainerService per-node catch narrowing plus onNodeSkipped callback, the Get-PveVm/Get-PveContainer WriteWarning wiring, and the xUnit coverage for the aggregation loop. --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
8a82146acc |
fix: allocate a real VMID for Copy-PveVm/Copy-PveContainer and honor -Storage (#179)
* fix: allocate a real VMID for Copy-PveVm/Copy-PveContainer and honor -Storage
Copy-PveVm and Copy-PveContainer defaulted newid to 0 when -NewVmId
was omitted, and never sent -Storage on the clone request even
though both cmdlets declare it. Both cmdlets now allocate via
ClusterConfigService.GetNextId when -NewVmId is null, and forward
-Storage into the clone form body. Since PVE rejects storage on a
linked clone, -Storage without -Full now fails fast client-side
instead of failing later against the API.
NewPveVmCmdlet and ImportPveOvaCmdlet hand-rolled the same
GET cluster/nextid call with a manual JObject parse; both now go
through ClusterConfigService.GetNextId so a response without a
data field raises the service's diagnosable InvalidOperationException
rather than a NullReferenceException.
Closes #135
* test: pin CloneVm/CloneContainer storage and newid form-body behavior
Offline xUnit coverage per ADR 0021: storage present in the clone
form body when supplied, absent when omitted, and newid forwarded
verbatim (never coerced to 0) by the service layer.
* fix: add missing storage parameter to VmService.CloneVm
VmService.cs was omitted from the earlier push; this restores the
storage parameter and form-body wiring that belongs with this fix.
* fix: drop the client-side -Storage/-Full guard from the Copy cmdlets
PVE returns the same error itself when storage is sent on a linked
clone, so the guard only saved one round trip, had no test, and rested
on a behaviour claim the OpenAPI spec does not document.
* test: cover the -Storage/-Full guard in Copy-PveVm and Copy-PveContainer
Offline Pester coverage for the client-side StorageRequiresFullClone
guard: -Storage without -Full throws before a session is required,
and -Storage with -Full does not trip the check.
* test: revert the Pester cases for the removed -Storage/-Full guard
The guard was dropped in
|
||
|
|
17832f15d9 |
fix: Wait-PveTask delegates to TaskService.WaitForTask (#178)
WaitPveTaskCmdlet hand-rolled its own poll loop (while(true) / Thread.Sleep / JObject.Parse), the exact pattern ADR 0001 forbids. It now delegates to TaskService.WaitForTask, passing a progress callback that drives WriteProgress — the seam WaitForTask was built for and that nothing called until now. Preserves the cmdlet's own documented contract (an omitted -Timeout waits indefinitely) by passing a 100-year sentinel instead of null, since TaskService.WaitForTask treats a null timeout as its own 10-minute default, not infinite. Reuses one PveHttpClient for the whole wait instead of letting TaskService open a fresh one per poll. Also strips the redundant trailing 'null, null, null' default arguments from 12 other WaitForTask call sites so they use the short form, per the issue. Left CopyPveContainerCmdlet.cs, ImportPveOvaCmdlet.cs and NewPveVmCmdlet.cs alone — issue #135 is touching those concurrently. Adds three xUnit tests to TaskServiceTests.cs proving the behavior the inline loop got wrong: no sleep before the first status check, the 1-second MinPollInterval clamp, and the progress callback firing on every poll. Mutation-tested by breaking each behavior in turn and confirming the corresponding test fails. Closes #140 Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
8ec09c2b84 |
docs: a PVE-behaviour claim documented against the published spec is verified, not deferred (ADR 0026) (#170)
* docs: a PVE-behaviour claim the PR documents against the spec is verified, not deferred The reviewer fetches the cited proxmox_api permalink with gh api, names it in the review, and approves when the spec supports the claim. Only inferred claims still go to the operator. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: record ADR 0026 and tighten the spec-verified rule per review Commit-anchored permalinks only, the gh api invocation spelled out, and the trade-off against the server-behaviour caveat stated in the prompt and in the ADR. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: cite the per-version OpenAPI file, and attribute the reserved list to the prompt Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
147c2c6007 |
ci: request the operator's review when the automated reviewer defers (#169)
* docs: deferred reviews mention the operator so the decision reaches their inbox Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci: request the operator's review when the automated reviewer defers A COMMENTED verdict sends no notification. The workflow now turns a deferral into a formal review request, which reaches the operator's queue and inbox. The prompt-only mention is dropped in favour of this. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
4cc4e86cfb |
ci: read the release tag from the environment, and refuse tags that are not a version (#159)
The tag name was substituted into the pwsh script text by expression, so a tag containing a quote could run arbitrary PowerShell in the job that holds NUGET_API_KEY. The script now reads it from an env var and rejects anything that is not vX.Y.Z with an optional prerelease suffix. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
918e2c098e |
docs: changelog entries for the wave 1 fixes (#168)
Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
e942878aa2 |
fix: plumb skiplock/force parameters through Remove-PveVm/Container (#164)
* fix: plumb skiplock parameter through Remove-PveVm and Remove-PveContainer
When -Force is specified, both cmdlets now pass skiplock=1 to PVE, which bypasses locks.
PVE honours the skiplock parameter for root@pam only.
Also updated help text on both cmdlets to clarify the limitation and behaviour.
Fixes #136.
* fix: plumb skiplock parameter for VMs, force for containers
VM removal: add skiplock=1 parameter when -Force is specified. PVE honors it
for root@pam only; non-root callers receive 403 errors. Updated help text.
Container removal: map -Force to force=1 (LXC-specific parameter for forcing
removal of running containers). Containers do not support skiplock.
Also: clarified class and parameter documentation to remove false claims about
-Force suppressing confirmation (it does not).
Fixes #136. Addresses correctness reviewer findings.
* fix: revert unrelated formatting churn, add container force test
The em-dash-to-hyphen sweep in commit
|
||
|
|
a7d2b877f5 |
fix: populate Privileges on PvePermission from access/permissions (#163)
UserService.GetPermissions unwrapped the path-keyed /access/permissions response and discarded prop.Value, so every returned PvePermission had a path but no privilege data. Add a Privileges dictionary populated from the privilege map; a key's presence means the privilege is granted and its value is whether the grant propagates to sub-paths, matching PVE's documented "propagate boolean" contract for that endpoint. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
4cc18f1cc2 |
Fix #144: remove defunct DELETE /access/ticket and gain -Session parameter (#162)
* fix: remove defunct DELETE /access/ticket call and gain -Session parameter Disconnect-PveServer was calling DELETE /access/ticket, an endpoint that does not exist in the PVE API spec. The call always failed but errors were swallowed, so the cmdlet succeeded while claiming server-side invalidation occurred. Rebase on PveCmdletBase to gain the -Session parameter, making explicit session disconnection possible (the cmdlet previously only worked with the active session). Update help text to clarify that PVE tickets expire server-side after two hours and cannot be revoked. Remove the DELETE call entirely — tickets are stateless and expire on their own. Clear ModuleState.ActiveSession only when disconnecting the active session (no -Session given) or when explicitly disconnecting the active session. * fix: address reviewer findings on -Session handling and test coverage Fixes from correctness and api-compat reviews: 1. Use BoundParameters to distinguish -Session omitted from -Session $null, preventing accidental active-session clear when $null is passed. 2. Warn and return early when -Session is supplied but not the active session, avoiding silent no-ops that leave the user's session variable populated and functional but with expectations misaligned (they passed a session to disconnect it, but disconnecting a non-active session is now explicit). 3. Use ReferenceEquals() explicitly instead of == for the identity check, future-proofing against PveSession ever gaining value-equality semantics. 4. Fix the lifecycle test to check observable behavior (warning output) instead of reaching into null PrivateData. Tests now verify both "no session to disconnect" and "non-active session supplied" paths. * fix: remove unreliable offline lifecycle tests from Pester suite The lifecycle tests attempted to capture warning output and verify module state, but both approaches fail in the offline Pester environment: 1. WarningVariable captures don't work as expected in Pester contexts 2. ModuleState is not exposed through PrivateData and cannot be inspected from outside the module in offline tests Keep the reliable metadata test that verifies -Session parameter is exposed. The behavioral verification of the warning logic happens in the correctness review and will be validated in integration testing, not in offline unit tests. * fix: move reference equality check before ShouldProcess The ShouldProcess check was firing before validating whether the supplied -Session is actually the active session. This means -WhatIf would prompt "Disconnect host:port?" even for sessions that will not be disconnected (where the cmdlet just warns and returns). Move the ReferenceEquals check before ShouldProcess so mismatched sessions short-circuit with a warning before any prompting occurs. This accurately represents what -WhatIf/-Confirm is about to do. * test: restore and harden offline coverage for Disconnect-PveServer session handling Commit |
||
|
|
61ac247408 |
fix: honour OVF bus type, correct the SCSI/SATA controller mapping, and validate the descriptor (#165)
Import-PveOva ignored disk.BusType and always assigned scsi{i}; the
controller-mapping helper itself had CIM ResourceType 6 (Parallel SCSI HBA)
and 20 (Other storage device, VMware's SATA AHCI controller) swapped. Both
are fixed together since honouring BusType is what makes the mapping bug
observable. The cmdlet now tracks a running index per bus (scsi/sata/ide),
falls back to scsi for an unrecognized or missing bus type, and refuses to
emit a slot number beyond what PVE's qemu-server schema accepts (ide0-3,
sata0-5, scsi0-30), overflowing to scsi and erroring out before upload if
even that is exhausted.
OvfMetadata also trusted the OVF descriptor's ovf:href attribute (an
attacker-controlled disk file name inside a downloaded OVA) unvalidated,
letting a crafted href inject extra keys into the comma-separated PVE
property string written for import-from. hrefs are now checked against
^[A-Za-z0-9._-]+$ and rejected outright if they are exactly "." or "..",
closing a path-segment escape the character class alone did not block.
The regex is anchored with \A/\z rather than ^/$ so a trailing line feed
cannot slip past under .NET's default multiline-$ semantics.
The descriptor is now parsed through XmlReader with DtdProcessing.Prohibit
and XmlResolver = null instead of XmlDocument.LoadXml directly, closing
the internal-entity-expansion memory-exhaustion route a hostile descriptor
could otherwise use.
ParseOvfXml is now internal (assembly already has InternalsVisibleTo the
test project) so the new tests can drive it directly with inline OVF XML
strings instead of building TAR archives.
Reviewer findings acted on (Codex, correctness-reviewer, security-reviewer,
run in parallel against the working tree): the dot/dot-dot href bypass and
the missing \A/\z anchoring were found independently by all three and
fixed; the bus-slot ceiling was found by two and fixed. Findings not acted
on, with reasons: validating the local -Path OVA filename the same way
(security-reviewer, medium) — that name is operator-supplied, not drawn
from the attacker-controlled archive contents the issue names, and touching
it would widen the change past the two issues' stated scope; percent-
decoding/loosening the href character class to admit spec-legal names with
spaces or encoding (security-reviewer, correctness-reviewer, medium) —
issue #148 specifies this exact character class, and loosening it reopens
the property-string injection the issue asks to close; capping the raw
byte size of the extracted .ovf entry against decompression-bomb exhaustion
(security-reviewer, low/medium) — a distinct DoS vector from the DTD
entity expansion issue #148 names, not part of its stated scope; branching
CIM ResourceType 20 on ResourceSubType to separate SATA from NVMe
(security-reviewer, low) — issue #138 specifies the 6/20 mapping exactly
as fixed here. An existing Integration-tagged Pester assertion
(tests/PSProxmoxVE.Tests/Integration/12_OVA.Tests.ps1:66) checks
$config.Scsi0 specifically; if that suite's fixture OVA ever carries a
type-20 controller the disk will now land on sata0 instead and the
assertion will need updating — left alone here since it is excluded from
every offline run this change was verified against and touching it without
being able to run it against live PVE would be guessing.
Closes #138
Closes #148
Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
|
||
|
|
8255e506a6 |
fix: recognize boolean and varied integer formats for guest exec exited status (#160)
* fix: recognize boolean and varied integer formats for guest exec exited status The guest-agent schema declares 'exited' as a boolean, but the PVE API may return it in different formats: as a JSON boolean true, or as integers 1/0, or string '1'/'0'. The cmdlet was only checking for long 1L, causing it to timeout on any PVE build that passed a boolean true unchanged. Add ApiValueHelper.IsExited() to normalize these values and recognize true, 1L, 1, and '1' as exited. Update the polling loop to use it. * test: add integration tests for ApiValueHelper with real JSON payloads Add tests that feed JSON data through the actual JsonHelper.ToNative parsing pipeline to verify ApiValueHelper.IsExited correctly recognizes boolean true and numeric 1 values as they arrive from the PVE API. These tests pin the contract between the JSON parsing layer and the value-recognition logic, ensuring the fix for issue #141 works end-to-end with real API response shapes. * fix: restore correct IsExited implementation with type checks The helper must handle boolean true, long/int 1, and string "1" as the issue specifies. This restores the correct multi-type check that was inadvertently reverted. * fix: correct boolean value handling in IsExited --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
def8dc6b67 |
fix: verify checksums for downloaded ISOs/images, keep sshpass off argv (#166)
* fix: verify checksums for downloaded ISOs/images, keep sshpass off argv ensure-base-iso.sh downloaded the PVE install ISO over plain HTTP with no checksum, caching it on the persistent /opt/pve-integration mount and booting it as the nested trust root the integration suite relies on. ensure-cloud-images.sh fetched the Ubuntu cloud image and OVA over HTTPS but never checked them either. prepare-test-environment.sh and diagnose-cluster.sh passed the nested root password to sshpass via -p, putting it in the process table. create-api-token.sh, unused anywhere in the repo, minted a privsep=0 root token and echoed the secret unmasked. - ensure-base-iso.sh now downloads from https://enterprise.proxmox.com/iso and verifies against its SHA256SUMS on every run, including a cache hit. download.proxmox.com's own TLS cert does not list download.proxmox.com in its SAN (confirmed with curl/openssl from this environment), so https to that name fails certificate validation; enterprise.proxmox.com serves the identical ISO tree over a valid cert. Verification happens before the downloaded file is moved to its canonical cache path. - ensure-cloud-images.sh verifies the cloud image and OVA against Ubuntu's published SHA256SUMS the same way, matching by upstream filename since the cloud image is cached locally under a different extension (.img upstream, .qcow2 cached — the bytes are already qcow2-formatted). - prepare-test-environment.sh and diagnose-cluster.sh now export SSHPASS and call sshpass -e, keeping the password out of argv/ps. This also fixes a latent bug: the old unquoted `sshpass -p ${ROOT_PASS}` word-split any password containing whitespace. - create-api-token.sh deleted; grep across the repo found no caller. Reviewers (codex:codex-rescue, correctness-reviewer, security-reviewer) all independently found the same blocking bug in the first pass: when a cached file failed verification and the subsequent redownload then failed, ensure-cloud-images.sh fell through to a "keep the stale copy" branch and returned that same known-bad file with exit 0 — verification could be bypassed by inducing one failed redownload. Fixed by deleting the file immediately on a failed verification, before the redownload is attempted, so the later "is there a safe stale copy" check can no longer find it. Added a test case (case 5) that reproduces this exact sequence and mutation-tested it against the unfixed code. The three reviews also flagged a real but separate bug already fixed in this same change: `trap ... RETURN` inside a function nested in another function is not scoped to that function in bash — it re-fires on the OUTER function's return, referencing an out-of-scope local. Both verify_checksum() helpers now clean up their temp file explicitly instead of via trap. Findings not acted on, judged out of scope for this fix: - SHA256SUMS-fetch failures are treated the same as a checksum mismatch (delete + fail) rather than left untouched — a transient network blip destroys a good multi-GB cached ISO. This is the safer failure direction (never silently trust unverified bytes) and was a deliberate trade-off, not a defect. - ensure-cloud-images.sh's 7-day cache window can span an upstream republish of noble/current, causing a legitimate re-verification churn (not a security issue, a cache-hit-rate one). Pre-existing cache design, unrelated to adding verification. - wait-for-pve.sh (curl -d with the password on argv) and prepare-test-environment.sh's own positional password argument (from run-integration.sh) carry the same password-on-argv pattern this issue targeted in create-api-token.sh, sshpass -p and diagnose-cluster.sh, but neither script nor run-integration.sh was named in the issue. Left untouched per scope; worth a follow-up issue. - GPG/detached-signature verification of the upstream SHA256SUMS was not added — the new checks defend against cache poisoning and transit corruption, not a compromised origin. Worth a follow-up issue. - The two new self-checks (ensure-base-iso.test.sh, ensure-cloud-images.test.sh) are not wired into .github/workflows/unit-tests.yml's shell-selfchecks job. That file is code-owned and out of scope for this change; needs an operator follow-up. Password rotation (the Testpass123! value from before it moved to a secret) is unaddressed here per the contract — flagged for the operator. Mutation-tested: broke the post-download checksum check in ensure-base-iso.sh, confirmed the affected test cases failed, restored it. Broke the sshpass -e change back to -p, confirmed the new assertions in prepare-test-environment.test.sh failed, restored it. Broke the fail-open fix in ensure-cloud-images.sh, confirmed case 5 failed, restored it. Closes #149 * fix: also verify the stale-by-age fallback copy in ensure-cloud-images.sh PR review on #166 (COMMENTED, non-blocking) found the sibling of the fail-open bug already fixed in this branch: when the cached cloud image is stale by *age* (>= 7 days) rather than failed verification, the redownload-failure fallback could hand back that file with exit 0 without ever re-verifying it in this run. A file that failed the earlier verification is already deleted by the time the fallback runs, but a stale-by-age file skips verification entirely on the way in. Fixed by verifying the stale-by-age file at the point of actual fallback use — after the redownload has failed, not proactively before it's attempted, so a copy the redownload was about to replace anyway isn't deleted along a path that would have succeeded. Added two test cases (6, 7): a still-verifying stale-by-age copy is used as a fallback; one that no longer verifies is not. Mutation-tested by reverting to the unfixed fallback and confirming case 7 fails, then restored. --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
1bc46567f5 |
fix: inject the guest-lock retry delay so tests no longer race wall clock (#167)
PveHttpClientLockRetryTests set GuestLockRetry's retry budget via reflection on a private field with no production writer, and scripted two lock failures to land inside a 400ms window. A cold or loaded CI runner's first-attempt JIT and scheduling could burn past 400ms before the second attempt started, failing the test though the retry itself was correct (#134). GuestLockRetry.ExecuteAsync gains an internal overload that takes the inter-attempt delay as a Func<TimeSpan, Task>; the public overload keeps defaulting to Task.Delay, so production behaviour (45s window, budget/4 capped at 2s) is unchanged. PveHttpClient gains a matching internal constructor seam (window, handler, delay), replacing the reflection the tests used for both the private HttpClient and the retry window. Tests now pass a no-op delay, so the retry loop's real elapsed time drops to microseconds and the production 45s window can never be exhausted by runner speed. Two tests pin that production still waits for real: one records the delay invocations through the internal seam and asserts the computed interval, the other drives the public overload with a small window and asserts wall-clock time actually advances. Both were mutation-tested against a no-op-default regression and fail without the fix. The give-up test was renamed (PutAsync_DoesNotReissueWhenTheRetryWindowIsAlreadySpent) to describe what TimeSpan.Zero actually proves: the client never attempts a reissue once the budget reads spent, not a multi-attempt exhaustion sequence — a review finding on the original name. Out of scope, noted for follow-up: GuestLockRetryTests.cs's synchronous Execute() tests still use a 400ms ShortWindow with real Thread.Sleep, which is the same flake shape on the sync path; Execute() has no delay seam. PveHttpClientTimeoutTests.cs and PveHttpClientFormEncodingTests.cs still reflect on the private _httpClient field, which the new handler seam could also retire. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
1bf7483a4e |
fix: escape dynamic path segments in three Remove-* cmdlets (#161)
* fix: escape dynamic path segments in three Remove-* cmdlets Wrap Storage, Vnet, and Zone parameters with Uri.EscapeDataString() in RemovePveStorageCmdlet, RemovePveSdnVnetCmdlet, and RemovePveSdnZoneCmdlet to prevent path traversal attacks via the API path. Add xUnit test demonstrating that escaped paths preserve percent-encoding (preventing path collapse) while unescaped paths allow segment traversal. Fixes #145 * fix: route Remove-Pve{Storage,SdnVnet,SdnZone} through their services Delete the private PveHttpClient construction and inline Uri.EscapeDataString call in RemovePveStorageCmdlet, RemovePveSdnVnetCmdlet and RemovePveSdnZoneCmdlet; call StorageService.RemoveStorage / NetworkService.RemoveSdnZone / NetworkService.RemoveSdnVnet instead, which already escape the identifier identically and are now the single place doing so. Add ValidatePattern on the Storage/Vnet/Zone parameters as defense in depth, anchored with \A/\z so a trailing newline cannot slip a disallowed character past the gate. Replace PveHttpClientPathEscapingTests with a version that actually regression-tests the real Uri parser (asserts both that %2F survives and that the unescaped form is absent), and add StorageServiceTests/NetworkServiceTests cases that mock IPveHttpClient and verify the exact escaped DELETE path for a traversal-attempt name. --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
68f953075d |
ci: run integration tests on manual dispatch only during the remediation waves (#158)
Every merge to main queued a ~45 minute integration run behind fixes that are already pinned by offline tests. Runs now happen on workflow_dispatch at wave boundaries. Restore the push trigger when #134-#157 have landed. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
09a2384cd6 |
docs: record the branch protection that is actually configured (#133)
* docs: record the branch protection that is actually configured CLAUDE.md has claimed since March that main had 'required build checks, required review, admin enforced'. Two of those three were wrong: direct pushes to main were still permitted until 2026-09-02, and admin bypass is now deliberately available rather than enforced. Records what is actually set: required checks, required PR with an approving review, stale reviews dismissed on push, and review from Code Owners required. Explains the narrow CODEOWNERS, so a future session knows why a workflow or ADR change needs the operator while ordinary work does not. Admin bypass is available on purpose. GitHub does not let an author approve their own PR, so without it an operator-authored governance change would deadlock. It is not a hole here: App installations do not get admin bypass, so it does nothing for a compromised or injected bot. ADR 0025 previously said enabling code-owner review was the operator's action and not done by that decision. It has been done, so the dismissal gate is now defence-in-depth rather than the load-bearing control - GitHub enforces the property directly, including against approvals from claude.yml, the ungated path the gate cannot see. * docs: trim a clause ADR 0025 stated twice in four lines Review nit on #133. The 'claude.yml is an ungated second path the dismissal step cannot see' clause appeared in consecutive paragraphs. The first states it; the second now refers back rather than restating. --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
c38ca95987 |
ci: take review instructions from the default branch, and gate self-approval (#132)
* ci: take review instructions from the default branch, and gate self-approval Moves the review prompt out of claude-code-review.yml into .github/review-prompt.md, materialized from the default branch at review time. Prompt edits then neither trip claude-code-action's anti-tamper gate nor influence the review of the PR making them. PR #131 is what prompted this: 38 files of ADR migration were blocked from review by a four-line prompt edit. A security review of the first draft found the naive materialization was itself exploitable, and that is fixed here rather than shipped: - git show "origin/$DEFAULT_BRANCH:..." takes an UNQUALIFIED refname, and gitrevisions(7) resolves refs/tags/<name> before refs/remotes/<name>. With fetch-depth: 0 fetching all tags, a tag named "origin/main" would supply the review instructions for every PR, exiting 0 with only a stderr warning. Reproduced end to end. Now resolves refs/remotes/origin/<default> to a SHA, logs it, and reads by SHA. - CLAUDE.md is materialized from the default branch too. The prompt judges against its Key Conventions list, so reading it from the PR checkout let a PR edit the list to permit its own violation. - A PR touching review-prompt.md, .github/workflows/ or CLAUDE.md now has any claude[bot] APPROVED dismissed via the API and the check failed. Prose alone cannot protect the root of trust. - The sentinel is grepped in the materialize step rather than only asserted by the model it protects. - Fork PRs are skipped, not failed. A required check permanently red on outside contributions trains the operator to override red checks. Also drops track_progress and gh pr comment so the review body and inline comments are the only channel, and adds actions: read plus gh pr checks / gh run view so the reviewer can verify build and test claims against CI's own result. It deliberately gets no build or test tools: those execute PR-authored code in a job that can approve the PR. ADR 0025 records the decision. * ci: fix three defects found in second-opinion review - Job-level 'actions: read' was missing. An explicit permissions block sets every unlisted permission to none, so additional_permissions: actions: read on the action alone granted nothing and the reviewer could not have read the check runs it was just told to verify claims against. Athena has both; only the action-level half was copied. - The governance detector checked review-prompt.md, .github/workflows/ and CLAUDE.md, while review-prompt.md told the reviewer DECISIONS.md and docs/decisions/ were mechanically covered too. A PR adding an ADR could escape the guard it was promised to be under. Detector now covers both, which means every ADR PR needs operator approval — that is the intended reading of ADR 0023, since the reviewer defers to ADRs as precedent. - The formal-review check counted ANY historical claude[bot] review, so a re-run after a new push went green on a verdict about the previous commit. Now scoped to the head SHA. Pre-existing, fixed here because the file was already open. Adds a fork-notice job. A skipped job reports its required check as PASSING, so skipping the review on fork PRs made them green with nothing reviewed and no trace of why; the notice puts it in the run summary. Prompt: pending checks are a race, so say unverified rather than reporting a queued check as a failure; and defer-to-operator maps to --comment, not --request-changes. * ci: withhold approval on review-governing PRs, not the review itself Operator ruling: Claude should still review the protected files and report what it finds; only the power to approve is reserved. The gate failed the check unconditionally whenever a PR touched a governance path, even when the reviewer had correctly submitted COMMENTED. That discarded a review that was wanted, and made a red check the normal outcome for a whole class of PR — training exactly the merge-past-red habit the fork-notice change exists to avoid. Now: a COMMENTED deferral passes, with a notice saying the check is green because the reviewer behaved, not because the PR is approved. Merge still waits for the operator, since COMMENTED does not satisfy branch protection. The step fails only when claude[bot] actually approved — that approval is dismissed and the red check records the disobedience. ADRs stay in the protected set, per the same ruling. * ci: close a green-with-live-approval hole and widen the governance detector Third security pass. The two that mattered: - Both review queries were unpaginated. GitHub returns 30 reviews oldest-first, so an approval submitted now sits on page 2 of any PR that already has 30 review objects — the withhold step would find nothing, print 'reviewer behaved correctly' and exit 0 green while the approval stood and satisfied branch protection. Reachable without an attacker: every inline comment creates a review object. Both queries now --paginate. - The detector covered CLAUDE.md but not .claude/, .mcp.json, AGENTS.md, or nested CLAUDE.md. .claude/settings.json is tracked and .gitignore had no claude entry, so a PR could add .claude/settings.local.json - which outranks settings.json - carrying env (redirect model traffic) or hooks (arbitrary shell in the job holding the approval token). Those are read by the runtime before the model starts, so no prompt-level rule can defend against them. Added to the detector and to .gitignore. Also: dismissal now matches .user.type == 'Bot' rather than the claude[bot] login literal, so an approval from another App is not invisible; the test fixtures, psd1 and CHANGELOG join the detector, since the prompt already reserved release tagging to the operator and did not enforce it; a failed dismissal says so loudly instead of aborting silently under bash -e and claiming success; the pre-review notice no longer promises a red check on the path that goes green; show-ref --verify replaces rev-parse, which still DWIMs on a ref that does not exist; and a concurrency group stops two runs interleaving dismissals. Dropped the '@claude re-review' suggestion from the fail-closed message: it pointed at an ungated workflow on exactly the PRs where approval is reserved. ADR 0025 now states the premise the whole design rests on - that code-owner review is off, deliberately, because enabling it would end bot merging - and records claude.yml as an open second path to a binding approval. * ci: narrow CODEOWNERS so code-owner review becomes usable Operator's suggestion, and it is better than what ADR 0025 previously recorded. CODEOWNERS was '* @goodolclint'. At that breadth 'Require review from Code Owners' is unusable — it would demand the operator on every PR and end the verdict-gated merge loop — which is why the setting is off and why the self-approval guard had to live in the workflow. Narrowed to the governance and release paths only, matching the detector. An ordinary PR has no code owner and an automated approval still merges it; a PR touching what governs review or what gets published requires the operator. That makes the setting safe to enable, and GitHub then enforces the property better than the workflow step can: not one-shot, no pagination limit, no bot identity to match, no dismissal permission needed, and it covers an approval from any source — including claude.yml, the ungated second path the dismissal step cannot see. Enabling the setting is the operator's action, not this commit's. Until then the workflow gate remains load-bearing, and it stays either way as defence-in-depth. Both files carry a keep-in-sync note; drift is silent in the direction that matters. ADR 0025 records the edge case: GitHub does not let an author approve their own PR, so an operator-authored governance PR would need admin enforcement toggled or to go through the bot. * ci: stop the green-path notice claiming more than it checked Third-party re-review: the empty-id branch announced 'It reviewed and deferred, as intended', but an empty list only means no automated APPROVED was found. It cannot distinguish a deferral from CHANGES_REQUESTED, from no verdict, or from no review at all — that a formal review exists at this head SHA is established by the verify step, not this one. The notice now says what was actually checked, and says plainly that green does not mean approved or adequately reviewed. ADR 0025 said a prompt edit 'gets a red check', contradicting its own statement two paragraphs earlier that a deferral passes. Corrected, and it now records the one governance path that genuinely gets no review: this workflow itself, where the action's anti-tamper gate means there is no verdict to observe. --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> |
||
|
|
b90791e2bf |
docs: migrate DECISIONS.md to house-format ADRs, and retire the review folder (#131)
D001-D021 become ADR 0001-0021 in docs/decisions/, one decision per file. D017's PESTER_VERSION amendment was a second decision in one entry and becomes ADR 0022. ADR 0023 records the migration and reverses the lane2-change-plan ruling that deliberately kept DECISIONS.md until the CI lane work landed. DECISIONS.md is reduced to a stub with a D-to-ADR redirect table, so the four released CHANGELOG entries and older issue bodies that cite it degrade to a redirect rather than a dead reference. docs/review/ and docs/lane2-change-plan.md are deleted (ADR 0024). Of 91 findings, 83 were resolved and six of the seven still open were already GitHub issues; F021 was the exception and is now #130. CLAUDE.md's Key Conventions list gains the two rules it was missing and becomes the checklist, with the ADRs carrying rationale. |
||
|
|
c3f051bba5 |
docs: record D021 — integration tests prove server semantics, payloads offline (#125)
Records the testing strategy decided while planning #120: an integration test must earn its place by testing something only a live PVE can answer, and request-payload correctness is verified offline against the mock IPveHttpClient harness. The boundary comes from #92. Set-PveNetwork sent bridge_vlan_aware=0 to clear a VLAN-aware bridge; the schema advertises a plain boolean, so the request succeeded, and PVE merged the key onto the stored stanza and ignored the 0. Only delete=bridge_vlan_aware works, and only a real PVE 9 revealed it. 37 of 194 concrete cmdlets construct PveHttpClient directly and have no offline seam; they convert before the next large coverage push. The suite tiers by area on PRs, ACME is covered by contract tests because a CA and DNS reachability cannot exist in CI, and Ceph lives behind an opt-in provisioning profile. |
||
|
|
1e3555a898 |
Merge pull request #118 from GoodOlClint/feat/network-bridge-vlan-aware
feat: VLAN-aware bridges via New-PveNetwork and Set-PveNetwork |
||
|
|
2dff02f2bd |
feat: VLAN-aware bridges via New-PveNetwork and Set-PveNetwork
PveNetwork already deserialised bridge_vlan_aware as BridgeVlanAware, so a VLAN-aware bridge could be read back but never created or changed. Both write paths now take a -BridgeVlanAware switch. Clearing the flag does not use bridge_vlan_aware=0. PVE merges the supplied keys onto the stored stanza and accepts that 0 without acting on it, so the obvious form is a silent no-op: an integration run against PVE 9 issued it and Get-PveNetwork still reported 1. The endpoint's delete list is what actually removes the key. The API schema advertises a plain boolean and gives no hint of this, which is why the behaviour is pinned by an integration test rather than inferred. Set-PveNetwork guards the switch on BoundParameters so an update that omits it leaves the flag alone; the create path follows the existing -Autostart form. Only bridge_vlan_aware is added. bridge_vids is an independent parameter that PVE defaults to 2-4094, and the issue asks only for the flag. Coverage: the integration suite pins create, disable, re-enable, and that an unrelated Set leaves the flag alone -- that last one kills a mutant that drops the BoundParameters guard, which every other test survives. A model test pins the read path the assertions depend on. The Pester unit tests assert only parameter metadata; the defect is server-side, so nothing offline can catch it. Closes #92 |
||
|
|
fc3c7ffff4 |
Merge pull request #117 from GoodOlClint/fix/guest-lock-retry
fix: retry the qemu-server flock instead of predicting it |
||
|
|
1e94d4188c |
fix: report each flock reissue, and scale the gap to the retry budget
Two non-blocking review observations. A 45s retry is indistinguishable from a hang with nothing on the wire, so GuestLockRetry.Execute takes an onRetry hook and InvokeGuestTask reports each reissue through WriteVerbose. The gap between attempts now scales with the budget, capped at the 2s production value. A caller passing a short window wants a fast answer rather than one long sleep, which also takes the retrying unit tests off a real 2s sleep each: the xUnit run drops from 8s to 4s. PveHttpClient's window becomes a field so those tests can shorten it too. |
||
|
|
db416a3f03 |
fix: retry the qemu-server flock instead of predicting it
WaitForStatusTransition refused to return while snapshot.Locked, and its comment quoted the exact error it was meant to prevent. Locked reads the guest config's lock: property; the failure is the flock on /var/lock/qemu-server/lock-<vmid>.conf, which PVE exposes nowhere. The flock cannot be observed, so it is retried. GuestLockRetry reissues an operation for a bounded 45s while PVE reports failing to enter lock_config for a guest, which it raises before doing any work. Two seams, because the failure has two surfaces. PveHttpClient.SendAsync retries the request for operations PVE serialises in the API handler; it takes a request factory because an HttpRequestMessage cannot be resent. PveCmdletBase.InvokeGuestTask reissues the call and re-waits its task for operations serialised in the forked worker, where the POST returns 200 and only the task fails. WaitForStatusTransition routes through the latter, hence Func<PveTask>. The predicate is path-specific and anchored at the start of what PVE said: lock_file uses identical wording for storage, LVM and HA locks, and a qmclone that fails after allocating disks must not be reissued into "VM already exists". That requires the raw text, so it reads PveTaskFailedException.ExitStatus and PveApiException.ApiMessage. The Locked check stays — it is correct for the config lock — with a comment that says so. Closes #113 |
||
|
|
23ad9840c6 |
Merge pull request #115 from GoodOlClint/ci/pin-pester
ci: pin Pester by exact version everywhere it is installed |
||
|
|
d287ea8e26 |
Merge branch 'main' into ci/pin-pester
# Conflicts: # .github/workflows/unit-tests.yml |
||
|
|
fc3681a785 |
Merge pull request #114 from GoodOlClint/fix/preflight-cleanup-iso
fix: reap the whole generated-ISO family without over-matching, and stop building python from the filename |
||
|
|
fc073e7e2a |
ci: pin Pester by exact version everywhere it is installed or imported
Pester was installed with -MinimumVersion 5.0 and no ceiling in the CI job image, both install steps in unit-tests.yml, and both Import-Module calls, plus the suite's own import inside the container. The image is rebuilt on every CI run and Pester is installed fresh on every unit-test run, so PSGallery chose the version — a new major could reach the required PR checks with no commit here, surfacing as unexplained test breakage on whichever PR ran next. It had already happened. Steps named "Install Pester 5" were resolving 6.1.0 on both legs, because Pester 6 declares PowerShellVersion 5.1 and so installs on Windows PowerShell too. Nothing broke — the suite uses only constructs common to 5 and 6, and runs 1566/0 under 6.1.0 with no deprecation warnings — but nobody chose it. The step names are corrected; they had been describing an install that stopped happening some time ago. Pinning the install alone is not enough, in two ways review found: An unset variable does not fail. -RequiredVersion accepts an empty value and degrades to "latest" for Install-Module and to "any" for Import-Module, both exiting 0, so a renamed or dropped env key would silently restore the float this commit removes. A guard step now fails the job instead. The point of use was still floored. run-integration.sh imported the suite's Pester with -MinimumVersion 5.0, so a second Pester reaching PSModulePath would win regardless of what was installed. The Dockerfile now promotes the ARG to ENV so the version is discoverable at runtime, and that import is pinned to it. The pin lives in two files, so shell-selfchecks asserts they agree — split-brain between the workflow and the image is precisely the unexplained breakage this is meant to prevent. CONTRIBUTING.md and CLAUDE.md are updated too; the contributor instructions were a third floating install site. Recorded as an amendment to D017 — the same principle as the nested PVE package pin, applied to the lane's own tooling. |
||
|
|
122e79407c |
fix: reap the whole generated-ISO family without over-matching, and stop building python from the filename
Two filed issues in one rewrite of preflight-cleanup.sh's ISO block, because they are the same twenty lines. #111 — ISO_FILENAME was interpolated into python3 -c PROGRAM TEXT inside a single-quoted literal, so a quote in the value escaped it and executed arbitrary Python in a container holding PVE_API_TOKEN, PVE_PASSWORD, the Terraform state and the storage VM's SSH key. It now arrives through the environment and is read with os.environ. The volid is passed to urllib's quote() via argv for the same reason, and an empty encode result now skips the volume instead of issuing a DELETE against the bare collection URL. #105 — generated ISOs embed a hash of first-boot.sh, so every change to that script mints a new filename. Deleting only the exact current name orphaned each earlier ISO on the storage permanently, because force-cleanup wipes the Terraform state that could otherwise reclaim it. The family is now swept by rebuilding the full generated shape: the captured prefix plus twelve hex characters plus .iso. A prefix test alone would also have matched a longer FQDN's family and any hand-uploaded "-manual-backup.iso" sibling, which in a script whose job is deletion is worse than the leak it fixes. Multi-delete applies only to that family. A name that is not generated — the storage VM's cloud image — keeps the original one-shot behaviour, since a basename can repeat across content namespaces and a plain name carries nothing that identifies a family. Adds preflight-cleanup.test.sh, wired into shell-selfchecks. The script had no coverage at all. It stubs curl and sleep, then asserts on the DELETEs issued: the family goes, the pinned base ISO and unrelated uploads stay, a non-hash sibling stays, the cloud image takes only itself, a quoted payload is data rather than code, and unset storage skips only the ISO branch. Every case also asserts the script ran to completion and removed the Terraform state, so a path that dies early cannot pass by having issued the right DELETEs first. |
||
|
|
8623118557 |
Merge pull request #110 from GoodOlClint/chore/local-dev-repair
chore: repair the local dev path and delete its dead scaffolding |
||
|
|
298df2a30b |
docs: cite the right lines for the fixed VMIDs
The review caught that run-integration.sh:106-138 covers pve_vmid() but not STORAGE_VMID, which is set at line 80. Cite both accurately. |
||
|
|
d5678284bc |
docs: full macOS recipe for the integration flow, and the shared VMID hazard
Expands the Rosetta note into a working recipe, after running the whole provision -> test -> cleanup lifecycle on an Apple Silicon Mac against the real parent cluster. Compose is the wrong entry point on a Mac: its dev-infra service builds rather than pulls, and bind-mounts /opt/pve-integration, which does not exist there. The macOS path pulls the image CI already built and drives run-integration.sh with docker run. Records that GHCR needs a classic PAT, since fine-grained tokens cannot reach it at all and the failure gives no hint why. Restores the x86 compose instructions, which the previous commit's rewrite consumed, and hoists the fixed-VMID warning out of the macOS section — 5080, 5091 and 5092 are shared with CI on the same parent cluster whatever host you run from, so a local run during a CI run collides, and a skipped force-cleanup fails the next run's headroom guard. Also warns that emulation runs the suite ~40% slower and so loses the qemu-server flock race (#113) that CI wins: Reset-PveVm, clone and Set-PveVmConfig fail locally on a tree that is green in CI. Provisioning and cleanup are unaffected. |
||
|
|
439a691516 |
docs: the dev-infra image needs Rosetta on Apple Silicon
"x86 only" was too strong. The image is amd64-only — proxmox-auto-install-assistant and the HashiCorp apt repo publish no arm64 — but it builds and runs on Apple Silicon once Docker Desktop's Rosetta emulation is on. Under the default qemu translation pwsh starts and reports its version, then segfaults on module discovery (uncaught target signal 11), which fails the build at Install-Module Pester and would fail Pester at test time. With Rosetta enabled the same Dockerfile builds to within 150 bytes of the image CI pushed for this commit, and Invoke-Pester runs. Worth stating explicitly because the failure is silent: the build step exits 1 with no diagnostic output, which reads as a Dockerfile defect rather than an emulation problem. |
||
|
|
08ee3ae249 |
chore: repair the local dev path and delete its dead scaffolding
The local dev environment had drifted badly from CI. Remove the parts that no longer describe anything real, and make the rest match how CI actually runs. Delete tests/dev.ps1. It wrapped run-integration.sh, which CI calls directly, and duplicated the module build that script already performs internally. As a second entry point it drifted: it still offered the PVE 8 leg retired in #88, mounted the Docker socket for storage containers replaced by the storage VM in #87, and pointed its remote-host examples at a runner decommissioned in the ARC migration. All four documents describing it used a positional syntax that bound the bare word to -Tests and then fell through to -Shell, so every documented command silently opened a container shell. Recorded as D019. Delete tests/infrastructure/runner/, a self-hosted-runner-in-Docker superseded by Actions Runner Controller. Make disk_storage and iso_storage required. Their defaults named a NAS that the lab replaced with Ceph, and CI overrides both from repository variables, so the defaults only ever misled local runs. require_env now fails at the top of a run rather than at terraform apply, and the descriptions point at tests/.env.test because cmd_provision deletes terraform.tfvars before applying. preflight-cleanup.sh no longer falls back to the literal "local" storage. An unset TF_VAR_iso_storage now skips only the ISO branch, leaving VM destroy and state cleanup intact, and emits a workflow annotation: force-cleanup is the only cleanup CI runs and it wipes Terraform state, so a silent skip strands the uploaded ISO with nothing left to reclaim it. Drop docker-ce-cli and the /var/run/docker.sock mount. Nothing in the container has called docker since #87 moved storage into a VM; the remaining docker calls run inside that VM over SSH. The CI job image is built from the same target, so this also removes a third-party apt repository from its supply chain. Rewrite tests/.env.test.example against what the code now requires, and fix the documented commands in CLAUDE.md, README.md, copilot-instructions.md and the integration README. |
||
|
|
1a848ff2d8 |
Merge pull request #109 from GoodOlClint/docs/lane2-decisions
docs: record D017 and D018 for the two-lane CI split |
||
|
|
c115863e07 | docs: record D017 and D018 for the two-lane CI split | ||
|
|
f95f08deb9 |
Merge pull request #108 from GoodOlClint/ci/lane2-reporting
ci: report package currency to a rolling issue and a data branch |