use std::fmt; /// A sandbox row's lifecycle, stored in `sandboxes.state`. A cleanup that owns /// the row is not a lifecycle state: the row records it apart, as its /// [`CleanupState`], over the teardown transition its intent names, and /// [`SandboxStatus`] carries the two together. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SandboxState { Creating, Created, Starting, Started, Stopping, Stopped, Deleting, Pausing, Paused, Resuming, Checkpointing, Suspending, Suspended, Restoring, Quarantined, Failed, } /// A cleanup that owns a sandbox row, stored in `sandboxes.cleanup_state` /// over the row's lifecycle. It is not a lifecycle state. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CleanupState { Required, Failed, } impl CleanupState { pub const fn as_str(self) -> &'static str { match self { Self::Required => "required", Self::Failed => "failed", } } /// The cleanup a `sandboxes.cleanup_state` value names. pub(crate) fn from_column(value: &str) -> Option { match value { "required" => Some(Self::Required), "failed" => Some(Self::Failed), _ => None, } } } impl SandboxState { /// Wait completion is not evidence that a VM or its resources were removed. /// Quarantine completes the wait while preserving uncertain resources. pub const fn completes_wait(self) -> bool { matches!( self, Self::Stopped | Self::Failed | Self::Suspended | Self::Quarantined ) } /// The RPC-admission machine, over lifecycle states. A row a cleanup owns /// admits only Delete and quarantine ([`SandboxStatus::can_transition_to`]), /// and a cleanup moving between required and failed is a cleanup edge /// (`db::edges::CLEANUP_EDGES`), not a lifecycle transition. pub(crate) fn can_transition_to(&self, target: SandboxState) -> bool { if target == SandboxState::Quarantined { return *self != SandboxState::Failed; } use SandboxState::*; matches!( (self, target), // Create/Delete lifecycle (Creating, Created) | (Created, Starting) | (Created, Deleting) // Start/Stop lifecycle | (Starting, Started) | (Started, Stopping) | (Stopping, Stopped) | (Stopped, Starting) | (Stopped, Deleting) // Pause/Resume lifecycle | (Started, Pausing) | (Pausing, Paused) | (Paused, Resuming) | (Resuming, Started) | (Paused, Stopping) // stop/kill from paused // Checkpoint lifecycle (VM keeps running, or from paused for migration) | (Started, Checkpointing) | (Paused, Checkpointing) | (Checkpointing, Started) | (Checkpointing, Paused) // Suspend lifecycle (snapshot + stop) | (Started, Suspending) // auto-pause + snapshot + kill | (Paused, Suspending) // snapshot + kill | (Suspending, Suspended) // Restore via Start (suspended -> starting -> started) | (Suspended, Starting) // Start(checkpoint=...) or fresh start | (Suspended, Restoring) // internal: Start routes through Restoring | (Restoring, Started) | (Restoring, Paused) // restore-to-paused (drain lifecycle-preserving evacuation) | (Suspended, Deleting) // delete snapshot + rootfs // Delete from non-transient states not yet covered above | (Started, Deleting) | (Paused, Deleting) // Delete from transient states (force-delete while operation in progress) | (Starting, Deleting) | (Stopping, Deleting) | (Resuming, Deleting) | (Pausing, Deleting) | (Suspending, Deleting) | (Restoring, Deleting) | (Checkpointing, Deleting) // Recovery | (Failed, Deleting) | (Quarantined, Deleting) ) } pub fn transition(self, target: SandboxState) -> Result { if self.can_transition_to(target) { Ok(target) } else if self.is_transient() { Err(StateTransitionError::Transient { action: target.action_name(), state: self.into(), operation: self.operation_name(), }) } else { Err(StateTransitionError::Invalid { action: target.action_name(), state: self.into(), required: target.required_states(), }) } } /// Whether a host-control caller matching this row's registered /// `(uid, netgtw_pid)` may be authorized as the sandbox's net-gateway. /// /// True only for the launch-through-teardown window in which the gateway /// sidecar is legitimately a live, policy-pulling process (launches happen /// in Starting/Restoring; the gateway stays resident across pause/resume /// and is killed during stop/suspend teardown). Outside that window the /// registration is stale or condemned — failure-marked rows keep the /// `(uid, netgtw_pid)` of processes cleanup already killed, and because /// uids are slot-derived and pids recycle, a stale registration can /// collide with a later sandbox's gateway and serve it the wrong policy. /// Deleting/cleanup states are excluded even though the process may /// briefly outlive the transition: a condemned gateway must not be served /// policy. pub fn can_authorize_net_gateway(&self) -> bool { use SandboxState::*; match self { Starting | Started | Stopping | Pausing | Paused | Resuming | Checkpointing | Suspending | Restoring => true, Creating | Created | Stopped | Suspended | Deleting | Quarantined | Failed => false, } } /// Whether the VM runtime may be resident outside any suspend, so a /// suspend backup that becomes durable now doesn't make the sandbox /// suspended. A suspend rolled back to `Paused` lands here, as does a /// sandbox started or restored since the capture. So does `Quarantined`: /// quarantine preserves resources it can't account for, a running VM /// included, and nothing settles them until an operator or a recovery /// does. `Suspending` and `Suspended` answer `false`, because whether a /// durable capture is theirs depends on the artifact they pin, not on the /// state. The remaining states never ran a VM, released it, or hand it to /// a cleanup of their own. pub fn holds_runtime_outside_suspend(self) -> bool { use SandboxState::*; match self { Starting | Started | Stopping | Pausing | Paused | Resuming | Checkpointing | Restoring | Quarantined => true, Creating | Created | Stopped | Deleting | Suspending | Suspended | Failed => false, } } /// Both `Started` and `Paused` sandboxes may have associated network gateways. pub const fn can_cycle_net_gateway(self) -> bool { matches!(self, Self::Started | Self::Paused) } /// Whether this sandbox still holds an on-disk `instance` subvolume whose /// btrfs parent lineage points at its template build — the reference that /// the incremental suspend codec (`btrfs send -p