arcbox-vm-driver 0.8.0

The VM driver port: the vocabulary of a VM on this host — spec, driver/handle traits, capabilities, guest-network port — that VMM adapters implement and orchestrators consume.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
//! What a handle *may* be able to do, one small trait per capability.
//!
//! A handle exposes each of these through an `Option<&dyn Cap>` accessor
//! (`VmHandle::vsock`, `VmHandle::checkpoint`, ...). `None` is the answer for
//! "this driver cannot" *and* for "this VM was not built with the device";
//! a method on one of these traits never returns an "unsupported" error as
//! its normal answer. [`crate::DriverCapabilities`] says which accessors a
//! driver's handles can ever return `Some` for.

use std::fmt;
use std::path::{Path, PathBuf};

use async_trait::async_trait;
use serde::{Deserialize, Serialize};

use crate::driver::{ExitStatus, RestoreSpec, VmHandle, VmRecord, VsockConn};
use crate::error::Result;
use crate::spec::{IsolationSpec, VmId, VmSpec};

/// Dial a vsock port inside the guest.
#[async_trait]
pub trait Vsock: Send + Sync {
    /// Connects to `port` in the guest and returns the stream.
    ///
    /// Fails with [`crate::Error::WrongState`] when the VM is not running,
    /// and with [`crate::Error::Io`] of kind
    /// [`ConnectionRefused`](std::io::ErrorKind::ConnectionRefused) when
    /// the guest has no listener on `port` yet — the one outcome a caller
    /// retries on; every other error is final.
    async fn dial(&self, port: u32) -> Result<VsockConn>;
}

/// Spawn the VMM before there is a guest to boot.
///
/// External-process VMMs pay for the process spawn, the jailer setup, and
/// the API socket up front. A warm pool prepares its slots long before a
/// boot is asked for, journals the pid before any guest runs, and binds the
/// READY vsock listener while nothing can race it. The split is invisible
/// to a plain boot: `VmDriver::boot(spec, dir)` MUST behave exactly like
/// `prepare(&spec.id, &spec.isolation, dir)` followed by
/// [`PreparedVm::boot`] with `spec`, and `restore` likewise — the contract
/// asserts it. Present iff [`crate::DriverCapabilities::prepare`].
#[async_trait]
pub trait Prepare: Send + Sync {
    /// Spawns the VMM process for `id` under `isolation`, with
    /// `runtime_dir` as its private scratch space, without booting a guest.
    async fn prepare(
        &self,
        id: &VmId,
        isolation: &IsolationSpec,
        runtime_dir: &Path,
    ) -> Result<Box<dyn PreparedVm>>;
}

/// A spawned VMM waiting for a spec.
///
/// Dropping it kills the process unless a `boot` or `restore` succeeded,
/// after which the returned handle owns it.
#[async_trait]
pub trait PreparedVm: Send + Sync {
    /// The VM's identity.
    fn id(&self) -> &VmId;

    /// The durable record — pid and API socket are already known, and the
    /// handle that `boot`/`restore` returns reports this same record.
    fn record(&self) -> VmRecord;

    /// `true` while the VMM process is up. Never signals a reaped pid: once
    /// this answers `false` it stays `false`.
    fn alive(&self) -> bool;

    /// Bind a vsock listener now; it is live before the guest starts, so a
    /// dial-out the guest makes at boot cannot be missed. `Some` iff
    /// [`crate::DriverCapabilities::vsock_listen`].
    fn vsock_listener(&self) -> Option<&dyn VsockListen> {
        None
    }

    /// Bring the files this VM boots from into the area its VMM can reach.
    /// `Some` iff [`crate::DriverCapabilities::staging`]. The handle of the
    /// VM booted on this process reaches the same area through
    /// [`VmHandle::staging`].
    fn staging(&self) -> Option<&dyn Staging> {
        None
    }

    /// Boots `spec` on this process. `spec.id` and `spec.isolation` must
    /// equal what `prepare` was given ([`crate::Error::InvalidSpec`]
    /// otherwise); the handle shares this process. At most one `boot` or
    /// `restore` succeeds per prepared VM.
    async fn boot(&self, spec: VmSpec) -> Result<Box<dyn VmHandle>>;

    /// Boots a checkpoint on this process; the same rules as [`boot`](Self::boot).
    async fn restore(
        &self,
        image: &CheckpointImage,
        spec: RestoreSpec,
    ) -> Result<Box<dyn VmHandle>>;

    /// Kills and reaps the process now. Idempotent, and valid after a boot
    /// too (it kills that VM); the status is how the process ended.
    async fn discard(&self) -> Result<ExitStatus>;
}

/// Bring the files a VM boots from into the private area its VMM can
/// reach.
///
/// A confined VMM — Firecracker under the jailer, chrooted into a per-VM
/// directory — can open nothing outside that area, so its kernel, its
/// disks, and a checkpoint's files have to be inside before the VM is told
/// about them. *Which* files those are is the orchestrator's business;
/// where they must be, and how they get there (a hard link, a private
/// copy, a device node), is the driver's. This capability is that split:
/// the caller says what a file is, and gets back the path its spec must
/// name for it. It never learns whether a confinement exists.
///
/// Present iff [`crate::DriverCapabilities::staging`], and reachable from
/// both grips on a VM — the prepared VM the driver spawned
/// ([`PreparedVm::staging`]) and the handle to the VM running on it
/// ([`VmHandle::staging`]) — naming the same area either way. That is what
/// lets a VM this driver *adopted*, whose prepared half died with the
/// process that made it, still take a disk back out. A driver whose VMs
/// read host paths as they are still offers it, as the identity: staging
/// such a file is a no-op that answers with the path it was given. That is
/// what lets an orchestrator stage unconditionally instead of branching on
/// a confinement it is not supposed to know about.
///
/// Staging a file that is already in the VM's area leaves it alone and
/// names it where it is — so a caller may stage what an earlier stage, or
/// a warm slot it claimed, already put there.
///
/// Everything staged belongs to the VM's area, so a disk that has to
/// outlive the VM is taken back out with
/// [`unstage_disk`](Self::unstage_disk) first. When that area is removed,
/// and by which verb, is the driver's own business — it is not
/// [`PreparedVm::discard`] on every adapter today, so a caller may assume
/// neither that a discard took the area with it nor that it left the area
/// standing.
#[async_trait]
pub trait Staging: Send + Sync {
    /// Stages `src` as this VM's kernel image, and returns the path the
    /// spec's [`BootSpec::Kernel`](crate::BootSpec::Kernel) must name.
    async fn stage_kernel(&self, src: &Path) -> Result<PathBuf>;

    /// Stages `source` as this VM's disk `id`, and returns the path the
    /// [`DiskSpec`](crate::DiskSpec) carrying that id must name.
    ///
    /// `id` is the disk id the spec will carry, not a file name, and a
    /// plain one — the same rule [`DiskSpec::id`](crate::DiskSpec) is held
    /// to, refused rather than resolved by an adapter, since staging
    /// writes and replaces at the name it makes from it. A driver that
    /// records disk paths in its checkpoints (Firecracker does) records
    /// the name it staged the disk under, and a restore reproduces that
    /// name from the same id — so a disk staged under one id and specced
    /// under another is a checkpoint that will not restore.
    async fn stage_disk(&self, id: &str, source: DiskSource<'_>) -> Result<PathBuf>;

    /// Moves this VM's disk `id` back out to `dst`, so it survives the VM.
    ///
    /// The disk is gone from the VM's area once this returns; putting it
    /// back into a later VM is [`DiskSource::Handover`]. `Ok(false)` means
    /// there was no such disk staged — for a driver that stages nothing,
    /// or a disk that was never brought in — which is an outcome, not a
    /// failure.
    ///
    /// The VM must not be writing to the disk. A caller takes a disk out
    /// of a VM it is about to discard, and the guest is already stopped or
    /// [`Quiesced`](crate::VmState::Quiesced) by then.
    ///
    /// `id` is a disk id, held to the same rule as
    /// [`DiskSpec::id`](crate::DiskSpec) — a plain name. An adapter
    /// refuses anything else rather than resolving it against its own
    /// layout, because this verb *moves a file*.
    async fn unstage_disk(&self, id: &str, dst: &Path) -> Result<bool>;

    /// Stages `image`'s files, and returns the image as this VM must name
    /// it for [`PreparedVm::restore`].
    ///
    /// A warm pool stages a checkpoint into a slot long before any restore
    /// is asked for, and that — not the confinement — is what makes this a
    /// verb of its own: the memory file is the guest's entire RAM, so
    /// bringing it in is the expensive half of a restore, and a slot that
    /// paid for it up front restores in milliseconds.
    async fn stage_checkpoint(&self, image: &CheckpointImage) -> Result<CheckpointImage>;
}

/// What a disk being staged is — which decides what the VM gets, and what
/// happens to the caller's own copy.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiskSource<'a> {
    /// A block device — a device-mapper volume, a loop device — that the
    /// VM must open as one. Mirrored, never copied.
    Device(&'a Path),
    /// A file the caller keeps: the VM gets a private copy, so guest
    /// writes never reach the original.
    Image(&'a Path),
    /// A file the caller gives up: it is moved in, and no longer exists at
    /// the source once staging returns. The counterpart of
    /// [`Staging::unstage_disk`] — the disk of a discarded VM, parked
    /// outside, coming back into a fresh one.
    Handover(&'a Path),
}

impl<'a> DiskSource<'a> {
    /// The host path this source names.
    pub fn path(&self) -> &'a Path {
        match self {
            Self::Device(path) | Self::Image(path) | Self::Handover(path) => path,
        }
    }
}

/// Accept vsock connections the guest initiates (the READY dial-out).
#[async_trait]
pub trait VsockListen: Send + Sync {
    /// Starts listening on host-side `port`; the guest connects to it.
    async fn listen(&self, port: u32) -> Result<Box<dyn VsockListener>>;
}

/// A bound vsock listener; one guest connection per `accept`.
#[async_trait]
pub trait VsockListener: Send {
    /// Waits for the next guest connection.
    ///
    /// Fails once the VM has exited — nothing more can arrive.
    async fn accept(&mut self) -> Result<VsockConn>;
}

/// Capture the VM's state to disk.
///
/// Quiescing is this capability's own business: the driver freezes the guest
/// for the capture and, per [`CheckpointOptions::after`], either resumes it
/// or leaves it [`crate::VmState::Quiesced`]. There is no pause verb on the
/// handle.
#[async_trait]
pub trait Checkpoint: Send + Sync {
    /// Writes a checkpoint of the VM into `dst` (created if missing).
    async fn checkpoint(&self, dst: &Path, opts: CheckpointOptions) -> Result<CheckpointImage>;
}

/// How to take a checkpoint.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CheckpointOptions {
    /// What the guest does once the capture is on disk.
    pub after: AfterCheckpoint,
    /// Full image or incremental.
    pub kind: CheckpointKind,
}

/// What the guest does once a checkpoint is captured.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum AfterCheckpoint {
    /// Keep running.
    #[default]
    Resume,
    /// Stay frozen; the handle reports [`crate::VmState::Quiesced`] until it
    /// is shut down. This is how an orchestrator implements "pause": hold,
    /// then `shutdown(Kill)`; "resume" is a restore.
    HoldQuiesced,
}

/// Full or incremental capture.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CheckpointKind {
    /// Everything: memory and device state.
    #[default]
    Full,
    /// Only pages dirtied since the previous checkpoint (needs
    /// [`crate::VmSpec::dirty_tracking`] and a driver whose
    /// [`crate::DriverCapabilities::diff_checkpoint`] is set).
    Diff,
}

/// A checkpoint on disk: where it is and who can read it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckpointImage {
    /// The directory holding the image files.
    pub dir: PathBuf,
    /// The on-disk format, named by the driver that wrote it.
    pub format: CheckpointFormat,
    /// Full or incremental.
    pub kind: CheckpointKind,
}

/// A checkpoint's on-disk format, e.g. `firecracker/v1`.
///
/// A driver refuses to restore a format it did not write
/// ([`crate::Error::ForeignCheckpoint`]); the name is how it tells.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CheckpointFormat(String);

impl CheckpointFormat {
    /// Names a format.
    pub fn new(name: impl Into<String>) -> Self {
        Self(name.into())
    }

    /// The format name.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for CheckpointFormat {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl AsRef<str> for CheckpointFormat {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

/// Find a VM that outlived the process which booted it — or clear away what
/// it left when it did not.
///
/// Present on external-process VMMs only: an in-process VM dies with its
/// process, so "adopt" has no meaning there and the accessor is `None`.
#[async_trait]
pub trait Adopt: Send + Sync {
    /// Rebuilds a handle for the VM `record` names.
    ///
    /// `Ok(None)` means nothing survived — a stale record, a dead pid — which
    /// is an outcome, not an absence of the capability.
    ///
    /// The handle is rebuilt from what the record says, not from what the
    /// live process can be made to admit: a VMM's control socket and the
    /// jail it is confined to
    /// ([`ProcessRecord::api_socket`](crate::ProcessRecord::api_socket),
    /// [`JailRecord`](crate::JailRecord)) are both invisible from outside
    /// once it is running. A record that carries neither — one written
    /// before they existed — is adopted as best the driver can, which for a
    /// jailed VM is a handle that can only kill it.
    async fn adopt(&self, record: &VmRecord) -> Result<Option<Box<dyn VmHandle>>>;

    /// Removes the host area of the VM `record` names, which is gone.
    ///
    /// A VM's area belongs to whichever grip owns its VMM: the
    /// [`PreparedVm`] that made it, or the handle of a VM this driver
    /// adopted. A VM that died with the process which booted it leaves
    /// neither, and leaves its area — which, where a driver confines its
    /// VMMs, holds everything [`Staging`] brought in. This is the only
    /// route left to it, and only the driver knows where "it" is.
    ///
    /// `isolation` is what the VM was configured to run under, because
    /// nothing can be read back off a process that is gone. A driver that
    /// confines nothing has no area to remove and succeeds. Where the
    /// record names the area itself
    /// ([`JailRecord`](crate::JailRecord)) that is the better answer —
    /// `isolation` describes what *this* process would have built, which is
    /// the same area only while the configuration has not moved under it.
    ///
    /// Already removed is success: a startup sweep runs this on every
    /// journal it does not keep, and may find what an earlier sweep
    /// cleared.
    ///
    /// Never called on a VM that is still running — [`adopt`](Self::adopt)
    /// answers that case with a handle, and the handle's stop takes the
    /// area.
    async fn discard_area(&self, record: &VmRecord, isolation: &IsolationSpec) -> Result<()>;
}

/// Give up ownership of a running VM without stopping it.
///
/// After `detach`, dropping the handle no longer kills the VM; the returned
/// record is what [`Adopt::adopt`] takes to pick it up again. Present iff
/// the driver has [`Adopt`].
#[async_trait]
pub trait Detach: Send + Sync {
    /// Releases the VM from this handle and returns its durable record.
    async fn detach(&self) -> Result<VmRecord>;
}

/// The guest memory balloon.
#[async_trait]
pub trait Balloon: Send + Sync {
    /// Asks the guest to give back memory until it holds `bytes`.
    async fn set_target(&self, bytes: u64) -> Result<()>;
    /// What the balloon was told and what it reports.
    async fn stats(&self) -> Result<BalloonStats>;
}

/// The balloon's target and, when the guest reports it, its actual size.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BalloonStats {
    /// The most recent target, in bytes.
    pub target_bytes: u64,
    /// The size the guest reports, in bytes, when it does.
    pub current_bytes: Option<u64>,
}

/// The guest console's output.
#[async_trait]
pub trait Console: Send + Sync {
    /// Takes up to `max_bytes` of console output not yet read.
    async fn read_output(&self, max_bytes: usize) -> Result<Vec<u8>>;
}

/// A point-in-time view of the VM's internals for diagnostics.
pub trait DebugSnapshot: Send + Sync {
    /// The driver's own JSON — queues, counters, whatever helps a post-mortem.
    fn snapshot(&self) -> serde_json::Value;
}