Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions nvx-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ nvx-rs/
│ ├── error.rs nvx::Error, its ErrorKind, and its stable ErrorCode
│ ├── exec/ the execution engine: the streams, credit, and outcome of an execution's two
│ │ sides, without I/O, threads, or features, for any framing and host
│ ├── host/ feature `host` (default): the runtime that every launch shares, VM directories,
│ │ artifacts, launch, network and egress policy files, filesystem, NVXS connection,
│ │ session, process, platform, VM instances, sandbox stores, templates' manifests,
│ │ compatibility keys, store, and leases, outcome reports, file seals, and the
│ │ sandbox launch profile of `nvx-rs sandbox`, its distro and scratch bindings,
│ │ and its prepared launches; with feature `cli`, the `nvx-rs` CLI
│ ├── host/ feature `host` (default): the runtime that every launch shares, VM
│ │ directories and configurations, artifacts, launch, network and egress policy
│ │ files, filesystem, NVXS connection, session, process, platform, VM instances,
│ │ sandbox stores, templates' manifests, compatibility keys, store, and leases,
│ │ outcome reports, file seals, and the sandbox launch profile of `nvx-rs sandbox`,
│ │ its distro and scratch bindings, and its prepared launches; with feature
│ │ `cli`, the `nvx-rs` CLI
│ ├── testing/ feature `testing`: the fake OpenVMM
│ ├── guest/ feature `guest`: the programs that run in Linux guests, the guest's NVXS
│ │ connection, the libraries through which a guest init or agent opens the
Expand Down Expand Up @@ -122,6 +123,7 @@ changes only when the module's purpose does.
| [`nvx::exec`](#nvxexec) | The execution engine: the streams, credit, and outcome of an execution's two sides, without I/O, for any framing |
| [`nvx::host::runtime`](#nvxhostruntime) | `Runtime`: what every launch shares, the runtime's files and their identity, the hypervisor and its probe, and the launch options |
| [`nvx::host::vm`](#nvxhostvm) | The directory of a VM, private to the current user, with the fixed names of the files and endpoints of its launches |
| [`nvx::host::config`](#nvxhostconfig) | `VmConfig`: a VM's processors, memory, root, view, export, and boot console as one value, checked without reading the host |
| [`nvx::host::artifacts`](#nvxhostartifacts) | Locating the OpenVMM executable and the guest artifacts that a VM runs, and their runtime identity |
| [`nvx::host::launch`](#nvxhostlaunch) | `LaunchSpec` and `VmmLaunch`: the OpenVMM command line of a managed microVM, its checks, and the inputs that it shares with an adapter's launch |
| [`nvx::host::network`](#nvxhostnetwork) | The one lowering of a network posture into OpenVMM options, for the CLI and external orchestrators |
Expand Down Expand Up @@ -290,6 +292,10 @@ and a proxy endpoint without credentials; the `WorkloadView`, its hostname,
and the `ExpectedResources`. The other inputs of the root's assembly are
invariants of the template.

`View` is the view that a VM's configuration, a [`VmConfig`](#nvxhostconfig),
gives its workload: its hostname and `MachineIdMode`, without an identity, which
each execution names.

`ActivationRecord::new` refuses a configuration that `Configuration::check`
finds past a public limit, `MAX_MAPPINGS`, `MAX_HOSTNAME_BYTES`, or
`MAX_PROXY_ENDPOINT_BYTES`, which keep every record within `MAX_RECORD_BYTES`,
Expand Down Expand Up @@ -441,6 +447,30 @@ credential that the host presents at a VM's control endpoint: `as_bytes`
returns it, and its `Debug` form hides it. The library never writes it to a
file; a caller that reconnects after a restart keeps it.

### `nvx::host::config`

`VmConfig`: the configuration of a VM as one value, its processors, its memory
in MiB, its `RootConfig`, the `View` that its workload has of it, the
`ExportPlan` of the host paths that it exports, if any, and the
`ConsoleAttachment` of its boot console, a `Listener` for a client or
`Disconnected`; the console's device exists either way.

A root is `Blockless`, the guest's own system from its initramfs, or `Layered`,
a `LayeredRoot`: the path of an image and its `RootImage`, EROFS or a GPT disk,
with the identity that the guest expects, an optional bound of a GPT disk's
first partition, the `OverlayMode`, and the `UpperConfig` of the overlay's
upper, a scratch image with its `Journal` policy, or RAM up to a cap of 1 to 90
percent of the VM's memory, 50 when the caller has no other
(`UpperConfig::DEFAULT_MAX_MEMORY_PERCENT`).

`VmConfig::check` checks the whole value without reading the host: it opens no
path and checks no support of the host. It fails with an `nvx::Error` of kind
`Unsupported` that names the first refusal: no processor or no memory; a layered
root without an image or a scratch path, with a bound of an EROFS image or of 0,
or with a RAM upper's cap outside 1 to 90 percent; a hostname that an activation
record refuses; or an export that `ExportPlan::check` refuses for the guest
targets that the root reserves.

### `nvx::host::artifacts`

`Artifacts` (also `nvx::Artifacts`): the OpenVMM executable, guest kernel, and
Expand Down
32 changes: 32 additions & 0 deletions nvx-rs/public-api/linux.txt
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ field nvx::activation::Upper::Scratch::binding
field nvx::activation::Upper::Scratch::journal
variant nvx::activation::Upper::Tmpfs
const nvx::activation::VERSION
struct nvx::activation::View
field nvx::activation::View::hostname
field nvx::activation::View::machine_id
struct nvx::activation::WorkloadIdentity
field nvx::activation::WorkloadIdentity::gid
field nvx::activation::WorkloadIdentity::uid
Expand Down Expand Up @@ -1746,6 +1749,35 @@ fn nvx::host::artifacts::check_manifest
fn nvx::host::artifacts::openvmm_executable
mod nvx::host::cli (hidden)
fn nvx::host::cli::main (hidden)
mod nvx::host::config
enum nvx::host::config::ConsoleAttachment
variant nvx::host::config::ConsoleAttachment::Disconnected
variant nvx::host::config::ConsoleAttachment::Listener
struct nvx::host::config::LayeredRoot
field nvx::host::config::LayeredRoot::format
field nvx::host::config::LayeredRoot::gpt_first_partition_bound
field nvx::host::config::LayeredRoot::image
field nvx::host::config::LayeredRoot::overlay
field nvx::host::config::LayeredRoot::upper
enum nvx::host::config::RootConfig
variant nvx::host::config::RootConfig::Blockless
variant nvx::host::config::RootConfig::Layered
field nvx::host::config::RootConfig::Layered::0
enum nvx::host::config::UpperConfig
const nvx::host::config::UpperConfig::DEFAULT_MAX_MEMORY_PERCENT
variant nvx::host::config::UpperConfig::Ram
field nvx::host::config::UpperConfig::Ram::max_memory_percent
variant nvx::host::config::UpperConfig::Scratch
field nvx::host::config::UpperConfig::Scratch::journal
field nvx::host::config::UpperConfig::Scratch::path
struct nvx::host::config::VmConfig
method nvx::host::config::VmConfig::check
field nvx::host::config::VmConfig::console
field nvx::host::config::VmConfig::filesystem
field nvx::host::config::VmConfig::memory_mib
field nvx::host::config::VmConfig::processors
field nvx::host::config::VmConfig::root
field nvx::host::config::VmConfig::view
mod nvx::host::filesystem
struct nvx::host::filesystem::Bind
field nvx::host::filesystem::Bind::child
Expand Down
32 changes: 32 additions & 0 deletions nvx-rs/public-api/windows.txt
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ field nvx::activation::Upper::Scratch::binding
field nvx::activation::Upper::Scratch::journal
variant nvx::activation::Upper::Tmpfs
const nvx::activation::VERSION
struct nvx::activation::View
field nvx::activation::View::hostname
field nvx::activation::View::machine_id
struct nvx::activation::WorkloadIdentity
field nvx::activation::WorkloadIdentity::gid
field nvx::activation::WorkloadIdentity::uid
Expand Down Expand Up @@ -672,6 +675,35 @@ fn nvx::host::artifacts::check_manifest
fn nvx::host::artifacts::openvmm_executable
mod nvx::host::cli (hidden)
fn nvx::host::cli::main (hidden)
mod nvx::host::config
enum nvx::host::config::ConsoleAttachment
variant nvx::host::config::ConsoleAttachment::Disconnected
variant nvx::host::config::ConsoleAttachment::Listener
struct nvx::host::config::LayeredRoot
field nvx::host::config::LayeredRoot::format
field nvx::host::config::LayeredRoot::gpt_first_partition_bound
field nvx::host::config::LayeredRoot::image
field nvx::host::config::LayeredRoot::overlay
field nvx::host::config::LayeredRoot::upper
enum nvx::host::config::RootConfig
variant nvx::host::config::RootConfig::Blockless
variant nvx::host::config::RootConfig::Layered
field nvx::host::config::RootConfig::Layered::0
enum nvx::host::config::UpperConfig
const nvx::host::config::UpperConfig::DEFAULT_MAX_MEMORY_PERCENT
variant nvx::host::config::UpperConfig::Ram
field nvx::host::config::UpperConfig::Ram::max_memory_percent
variant nvx::host::config::UpperConfig::Scratch
field nvx::host::config::UpperConfig::Scratch::journal
field nvx::host::config::UpperConfig::Scratch::path
struct nvx::host::config::VmConfig
method nvx::host::config::VmConfig::check
field nvx::host::config::VmConfig::console
field nvx::host::config::VmConfig::filesystem
field nvx::host::config::VmConfig::memory_mib
field nvx::host::config::VmConfig::processors
field nvx::host::config::VmConfig::root
field nvx::host::config::VmConfig::view
mod nvx::host::filesystem
struct nvx::host::filesystem::Bind
field nvx::host::filesystem::Bind::child
Expand Down
22 changes: 19 additions & 3 deletions nvx-rs/src/activation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
//! - the [`WorkloadView`]: the hostname, the [`MachineIdMode`], the image's own machine ID or one
//! that the guest generates, and the [`WorkloadIdentity`], if any: a UID and a GID, neither 0
//! nor `u32::MAX`, which the guest ignores as it applies the record, since each execution of a
//! clone names its own identity, as CRI containers do;
//! clone names its own identity, as CRI containers do. A VM's configuration names its hostname
//! and machine ID in a [`View`], which leaves the identity to each execution;
//! - the root's settings: the [`OverlayMode`], the [`Journal`] policy of a scratch upper, and the
//! bound of a GPT disk's first partition;
//! - the [`ExpectedResources`]: the CPU count and the memory size that the guest must have, as a
Expand Down Expand Up @@ -481,6 +482,16 @@ pub struct WorkloadView {
pub identity: Option<WorkloadIdentity>,
}

/// The view that a VM's workload has of it: its hostname and its machine ID, without an identity
/// of the executions, which each execution names.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct View {
/// The hostname: 1 to [`MAX_HOSTNAME_BYTES`] bytes, without a NUL.
pub hostname: Vec<u8>,
/// The machine ID that the workload sees.
pub machine_id: MachineIdMode,
}

/// The resources that the guest must have, as a clone's repair produces them.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ExpectedResources {
Expand Down Expand Up @@ -550,8 +561,7 @@ impl Configuration {
{
return Err(ActivationError::ProxyEndpoint);
}
let hostname = &self.workload.hostname;
if hostname.is_empty() || hostname.len() > MAX_HOSTNAME_BYTES || hostname.contains(&0) {
if !is_hostname(&self.workload.hostname) {
return Err(ActivationError::Hostname);
}
// The kernel reads `u32::MAX` as an unchanged ID, which the guest's entry refuses.
Expand Down Expand Up @@ -745,6 +755,12 @@ impl fmt::Display for ActivationError {

impl std::error::Error for ActivationError {}

/// Returns whether `hostname` is a hostname that a record or a VM's [`View`] holds: 1 to
/// [`MAX_HOSTNAME_BYTES`] bytes without a NUL, since the guest's entry sets any other.
pub(crate) fn is_hostname(hostname: &[u8]) -> bool {
!hostname.is_empty() && hostname.len() <= MAX_HOSTNAME_BYTES && !hostname.contains(&0)
}

/// Returns whether `endpoint` is a proxy endpoint that a record holds: 1 to
/// [`MAX_PROXY_ENDPOINT_BYTES`] visible ASCII characters, without an `@`, which would start a
/// URL's user information, where credentials go.
Expand Down
165 changes: 165 additions & 0 deletions nvx-rs/src/host/config/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//! The configuration of a VM, as one value that [`VmConfig::check`] checks without reading the
//! host: the VM's processors and memory, its [`RootConfig`], the [`View`] that its workload has
//! of it, the host paths that it exports, and its [`ConsoleAttachment`].

use std::ops::RangeInclusive;
use std::path::PathBuf;

use super::filesystem::{ExportPlan, ReservedTargets};
use crate::activation::{self, Journal, MAX_HOSTNAME_BYTES, OverlayMode, View};
use crate::error::{Error, Result};
use crate::protocol::root::{ImageFormat, RootImage};

/// The caps of a RAM upper that [`VmConfig::check`] accepts, in percent of the VM's memory.
const RAM_UPPER_CAPS: RangeInclusive<u8> = 1..=90;

/// The configuration of a VM.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VmConfig {
/// The number of processors: at least 1.
pub processors: u32,
/// The memory, in MiB: at least 1.
pub memory_mib: u64,
/// The root that the guest runs.
pub root: RootConfig,
/// The view that the VM's workload has of it.
pub view: View,
/// The host paths that the VM exports to its guest, if any.
pub filesystem: Option<ExportPlan>,
/// Where the output of the VM's boot console goes.
pub console: ConsoleAttachment,
}

/// The root of a VM.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RootConfig {
/// The guest's own system, from its initramfs, without a block device.
Blockless,
/// A read-only image under an upper, which overlayfs combines.
Layered(LayeredRoot),
}

/// A layered root: a read-only image under an upper.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LayeredRoot {
/// The image, which OpenVMM opens by its path.
pub image: PathBuf,
/// The image's format, and the identity that the guest expects of the image, if any.
pub format: RootImage,
/// The most bytes that the first partition of a GPT disk may hold, if bounded: at least 1. An
/// EROFS image has none.
pub gpt_first_partition_bound: Option<u64>,
/// Where the overlay keeps its upper.
pub upper: UpperConfig,
/// The overlay's mode.
pub overlay: OverlayMode,
}

/// Where a layered root keeps the upper of its overlay.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpperConfig {
/// A whole-device ext4 filesystem on a scratch image.
Scratch {
/// The scratch image, which OpenVMM opens by its path.
path: PathBuf,
/// Whether the scratch's filesystem may have a journal.
journal: Journal,
},
/// RAM, up to a cap.
Ram {
/// The most that the upper may hold, in percent of the VM's memory: 1 to 90.
max_memory_percent: u8,
},
}

impl UpperConfig {
/// The cap of a RAM upper that a caller sets when it has no other: half of the VM's memory.
pub const DEFAULT_MAX_MEMORY_PERCENT: u8 = 50;
}

/// Where the output of a VM's boot console goes. The console's device exists either way.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConsoleAttachment {
/// OpenVMM listens for a client of the console.
Listener,
/// OpenVMM discards the console's output.
Disconnected,
}

impl VmConfig {
/// Checks the whole configuration without reading the host: it opens no path, and checks no
/// support of the host.
///
/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), naming the first
/// refusal in the order of the fields: no processor; no memory; a layered root without an image
/// path, with a bound of an EROFS image's first partition or a bound of 0, with a scratch upper
/// without a path, or with a RAM upper whose cap is not 1 to 90 percent; a hostname that is
/// empty, longer than [`MAX_HOSTNAME_BYTES`], or holds a NUL; and an export that
/// [`ExportPlan::check`] refuses for the root, whose error is the source.
pub fn check(&self) -> Result<()> {
if self.processors == 0 {
return Err(Error::unsupported("a VM needs at least one processor"));
}
if self.memory_mib == 0 {
return Err(Error::unsupported("a VM needs memory"));
}
let reserved = match &self.root {
RootConfig::Blockless => ReservedTargets::BLOCKLESS_ROOT,
RootConfig::Layered(root) => {
root.check()?;
ReservedTargets::CONTAINER_ROOT
}
};
if !activation::is_hostname(&self.view.hostname) {
return Err(Error::unsupported(format!(
"the hostname is not 1 to {MAX_HOSTNAME_BYTES} bytes without a NUL"
)));
}
if let Some(plan) = &self.filesystem {
plan.check(reserved).map_err(|error| {
Error::unsupported("the export plan is invalid").with_source(error)
})?;
}
Ok(())
}
}

impl LayeredRoot {
/// Checks the root's fields, as [`VmConfig::check`] describes.
fn check(&self) -> Result<()> {
if self.image.as_os_str().is_empty() {
return Err(Error::unsupported("the layered root names no image"));
}
match (self.format.format, self.gpt_first_partition_bound) {
(ImageFormat::Erofs, Some(_)) => {
return Err(Error::unsupported(
"an EROFS image has no first partition to bound, as a GPT disk has",
));
}
(_, Some(0)) => {
return Err(Error::unsupported(
"the bound of a GPT disk's first partition is 0, but a partition holds at \
least one block",
));
}
_ => {}
}
match &self.upper {
UpperConfig::Scratch { path, .. } if path.as_os_str().is_empty() => Err(
Error::unsupported("the scratch upper names no scratch image"),
),
UpperConfig::Ram { max_memory_percent }
if !RAM_UPPER_CAPS.contains(max_memory_percent) =>
{
Err(Error::unsupported(format!(
"the RAM upper's cap of {max_memory_percent}% of the VM's memory is not 1 to \
90%"
)))
}
_ => Ok(()),
}
}
}

#[cfg(test)]
mod tests;
Loading
Loading