FM-602: Proxmox guest lifecycle and task operations - #104
Conversation
There was a problem hiding this comment.
3 issues found across 15 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/fleetctl/src/lib.rs">
<violation number="1" location="crates/fleetctl/src/lib.rs:1226">
P3: The parser accepts the four new lifecycle commands, but `usage()` still omits them; add their syntax so help and error messages expose the new interface.</violation>
</file>
<file name="crates/fleet-controller/src/proxmox_exec.rs">
<violation number="1" location="crates/fleet-controller/src/proxmox_exec.rs:141">
P1: When the lifecycle request or a poll interval crosses `timeoutSeconds`, this executor can still accept a later `Ok` status because the deadline is not checked before that terminal return. Start the deadline before the mutation and bound/check each poll and sleep against the remaining time.</violation>
</file>
<file name="crates/fleet-controller/tests/proxmox.rs">
<violation number="1" location="crates/fleet-controller/tests/proxmox.rs:132">
P3: The `_worker` field is never populated: both `harness_with` and `lifecycle_harness` construct `Harness` with `_worker: None`, and its doc comment claims it is "aborted on drop." The only real worker handle is the local `_worker_handle` in `lifecycle_harness`, which goes out of scope at the end of the constructor; dropping a tokio `JoinHandle` detaches the task, it does not abort it. Store the handle in `_worker` and abort it in `Drop` (which currently only shuts down the HTTP server), or remove the field and correct the comment.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| .guest_lifecycle(request.clone(), node, vmid, action) | ||
| .await | ||
| .map_err(|error| format!("the lifecycle action failed: {error}"))?; | ||
| let started = std::time::Instant::now(); |
There was a problem hiding this comment.
P1: When the lifecycle request or a poll interval crosses timeoutSeconds, this executor can still accept a later Ok status because the deadline is not checked before that terminal return. Start the deadline before the mutation and bound/check each poll and sleep against the remaining time.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/fleet-controller/src/proxmox_exec.rs, line 141:
<comment>When the lifecycle request or a poll interval crosses `timeoutSeconds`, this executor can still accept a later `Ok` status because the deadline is not checked before that terminal return. Start the deadline before the mutation and bound/check each poll and sleep against the remaining time.</comment>
<file context>
@@ -0,0 +1,315 @@
+ .guest_lifecycle(request.clone(), node, vmid, action)
+ .await
+ .map_err(|error| format!("the lifecycle action failed: {error}"))?;
+ let started = std::time::Instant::now();
+ loop {
+ let status = self
</file context>
| } | ||
| _ => Err(CliError { message: usage() }), | ||
| }, | ||
| "start" | "stop" | "shutdown" | "reboot" => { |
There was a problem hiding this comment.
P3: The parser accepts the four new lifecycle commands, but usage() still omits them; add their syntax so help and error messages expose the new interface.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/fleetctl/src/lib.rs, line 1226:
<comment>The parser accepts the four new lifecycle commands, but `usage()` still omits them; add their syntax so help and error messages expose the new interface.</comment>
<file context>
@@ -1208,6 +1223,77 @@ fn parse_proxmox_command(verb: &str, rest: &[&str]) -> Result<Command, CliError>
}
_ => Err(CliError { message: usage() }),
},
+ "start" | "stop" | "shutdown" | "reboot" => {
+ let mut account_id = None;
+ let mut node = None;
</file context>
| address: std::net::SocketAddr, | ||
| shutdown: Option<tokio::sync::oneshot::Sender<()>>, | ||
| /// The lifecycle harness's worker; aborted on drop. | ||
| _worker: Option<tokio::task::JoinHandle<()>>, |
There was a problem hiding this comment.
P3: The _worker field is never populated: both harness_with and lifecycle_harness construct Harness with _worker: None, and its doc comment claims it is "aborted on drop." The only real worker handle is the local _worker_handle in lifecycle_harness, which goes out of scope at the end of the constructor; dropping a tokio JoinHandle detaches the task, it does not abort it. Store the handle in _worker and abort it in Drop (which currently only shuts down the HTTP server), or remove the field and correct the comment.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/fleet-controller/tests/proxmox.rs, line 132:
<comment>The `_worker` field is never populated: both `harness_with` and `lifecycle_harness` construct `Harness` with `_worker: None`, and its doc comment claims it is "aborted on drop." The only real worker handle is the local `_worker_handle` in `lifecycle_harness`, which goes out of scope at the end of the constructor; dropping a tokio `JoinHandle` detaches the task, it does not abort it. Store the handle in `_worker` and abort it in `Drop` (which currently only shuts down the HTTP server), or remove the field and correct the comment.</comment>
<file context>
@@ -128,6 +128,8 @@ struct Harness {
address: std::net::SocketAddr,
shutdown: Option<tokio::sync::oneshot::Sender<()>>,
+ /// The lifecycle harness's worker; aborted on drop.
+ _worker: Option<tokio::task::JoinHandle<()>>,
}
</file context>
|
All 12 findings addressed in 19db6f4: P1s
P2s
P3s
|
There was a problem hiding this comment.
1 existing issue remains and 1 new issue found across 8 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/fleet-api/src/proxmox.rs">
<violation number="1" location="crates/fleet-api/src/proxmox.rs:1039">
P3: The new mismatch guard introduces a 400 branch (body `vmid` versus path `vmid`) but no test exercises it: the controller end-to-end suite covers the action-validation 400 and the ok/timeout/error task paths (per the PR description), and the fleet-api handlers have no unit test for this exact mismatch. Add one test asserting a mismatched body VMID yields 400 so the branch is pinned.</violation>
</file>
Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Re-trigger cubic
| let principal = crate::operations::principal_or_error(principal, correlation_id)?; | ||
| // The body's VMID must agree with the path's: two names for one guest | ||
| // is a malformed request, not a fallback. | ||
| if request.vmid != vmid { |
There was a problem hiding this comment.
P3: The new mismatch guard introduces a 400 branch (body vmid versus path vmid) but no test exercises it: the controller end-to-end suite covers the action-validation 400 and the ok/timeout/error task paths (per the PR description), and the fleet-api handlers have no unit test for this exact mismatch. Add one test asserting a mismatched body VMID yields 400 so the branch is pinned.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/fleet-api/src/proxmox.rs, line 1039:
<comment>The new mismatch guard introduces a 400 branch (body `vmid` versus path `vmid`) but no test exercises it: the controller end-to-end suite covers the action-validation 400 and the ok/timeout/error task paths (per the PR description), and the fleet-api handlers have no unit test for this exact mismatch. Add one test asserting a mismatched body VMID yields 400 so the branch is pinned.</comment>
<file context>
@@ -1034,6 +1034,14 @@ pub async fn start_proxmox_lifecycle(
let principal = crate::operations::principal_or_error(principal, correlation_id)?;
+ // The body's VMID must agree with the path's: two names for one guest
+ // is a malformed request, not a fallback.
+ if request.vmid != vmid {
+ return Err(crate::machines::invalid_request(
+ "the body's vmid does not match the path's guest",
</file context>
Closes #103 (FM-602).
Implementation approach (from the issue)
FM-600/FM-601 delivered the trust flow and the read layer; this PR adds the first Proxmox mutations as durable operations.
What landed
fleet-provider-proxmox):guest_lifecycle(start/stop/shutdown/reboot → the parsed UPID) andtask_statuson theProxmoxSourceport.Upid::parseis Fleet-owned (UPID:node:pid:pstart:starttime:type:id:user:— the node it polls comes from the parse, never from trust in the caller; malformed shapes refuse with bounded details).TaskStatusis an honest enum:Running/Ok/Error{detail}/Unknown— an unknown task is honest uncertainty, never assumed success (PVE rotates old task entries out).proxmox.guest.start|stop|shutdown|reboot): registered inCREATABLE_KINDS(31 → 35) with a newproxmox.operatepermission (catalog 38 → 39). The kinds are catalog-level like the source kinds — a Proxmox guest is not a Fleet machine, so the permission is enforced withresource: Noneon both the dedicated endpoint and the generic surface (the generic-surface routing previously required amachineIdfor any Some-permission kind; the catalog-level branch is now explicit inOperations::create, which also fixes the latent gap for the source kinds).fleet-controller/src/proxmox_exec.rs):ProxmoxLifecycleExecutor— resolves the account through the same explicit-trust gate (unconfirmed fingerprint refuses before any network call), runs the action, and polls the UPID on a fixed 2 s interval against a deadline (a sleep between polls, never a wall-clock race — the legacywaitForTaskflake is exactly what this avoids). Terminal mapping: taskOK→ succeeded;ERROR→ failed with the bounded detail; deadline expiry → failed naming that the final state is unknown; unreadable status → failed astask_unknown. Cancellation stops the waiting, not the remote task — PVE keeps running the action and the operation records that honestly; remote task cancellation is deferred to epic M6 epic: Template, clone, and snapshot operations #12's review.ProxmoxDispatchroutes the lifecycle kinds in the worker chain; a controller without a secret store composes an absent credential store so lifecycle operations fail honestly.POST /api/v1/proxmox/accounts/{id}/guests/{vmid}/{action}(202, durable operation, idempotency-key supported, malformed actions refuse as 400) in the OpenAPI doc (client regenerated);fleetctl proxmox start|stop|shutdown|reboot --account --node --vmid [--wait] [--timeout].proxmox.operateis checked at the API before the operation is created and re-applied at the account boundary in the executor.Tests
taskState: ok; task-ERROR → failed with the PVE detail surfaced; deadline expiry with an always-running task → failed naming the uncertainty; unrecognized action → 400.--wait/--timeout, and the refusals.cargo xtask verifypasses; live smoke remains on the epic's real-cluster checklist.Non-goals (respected)
Destructive operations (epic #12); guest OS provisioning; remote task cancellation.
Summary by cubic
Adds the first Proxmox mutations as durable operations: guest lifecycle start/stop/shutdown/reboot and UPID task-status polling, closing #103 (FM-602).
The new
proxmox.guest.start|stop|shutdown|rebootkinds are catalog-level like the source kinds — a Proxmox guest is not a Fleet machine — and gated by a newproxmox.operatepermission enforced at the API and re-applied at the account boundary in the executor.fleet-controller'sProxmoxLifecycleExecutorresolves the account through the same explicit-trust gate as the read layer, runs the action, and polls the task on a fixed 2-second interval against a deadline, avoiding the legacywaitForTaskwall-clock race. Terminal states are honest: task OK succeeds, ERROR fails with the PVE detail, and a deadline expiry or unknown status fails naming the uncertainty — never assumed success. Cancellation stops the waiting only; PVE keeps running the remote task and the operation records that honestly. A controller without a secret store composes an absent credential store so lifecycle operations fail cleanly.Surfaces the operations through
POST /api/v1/proxmox/accounts/{accountId}/guests/{vmid}/{action}(202, durable, idempotency-key supported, malformed actions 400) andfleetctl proxmox start|stop|shutdown|reboot --account --node --vmid [--wait] [--timeout]. Destructive operations and remote task cancellation are deferred to epic #12's review.methodfield so lifecycle mutations usePOSTwhile reads stayGET.Written for commit 19db6f4. Summary will update on new commits.