Skip to main content

arcbox_vm_driver/
capability.rs

1//! What a handle *may* be able to do, one small trait per capability.
2//!
3//! A handle exposes each of these through an `Option<&dyn Cap>` accessor
4//! (`VmHandle::vsock`, `VmHandle::checkpoint`, ...). `None` is the answer for
5//! "this driver cannot" *and* for "this VM was not built with the device";
6//! a method on one of these traits never returns an "unsupported" error as
7//! its normal answer. [`crate::DriverCapabilities`] says which accessors a
8//! driver's handles can ever return `Some` for.
9
10use std::fmt;
11use std::path::{Path, PathBuf};
12
13use async_trait::async_trait;
14use serde::{Deserialize, Serialize};
15
16use crate::driver::{ExitStatus, RestoreSpec, VmHandle, VmRecord, VsockConn};
17use crate::error::Result;
18use crate::spec::{IsolationSpec, VmId, VmSpec};
19
20/// Dial a vsock port inside the guest.
21#[async_trait]
22pub trait Vsock: Send + Sync {
23    /// Connects to `port` in the guest and returns the stream.
24    ///
25    /// Fails with [`crate::Error::WrongState`] when the VM is not running,
26    /// and with [`crate::Error::Io`] of kind
27    /// [`ConnectionRefused`](std::io::ErrorKind::ConnectionRefused) when
28    /// the guest has no listener on `port` yet — the one outcome a caller
29    /// retries on; every other error is final.
30    async fn dial(&self, port: u32) -> Result<VsockConn>;
31}
32
33/// Spawn the VMM before there is a guest to boot.
34///
35/// External-process VMMs pay for the process spawn, the jailer setup, and
36/// the API socket up front. A warm pool prepares its slots long before a
37/// boot is asked for, journals the pid before any guest runs, and binds the
38/// READY vsock listener while nothing can race it. The split is invisible
39/// to a plain boot: `VmDriver::boot(spec, dir)` MUST behave exactly like
40/// `prepare(&spec.id, &spec.isolation, dir)` followed by
41/// [`PreparedVm::boot`] with `spec`, and `restore` likewise — the contract
42/// asserts it. Present iff [`crate::DriverCapabilities::prepare`].
43#[async_trait]
44pub trait Prepare: Send + Sync {
45    /// Spawns the VMM process for `id` under `isolation`, with
46    /// `runtime_dir` as its private scratch space, without booting a guest.
47    async fn prepare(
48        &self,
49        id: &VmId,
50        isolation: &IsolationSpec,
51        runtime_dir: &Path,
52    ) -> Result<Box<dyn PreparedVm>>;
53}
54
55/// A spawned VMM waiting for a spec.
56///
57/// Dropping it kills the process unless a `boot` or `restore` succeeded,
58/// after which the returned handle owns it.
59#[async_trait]
60pub trait PreparedVm: Send + Sync {
61    /// The VM's identity.
62    fn id(&self) -> &VmId;
63
64    /// The durable record — pid and API socket are already known, and the
65    /// handle that `boot`/`restore` returns reports this same record.
66    fn record(&self) -> VmRecord;
67
68    /// `true` while the VMM process is up. Never signals a reaped pid: once
69    /// this answers `false` it stays `false`.
70    fn alive(&self) -> bool;
71
72    /// Bind a vsock listener now; it is live before the guest starts, so a
73    /// dial-out the guest makes at boot cannot be missed. `Some` iff
74    /// [`crate::DriverCapabilities::vsock_listen`].
75    fn vsock_listener(&self) -> Option<&dyn VsockListen> {
76        None
77    }
78
79    /// Bring the files this VM boots from into the area its VMM can reach.
80    /// `Some` iff [`crate::DriverCapabilities::staging`]. The handle of the
81    /// VM booted on this process reaches the same area through
82    /// [`VmHandle::staging`].
83    fn staging(&self) -> Option<&dyn Staging> {
84        None
85    }
86
87    /// Boots `spec` on this process. `spec.id` and `spec.isolation` must
88    /// equal what `prepare` was given ([`crate::Error::InvalidSpec`]
89    /// otherwise); the handle shares this process. At most one `boot` or
90    /// `restore` succeeds per prepared VM.
91    async fn boot(&self, spec: VmSpec) -> Result<Box<dyn VmHandle>>;
92
93    /// Boots a checkpoint on this process; the same rules as [`boot`](Self::boot).
94    async fn restore(
95        &self,
96        image: &CheckpointImage,
97        spec: RestoreSpec,
98    ) -> Result<Box<dyn VmHandle>>;
99
100    /// Kills and reaps the process now. Idempotent, and valid after a boot
101    /// too (it kills that VM); the status is how the process ended.
102    async fn discard(&self) -> Result<ExitStatus>;
103}
104
105/// Bring the files a VM boots from into the private area its VMM can
106/// reach.
107///
108/// A confined VMM — Firecracker under the jailer, chrooted into a per-VM
109/// directory — can open nothing outside that area, so its kernel, its
110/// disks, and a checkpoint's files have to be inside before the VM is told
111/// about them. *Which* files those are is the orchestrator's business;
112/// where they must be, and how they get there (a hard link, a private
113/// copy, a device node), is the driver's. This capability is that split:
114/// the caller says what a file is, and gets back the path its spec must
115/// name for it. It never learns whether a confinement exists.
116///
117/// Present iff [`crate::DriverCapabilities::staging`], and reachable from
118/// both grips on a VM — the prepared VM the driver spawned
119/// ([`PreparedVm::staging`]) and the handle to the VM running on it
120/// ([`VmHandle::staging`]) — naming the same area either way. That is what
121/// lets a VM this driver *adopted*, whose prepared half died with the
122/// process that made it, still take a disk back out. A driver whose VMs
123/// read host paths as they are still offers it, as the identity: staging
124/// such a file is a no-op that answers with the path it was given. That is
125/// what lets an orchestrator stage unconditionally instead of branching on
126/// a confinement it is not supposed to know about.
127///
128/// Staging a file that is already in the VM's area leaves it alone and
129/// names it where it is — so a caller may stage what an earlier stage, or
130/// a warm slot it claimed, already put there.
131///
132/// Everything staged belongs to the VM's area, so a disk that has to
133/// outlive the VM is taken back out with
134/// [`unstage_disk`](Self::unstage_disk) first. When that area is removed,
135/// and by which verb, is the driver's own business — it is not
136/// [`PreparedVm::discard`] on every adapter today, so a caller may assume
137/// neither that a discard took the area with it nor that it left the area
138/// standing.
139#[async_trait]
140pub trait Staging: Send + Sync {
141    /// Stages `src` as this VM's kernel image, and returns the path the
142    /// spec's [`BootSpec::Kernel`](crate::BootSpec::Kernel) must name.
143    async fn stage_kernel(&self, src: &Path) -> Result<PathBuf>;
144
145    /// Stages `source` as this VM's disk `id`, and returns the path the
146    /// [`DiskSpec`](crate::DiskSpec) carrying that id must name.
147    ///
148    /// `id` is the disk id the spec will carry, not a file name, and a
149    /// plain one — the same rule [`DiskSpec::id`](crate::DiskSpec) is held
150    /// to, refused rather than resolved by an adapter, since staging
151    /// writes and replaces at the name it makes from it. A driver that
152    /// records disk paths in its checkpoints (Firecracker does) records
153    /// the name it staged the disk under, and a restore reproduces that
154    /// name from the same id — so a disk staged under one id and specced
155    /// under another is a checkpoint that will not restore.
156    async fn stage_disk(&self, id: &str, source: DiskSource<'_>) -> Result<PathBuf>;
157
158    /// Moves this VM's disk `id` back out to `dst`, so it survives the VM.
159    ///
160    /// The disk is gone from the VM's area once this returns; putting it
161    /// back into a later VM is [`DiskSource::Handover`]. `Ok(false)` means
162    /// there was no such disk staged — for a driver that stages nothing,
163    /// or a disk that was never brought in — which is an outcome, not a
164    /// failure.
165    ///
166    /// The VM must not be writing to the disk. A caller takes a disk out
167    /// of a VM it is about to discard, and the guest is already stopped or
168    /// [`Quiesced`](crate::VmState::Quiesced) by then.
169    ///
170    /// `id` is a disk id, held to the same rule as
171    /// [`DiskSpec::id`](crate::DiskSpec) — a plain name. An adapter
172    /// refuses anything else rather than resolving it against its own
173    /// layout, because this verb *moves a file*.
174    async fn unstage_disk(&self, id: &str, dst: &Path) -> Result<bool>;
175
176    /// Stages `image`'s files, and returns the image as this VM must name
177    /// it for [`PreparedVm::restore`].
178    ///
179    /// A warm pool stages a checkpoint into a slot long before any restore
180    /// is asked for, and that — not the confinement — is what makes this a
181    /// verb of its own: the memory file is the guest's entire RAM, so
182    /// bringing it in is the expensive half of a restore, and a slot that
183    /// paid for it up front restores in milliseconds.
184    async fn stage_checkpoint(&self, image: &CheckpointImage) -> Result<CheckpointImage>;
185}
186
187/// What a disk being staged is — which decides what the VM gets, and what
188/// happens to the caller's own copy.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum DiskSource<'a> {
191    /// A block device — a device-mapper volume, a loop device — that the
192    /// VM must open as one. Mirrored, never copied.
193    Device(&'a Path),
194    /// A file the caller keeps: the VM gets a private copy, so guest
195    /// writes never reach the original.
196    Image(&'a Path),
197    /// A file the caller gives up: it is moved in, and no longer exists at
198    /// the source once staging returns. The counterpart of
199    /// [`Staging::unstage_disk`] — the disk of a discarded VM, parked
200    /// outside, coming back into a fresh one.
201    Handover(&'a Path),
202}
203
204impl<'a> DiskSource<'a> {
205    /// The host path this source names.
206    pub fn path(&self) -> &'a Path {
207        match self {
208            Self::Device(path) | Self::Image(path) | Self::Handover(path) => path,
209        }
210    }
211}
212
213/// Accept vsock connections the guest initiates (the READY dial-out).
214#[async_trait]
215pub trait VsockListen: Send + Sync {
216    /// Starts listening on host-side `port`; the guest connects to it.
217    async fn listen(&self, port: u32) -> Result<Box<dyn VsockListener>>;
218}
219
220/// A bound vsock listener; one guest connection per `accept`.
221#[async_trait]
222pub trait VsockListener: Send {
223    /// Waits for the next guest connection.
224    ///
225    /// Fails once the VM has exited — nothing more can arrive.
226    async fn accept(&mut self) -> Result<VsockConn>;
227}
228
229/// Capture the VM's state to disk.
230///
231/// Quiescing is this capability's own business: the driver freezes the guest
232/// for the capture and, per [`CheckpointOptions::after`], either resumes it
233/// or leaves it [`crate::VmState::Quiesced`]. There is no pause verb on the
234/// handle.
235#[async_trait]
236pub trait Checkpoint: Send + Sync {
237    /// Writes a checkpoint of the VM into `dst` (created if missing).
238    async fn checkpoint(&self, dst: &Path, opts: CheckpointOptions) -> Result<CheckpointImage>;
239}
240
241/// How to take a checkpoint.
242#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
243pub struct CheckpointOptions {
244    /// What the guest does once the capture is on disk.
245    pub after: AfterCheckpoint,
246    /// Full image or incremental.
247    pub kind: CheckpointKind,
248}
249
250/// What the guest does once a checkpoint is captured.
251#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
252pub enum AfterCheckpoint {
253    /// Keep running.
254    #[default]
255    Resume,
256    /// Stay frozen; the handle reports [`crate::VmState::Quiesced`] until it
257    /// is shut down. This is how an orchestrator implements "pause": hold,
258    /// then `shutdown(Kill)`; "resume" is a restore.
259    HoldQuiesced,
260}
261
262/// Full or incremental capture.
263#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
264#[serde(rename_all = "snake_case")]
265pub enum CheckpointKind {
266    /// Everything: memory and device state.
267    #[default]
268    Full,
269    /// Only pages dirtied since the previous checkpoint (needs
270    /// [`crate::VmSpec::dirty_tracking`] and a driver whose
271    /// [`crate::DriverCapabilities::diff_checkpoint`] is set).
272    Diff,
273}
274
275/// A checkpoint on disk: where it is and who can read it.
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277pub struct CheckpointImage {
278    /// The directory holding the image files.
279    pub dir: PathBuf,
280    /// The on-disk format, named by the driver that wrote it.
281    pub format: CheckpointFormat,
282    /// Full or incremental.
283    pub kind: CheckpointKind,
284}
285
286/// A checkpoint's on-disk format, e.g. `firecracker/v1`.
287///
288/// A driver refuses to restore a format it did not write
289/// ([`crate::Error::ForeignCheckpoint`]); the name is how it tells.
290#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
291#[serde(transparent)]
292pub struct CheckpointFormat(String);
293
294impl CheckpointFormat {
295    /// Names a format.
296    pub fn new(name: impl Into<String>) -> Self {
297        Self(name.into())
298    }
299
300    /// The format name.
301    pub fn as_str(&self) -> &str {
302        &self.0
303    }
304}
305
306impl fmt::Display for CheckpointFormat {
307    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308        f.write_str(&self.0)
309    }
310}
311
312impl AsRef<str> for CheckpointFormat {
313    fn as_ref(&self) -> &str {
314        &self.0
315    }
316}
317
318/// Find a VM that outlived the process which booted it — or clear away what
319/// it left when it did not.
320///
321/// Present on external-process VMMs only: an in-process VM dies with its
322/// process, so "adopt" has no meaning there and the accessor is `None`.
323#[async_trait]
324pub trait Adopt: Send + Sync {
325    /// Rebuilds a handle for the VM `record` names.
326    ///
327    /// `Ok(None)` means nothing survived — a stale record, a dead pid — which
328    /// is an outcome, not an absence of the capability.
329    ///
330    /// The handle is rebuilt from what the record says, not from what the
331    /// live process can be made to admit: a VMM's control socket and the
332    /// jail it is confined to
333    /// ([`ProcessRecord::api_socket`](crate::ProcessRecord::api_socket),
334    /// [`JailRecord`](crate::JailRecord)) are both invisible from outside
335    /// once it is running. A record that carries neither — one written
336    /// before they existed — is adopted as best the driver can, which for a
337    /// jailed VM is a handle that can only kill it.
338    async fn adopt(&self, record: &VmRecord) -> Result<Option<Box<dyn VmHandle>>>;
339
340    /// Removes the host area of the VM `record` names, which is gone.
341    ///
342    /// A VM's area belongs to whichever grip owns its VMM: the
343    /// [`PreparedVm`] that made it, or the handle of a VM this driver
344    /// adopted. A VM that died with the process which booted it leaves
345    /// neither, and leaves its area — which, where a driver confines its
346    /// VMMs, holds everything [`Staging`] brought in. This is the only
347    /// route left to it, and only the driver knows where "it" is.
348    ///
349    /// `isolation` is what the VM was configured to run under, because
350    /// nothing can be read back off a process that is gone. A driver that
351    /// confines nothing has no area to remove and succeeds. Where the
352    /// record names the area itself
353    /// ([`JailRecord`](crate::JailRecord)) that is the better answer —
354    /// `isolation` describes what *this* process would have built, which is
355    /// the same area only while the configuration has not moved under it.
356    ///
357    /// Already removed is success: a startup sweep runs this on every
358    /// journal it does not keep, and may find what an earlier sweep
359    /// cleared.
360    ///
361    /// Never called on a VM that is still running — [`adopt`](Self::adopt)
362    /// answers that case with a handle, and the handle's stop takes the
363    /// area.
364    async fn discard_area(&self, record: &VmRecord, isolation: &IsolationSpec) -> Result<()>;
365}
366
367/// Give up ownership of a running VM without stopping it.
368///
369/// After `detach`, dropping the handle no longer kills the VM; the returned
370/// record is what [`Adopt::adopt`] takes to pick it up again. Present iff
371/// the driver has [`Adopt`].
372#[async_trait]
373pub trait Detach: Send + Sync {
374    /// Releases the VM from this handle and returns its durable record.
375    async fn detach(&self) -> Result<VmRecord>;
376}
377
378/// The guest memory balloon.
379#[async_trait]
380pub trait Balloon: Send + Sync {
381    /// Asks the guest to give back memory until it holds `bytes`.
382    async fn set_target(&self, bytes: u64) -> Result<()>;
383    /// What the balloon was told and what it reports.
384    async fn stats(&self) -> Result<BalloonStats>;
385}
386
387/// The balloon's target and, when the guest reports it, its actual size.
388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
389pub struct BalloonStats {
390    /// The most recent target, in bytes.
391    pub target_bytes: u64,
392    /// The size the guest reports, in bytes, when it does.
393    pub current_bytes: Option<u64>,
394}
395
396/// The guest console's output.
397#[async_trait]
398pub trait Console: Send + Sync {
399    /// Takes up to `max_bytes` of console output not yet read.
400    async fn read_output(&self, max_bytes: usize) -> Result<Vec<u8>>;
401}
402
403/// A point-in-time view of the VM's internals for diagnostics.
404pub trait DebugSnapshot: Send + Sync {
405    /// The driver's own JSON — queues, counters, whatever helps a post-mortem.
406    fn snapshot(&self) -> serde_json::Value;
407}