Skip to content
Merged
33 changes: 33 additions & 0 deletions doc/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,39 @@ boot of `nvx-rs run` does.
See [Run](run.md) for artifact preparation, the security boundary, and current
snapshot/configuration limitations.

### `snapshot capture`

```sh
nvx-rs/target/release/nvx-rs snapshot capture baseline \
--store-dir /private/snapshots --hypervisor kvm
```

Use `mshv` on Linux/MSHV or `whp` on Windows. The store's parent must exist;
the store and its capture VM are private to the current user. Capture uses the
identified OpenVMM, kernel and Alpine initramfs already installed in the artifact
root, without downloading substitutes or accepting `NVX_OPENVMM` overrides.

The VM boots with `nvx_sandbox=1` and no workload or layered root. Its sparse
placeholder image is at most `--image-capacity-mib` (1024 by default), and its
dormant scratch is `--scratch-mib` (64 by default). `--processors` (1) and
`--memory-mib` (256) select boot resources; `--processor-capacity` and
`--memory-capacity-mib` may reserve larger capacities. RAM capacity must be a
multiple of 128 MiB. `--cpu-profile` selects the OpenVMM profile.

Only a successful capture whose own VMM exits within `--timeout` (60 whole
seconds by default) can publish. The immutable named directory contains exactly
`key.bin`, `snapshot.json` and `openvmm/`; an existing name is never replaced.
Capture's own VM directory is then removed. A timed-out VM is stopped but its
capture is never finished. Other control or launch-record failures also stop
the command's verified VM without finishing it. The next capture command using
this store ends and removes an interrupted attempt through ordinary launch
recovery, without resuming or publishing it. Indeterminate recovery retains its
evidence and reports an error. Unknown files or nonempty unjournaled capture-slot
contents are kept for inspection, not deleted; an empty launch lock alone is
harmless. Known host and OpenVMM atomic staging files are recovered only with
a valid launch journal. Captures in one store serialize; snapshot locks remain
separate from their immutable directories. Restore commands are not added here.

### `download`

```text
Expand Down
10 changes: 10 additions & 0 deletions nvx-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2519,6 +2519,7 @@ nvx-rs [-h] [--root PATH] download [--repository OWNER/REPOSITORY] [--hypervisor
nvx-rs [-h] [--root PATH] egress compile PATH
nvx-rs [-h] [--root PATH] run [OPTIONS]
nvx-rs [-h] [--root PATH] sandbox [run|provision|start|exec|stop|deprovision] [OPTIONS]
nvx-rs [-h] [--root PATH] snapshot capture NAME [OPTIONS]
```

```bash
Expand Down Expand Up @@ -2569,6 +2570,15 @@ which take many options, name them `[OPTIONS]`, as in
`usage: nvx-rs run [OPTIONS]`, and their help lists every option. A usage error
names the parser after the usage, as in `nvx-rs run: error[CODE]: MESSAGE`.

`snapshot capture NAME --store-dir PATH` captures a blockless VM before any
layered root or workload exists. It journals the launch before spawning,
requests capture through authenticated NVXC, and requires that VM's timely
exit before finishing and publishing exactly the strict snapshot's three
entries. The next capture using that store ends an interrupted attempt through
ordinary launch recovery, never resuming it. See
[`snapshot capture`](../doc/usage.md#snapshot-capture) for its resource options
and recovery behavior. Restore commands are separate follow-ups.

### Artifact root

Every command uses the artifacts of one root: a repository checkout, with the
Expand Down
23 changes: 20 additions & 3 deletions nvx-rs/src/host/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! nvx-rs [-h] [--root PATH] egress compile PATH
//! nvx-rs [-h] [--root PATH] run [OPTIONS]
//! nvx-rs [-h] [--root PATH] sandbox [run|provision|start|exec|stop|deprovision] [OPTIONS]
//! nvx-rs [-h] [--root PATH] snapshot capture NAME [OPTIONS]
//! ```
//!
//! - `artifacts` prints the paths of the artifacts of the artifact root as JSON.
Expand All @@ -18,6 +19,7 @@
//! exit statuses of the `nvx.py run` that it replaced.
//! - `sandbox` runs or manages a sandbox, with the options, outputs, exit statuses, outcome
//! reports, and state directories of the `nvx.py sandbox` that it replaced.
//! - `snapshot capture` captures a blockless VM into an immutable named snapshot.
//!
//! The artifact root is `--root`, or else the release or repository checkout that holds the
//! executable, and the `NVX_OPENVMM` environment variable names another OpenVMM executable.
Expand Down Expand Up @@ -97,25 +99,33 @@ pub(crate) fn hex(bytes: &[u8]) -> String {
}

/// The commands, in the order in which usage messages list them.
const COMMANDS: [&str; 5] = ["artifacts", "download", "egress", "run", "sandbox"];
const COMMANDS: [&str; 6] = [
"artifacts",
"download",
"egress",
"run",
"sandbox",
"snapshot",
];
/// The options of the top-level parser.
const OPTIONS: [Spec; 1] = [Spec::one("--root")];
/// The usage and help of the top-level parser.
const SYNTAX: Syntax = Syntax {
command: "",
arguments: "[-h] [--root PATH] {artifacts,download,egress,run,sandbox} ...",
arguments: "[-h] [--root PATH] {artifacts,download,egress,run,sandbox,snapshot} ...",
help: "\
Run NVX microVMs and sandboxes, download and look up NVX artifacts, and compile
egress policies.

positional arguments:
{artifacts,download,egress,run,sandbox}
{artifacts,download,egress,run,sandbox,snapshot}
artifacts print the paths of the NVX artifacts as JSON
download download and install the NVX release of the checkout
egress compile egress policy files
run run an OpenVMM microVM
sandbox run or manage workloads over EROFS layers and private
ext4 scratch
snapshot capture an immutable named snapshot

options:
-h, --help show this help message and exit
Expand Down Expand Up @@ -334,6 +344,8 @@ enum Command {
Run(Box<run::Options>),
/// `sandbox ...`.
Sandbox(Box<sandbox::Options>),
/// `snapshot capture ...`.
Snapshot(snapshots::capture::Options),
}

/// The top-level options.
Expand Down Expand Up @@ -389,6 +401,10 @@ fn run(arguments: &[OsString]) -> Result {
let (options, unrecognized) = sandbox::parse(rest)?;
(Command::Sandbox(Box::new(options)), unrecognized)
}
"snapshot" => {
let (options, unrecognized) = snapshots::capture::parse(rest)?;
(Command::Snapshot(options), unrecognized)
}
other => {
return Err(usage(
ErrorCode::ArgumentInvalid,
Expand All @@ -413,6 +429,7 @@ fn run(arguments: &[OsString]) -> Result {
Command::EgressCompile(path) => egress::run(&path),
Command::Run(options) => run::run(root, *options),
Command::Sandbox(options) => sandbox::run(root, *options),
Command::Snapshot(options) => snapshots::capture::run(root, options),
}
}

Expand Down
2 changes: 1 addition & 1 deletion nvx-rs/src/host/cli/sandbox/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ use self::state::RecordedDistro;
/// The artifacts of a sandbox, found when an operation first needs them.
pub(crate) struct Artifacts<'a> {
/// `--root`, if given.
root: Option<&'a str>,
pub(super) root: Option<&'a str>,
}

impl Artifacts<'_> {
Expand Down
6 changes: 2 additions & 4 deletions nvx-rs/src/host/cli/snapshots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
//! Names map to hexadecimal directory names under `snapshots/`. Each published directory
//! contains only the strict snapshot's three entries; its lock stays under `locks/`.
//! Completed private directories under `staging/` publish with one non-replacing rename.
#![cfg_attr(
not(test),
expect(dead_code, reason = "SL3.7b and SL3.8a use the named snapshot store")
)]

pub(super) mod capture;

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

Expand Down
Loading
Loading