diff --git a/nvx-rs/README.md b/nvx-rs/README.md index ff16eb8df..7236ca3b0 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -45,12 +45,12 @@ 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): 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, 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 @@ -120,6 +120,7 @@ changes only when the module's purpose does. | [`nvx::uuid`](#nvxuuid) | UUIDs, such as those of GPT disks and filesystems, in their canonical text and in GPT's layout | | [`nvx::Hypervisor`](#nvxhypervisor) | KVM, MSHV, or WHP, with its serde form behind the `serde` feature | | [`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::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 | @@ -379,6 +380,26 @@ The adapter chooses one byte window per stream, or frames charged against one pool that stdout and stderr share, and the maximum chunk, in `Limits`; a `Key`'s generation tells stale messages from foreign ones. +### `nvx::host::runtime` + +`Runtime`: what every launch shares, the paths of the OpenVMM executable, guest +kernel, and guest initramfs, their `RuntimeIdentity`, the hypervisor, an +optional CPU profile, whether OpenVMM breaks away from the caller's Windows job +object, and the `LaunchOptions`. Those are what OpenVMM does when the guest +powers off, a `GuestPowerAction`, by default `Exit`, which ends the VM, or +`Halt` or `Reset`, and how long OpenVMM waits for the host to authenticate on +the control console. The boot console is not among them: each VM attaches its +own. + +`RuntimeIdentity` labels the three files with the `ArtifactIdentity` tokens that +their install or staging metadata gives, such as `sha256:` and a digest, which +NVX compares and never computes, and adds the versions of the contracts that the +crate speaks; `differences` names the `IdentityPart`s in which two identities +differ. The sandbox store records it with each VM, and re-exports its types. + +`probe_hypervisor` checks that the host has the hypervisor: its device on Linux, +and the Windows Hypervisor Platform on Windows. + ### `nvx::host::artifacts` `Artifacts` (also `nvx::Artifacts`): the OpenVMM executable, guest kernel, and @@ -2563,12 +2584,13 @@ optional cause. | `ErrorKind` | Meaning | | --- | --- | | `Unavailable` | A missing or unusable artifact, hypervisor, or release, a guest image without a required control feature, or an unsupported host | -| `Unsupported` | A well-formed request that NVX cannot represent or enforce, such as a policy beyond a limit of OpenVMM or the guest agent | +| `Unsupported` | A request that NVX refuses: a value that it does not accept, such as a malformed or out-of-range one, options that it cannot combine, or a request that it cannot represent or enforce, such as a policy beyond a limit of OpenVMM or the guest agent | | `Exited` | The VM's OpenVMM process exited | | `Io` | A host I/O failure, or state that NVX recorded on the host and cannot read | | `NotFound` | State that an operation names and that does not exist on the host, such as the store of a sandbox that was never created or that a removal deleted | | `Protocol` | A control session that failed, or a request that the guest agent refused | | `TimedOut` | A deadline that passed | +| `RuntimeMismatch` | A VM that runs under another runtime identity than the artifacts', which NVX does not drive: only a stop, which terminates it, applies | | `Other` | Any other failure | Messages call an instance `sandbox NAME`, after the name that `Instance::create` @@ -2612,7 +2634,7 @@ usage errors, which no library error has. | `sandbox-running` | `Other` | The operation needs a stopped sandbox, but its VM, or a launch of it, may still run | | `sandbox-exited` | `Exited` | The sandbox's VM exited without a stop: the transition that finds it clears its runtime record, and a start launches it again | | `sandbox-busy` | `TimedOut` | Another lifecycle transition of the sandbox held its lock until the deadline passed | -| `runtime-mismatch` | `Other` | The sandbox's VM runs under another runtime identity than the artifacts', whose differing parts the message names, so only a stop, which terminates it, applies | +| `runtime-mismatch` | `RuntimeMismatch` | The sandbox's VM runs under another runtime identity than the artifacts', whose differing parts the message names, so only a stop, which terminates it, applies | | `vmm-indeterminate` | `Other` | Whether the OpenVMM process of the sandbox runs cannot be determined, so its records remain for a later attempt | | `format-unsupported` | `Io` | A record or report has a format or schema version that this build does not read, such as the state of a sandbox that another build provisioned or started | | `foreign-files` | `Other` | A directory of NVX's state, such as a sandbox's state directory or a template store, holds files that NVX does not own, which the message names and NVX keeps | diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 1f5fa02b3..933df01ac 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -72,6 +72,7 @@ variant nvx::ErrorKind::Io variant nvx::ErrorKind::NotFound variant nvx::ErrorKind::Other variant nvx::ErrorKind::Protocol +variant nvx::ErrorKind::RuntimeMismatch variant nvx::ErrorKind::TimedOut variant nvx::ErrorKind::Unavailable variant nvx::ErrorKind::Unsupported @@ -2015,7 +2016,6 @@ method nvx::host::platform::Transport::write_all type nvx::host::platform::TransportHandle fn nvx::host::platform::create_private_dir fn nvx::host::platform::file_identity -fn nvx::host::platform::probe_hypervisor fn nvx::host::platform::process_start_time mod nvx::host::process struct nvx::host::process::BootId @@ -2044,6 +2044,48 @@ field nvx::host::report::Report::category field nvx::host::report::Report::status_code field nvx::host::report::Report::teardown_complete fn nvx::host::report::read +mod nvx::host::runtime +struct nvx::host::runtime::ArtifactIdentity +method nvx::host::runtime::ArtifactIdentity::as_str +method nvx::host::runtime::ArtifactIdentity::new +method nvx::host::runtime::ArtifactIdentity::sha256 +enum nvx::host::runtime::GuestPowerAction +variant nvx::host::runtime::GuestPowerAction::Exit +variant nvx::host::runtime::GuestPowerAction::Halt +variant nvx::host::runtime::GuestPowerAction::Reset +enum nvx::host::runtime::IdentityPart +const nvx::host::runtime::IdentityPart::ALL +variant nvx::host::runtime::IdentityPart::BootContract +variant nvx::host::runtime::IdentityPart::ControlContract +variant nvx::host::runtime::IdentityPart::ControlSession +variant nvx::host::runtime::IdentityPart::Initramfs +variant nvx::host::runtime::IdentityPart::Kernel +variant nvx::host::runtime::IdentityPart::OpenVmm +method nvx::host::runtime::IdentityPart::name +struct nvx::host::runtime::LaunchOptions +field nvx::host::runtime::LaunchOptions::control_auth_timeout +field nvx::host::runtime::LaunchOptions::guest_shutdown +const nvx::host::runtime::MAX_IDENTITY_LEN +struct nvx::host::runtime::Runtime +field nvx::host::runtime::Runtime::cpu_profile +field nvx::host::runtime::Runtime::hypervisor +field nvx::host::runtime::Runtime::identity +field nvx::host::runtime::Runtime::initramfs +field nvx::host::runtime::Runtime::kernel +field nvx::host::runtime::Runtime::openvmm +field nvx::host::runtime::Runtime::options +field nvx::host::runtime::Runtime::windows_breakaway +struct nvx::host::runtime::RuntimeIdentity +field nvx::host::runtime::RuntimeIdentity::boot_contract +method nvx::host::runtime::RuntimeIdentity::check +field nvx::host::runtime::RuntimeIdentity::control_contract +field nvx::host::runtime::RuntimeIdentity::control_session +method nvx::host::runtime::RuntimeIdentity::differences +field nvx::host::runtime::RuntimeIdentity::initramfs +field nvx::host::runtime::RuntimeIdentity::kernel +method nvx::host::runtime::RuntimeIdentity::new +field nvx::host::runtime::RuntimeIdentity::openvmm +fn nvx::host::runtime::probe_hypervisor mod nvx::host::sandbox enum nvx::host::sandbox::BlockRole variant nvx::host::sandbox::BlockRole::Distro diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index b3952abbd..2de59d619 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -72,6 +72,7 @@ variant nvx::ErrorKind::Io variant nvx::ErrorKind::NotFound variant nvx::ErrorKind::Other variant nvx::ErrorKind::Protocol +variant nvx::ErrorKind::RuntimeMismatch variant nvx::ErrorKind::TimedOut variant nvx::ErrorKind::Unavailable variant nvx::ErrorKind::Unsupported @@ -953,7 +954,6 @@ method nvx::host::platform::Transport::write_all type nvx::host::platform::TransportHandle fn nvx::host::platform::create_private_dir fn nvx::host::platform::file_identity -fn nvx::host::platform::probe_hypervisor fn nvx::host::platform::process_start_time mod nvx::host::process struct nvx::host::process::BootId @@ -982,6 +982,48 @@ field nvx::host::report::Report::category field nvx::host::report::Report::status_code field nvx::host::report::Report::teardown_complete fn nvx::host::report::read +mod nvx::host::runtime +struct nvx::host::runtime::ArtifactIdentity +method nvx::host::runtime::ArtifactIdentity::as_str +method nvx::host::runtime::ArtifactIdentity::new +method nvx::host::runtime::ArtifactIdentity::sha256 +enum nvx::host::runtime::GuestPowerAction +variant nvx::host::runtime::GuestPowerAction::Exit +variant nvx::host::runtime::GuestPowerAction::Halt +variant nvx::host::runtime::GuestPowerAction::Reset +enum nvx::host::runtime::IdentityPart +const nvx::host::runtime::IdentityPart::ALL +variant nvx::host::runtime::IdentityPart::BootContract +variant nvx::host::runtime::IdentityPart::ControlContract +variant nvx::host::runtime::IdentityPart::ControlSession +variant nvx::host::runtime::IdentityPart::Initramfs +variant nvx::host::runtime::IdentityPart::Kernel +variant nvx::host::runtime::IdentityPart::OpenVmm +method nvx::host::runtime::IdentityPart::name +struct nvx::host::runtime::LaunchOptions +field nvx::host::runtime::LaunchOptions::control_auth_timeout +field nvx::host::runtime::LaunchOptions::guest_shutdown +const nvx::host::runtime::MAX_IDENTITY_LEN +struct nvx::host::runtime::Runtime +field nvx::host::runtime::Runtime::cpu_profile +field nvx::host::runtime::Runtime::hypervisor +field nvx::host::runtime::Runtime::identity +field nvx::host::runtime::Runtime::initramfs +field nvx::host::runtime::Runtime::kernel +field nvx::host::runtime::Runtime::openvmm +field nvx::host::runtime::Runtime::options +field nvx::host::runtime::Runtime::windows_breakaway +struct nvx::host::runtime::RuntimeIdentity +field nvx::host::runtime::RuntimeIdentity::boot_contract +method nvx::host::runtime::RuntimeIdentity::check +field nvx::host::runtime::RuntimeIdentity::control_contract +field nvx::host::runtime::RuntimeIdentity::control_session +method nvx::host::runtime::RuntimeIdentity::differences +field nvx::host::runtime::RuntimeIdentity::initramfs +field nvx::host::runtime::RuntimeIdentity::kernel +method nvx::host::runtime::RuntimeIdentity::new +field nvx::host::runtime::RuntimeIdentity::openvmm +fn nvx::host::runtime::probe_hypervisor mod nvx::host::sandbox enum nvx::host::sandbox::BlockRole variant nvx::host::sandbox::BlockRole::Distro diff --git a/nvx-rs/src/contract.rs b/nvx-rs/src/contract.rs index 1f95e9560..666a817ca 100644 --- a/nvx-rs/src/contract.rs +++ b/nvx-rs/src/contract.rs @@ -2,10 +2,10 @@ //! compatibility of NVX releases with them. //! //! The store records the boot contract, control contract, and control session versions of the -//! build that started a VM in the VM's runtime identity, `nvx::host::store::RuntimeIdentity`, and a -//! build drives only the VMs whose runtime identity is its own. So each version changes whenever a -//! guest that a build of the earlier version started could not be driven as one that a build of -//! the later version starts: +//! build that started a VM in the VM's runtime identity, `nvx::host::runtime::RuntimeIdentity`, +//! and a build drives only the VMs whose runtime identity is its own. So each version changes +//! whenever a guest that a build of the earlier version started could not be driven as one that a +//! build of the later version starts: //! //! - [`MICROVM_ABI_VERSION`] is the microVM ABI of the pinned OpenVMM: the machine, its devices, //! and their addresses, through which OpenVMM boots the guest and saves its state. It changes diff --git a/nvx-rs/src/error.rs b/nvx-rs/src/error.rs index 9cfdf3701..989538bab 100644 --- a/nvx-rs/src/error.rs +++ b/nvx-rs/src/error.rs @@ -13,8 +13,9 @@ pub enum ErrorKind { /// guest image without the control contract or features that this crate needs, or a host /// that NVX does not support. Unavailable, - /// The request is well formed, but NVX cannot represent or enforce it, for example because it - /// exceeds a limit of OpenVMM or the guest agent. + /// NVX refuses the request: a value that it does not accept, such as a malformed or + /// out-of-range one, options that it cannot combine, or a request that it cannot represent or + /// enforce, for example because it exceeds a limit of OpenVMM or the guest agent. Unsupported, /// The OpenVMM process of a VM exited. Exited, @@ -29,6 +30,9 @@ pub enum ErrorKind { Protocol, /// A deadline passed. TimedOut, + /// A VM runs under another runtime identity than the artifacts', so NVX does not drive it: + /// only a stop, which terminates it, applies. + RuntimeMismatch, /// Any other failure. Other, } @@ -44,6 +48,7 @@ impl ErrorKind { Self::NotFound => "not-found", Self::Protocol => "protocol", Self::TimedOut => "timed-out", + Self::RuntimeMismatch => "runtime-mismatch", Self::Other => "other", } } @@ -160,7 +165,7 @@ pub enum ErrorCode { } impl ErrorCode { - /// Every code of this release, the codes of the kinds first. + /// Every code of this release. pub const ALL: &'static [ErrorCode] = &[ Self::Unavailable, Self::Unsupported, @@ -216,7 +221,7 @@ impl ErrorCode { Self::SandboxRunning => "sandbox-running", Self::SandboxExited => "sandbox-exited", Self::SandboxBusy => "sandbox-busy", - Self::RuntimeMismatch => "runtime-mismatch", + Self::RuntimeMismatch => ErrorKind::RuntimeMismatch.as_str(), Self::VmmIndeterminate => "vmm-indeterminate", Self::FormatUnsupported => "format-unsupported", Self::ForeignFiles => "foreign-files", @@ -247,11 +252,11 @@ impl ErrorCode { Self::NotFound | Self::SandboxNotProvisioned => ErrorKind::NotFound, Self::Protocol | Self::ExecRefused | Self::EndpointClosed => ErrorKind::Protocol, Self::TimedOut | Self::SandboxBusy | Self::ScratchClaimed => ErrorKind::TimedOut, + Self::RuntimeMismatch => ErrorKind::RuntimeMismatch, Self::Other | Self::SandboxProvisioned | Self::SandboxNotRunning | Self::SandboxRunning - | Self::RuntimeMismatch | Self::VmmIndeterminate | Self::ForeignFiles | Self::OutcomeReportExists @@ -277,6 +282,7 @@ impl From for ErrorCode { ErrorKind::NotFound => Self::NotFound, ErrorKind::Protocol => Self::Protocol, ErrorKind::TimedOut => Self::TimedOut, + ErrorKind::RuntimeMismatch => Self::RuntimeMismatch, ErrorKind::Other => Self::Other, } } diff --git a/nvx-rs/src/error/tests.rs b/nvx-rs/src/error/tests.rs index ea926d394..eb87d7dc0 100644 --- a/nvx-rs/src/error/tests.rs +++ b/nvx-rs/src/error/tests.rs @@ -24,10 +24,21 @@ fn codes_determine_kinds() { assert_eq!(error.code(), ErrorCode::SandboxExited); assert_eq!(error.kind(), ErrorKind::Exited); assert_eq!(error.into_parts().0, ErrorKind::Exited); + let mismatch = Error::new(ErrorCode::RuntimeMismatch, "another runtime"); + assert_eq!(mismatch.code(), ErrorCode::RuntimeMismatch); + assert_eq!(mismatch.kind(), ErrorKind::RuntimeMismatch); + let (kind, message, source) = mismatch.into_parts(); + assert_eq!(kind, ErrorKind::RuntimeMismatch); + assert_eq!(message, "another runtime"); + assert!(source.is_none()); assert_eq!( Error::new(ErrorKind::TimedOut, "late").code(), ErrorCode::TimedOut ); + assert_eq!( + Error::new(ErrorKind::RuntimeMismatch, "elsewhere").code(), + ErrorCode::RuntimeMismatch + ); assert_eq!(Error::unsupported("no").code(), ErrorCode::Unsupported); assert_eq!( ErrorCode::SandboxNotRunning.to_string(), @@ -45,6 +56,7 @@ fn every_kind_has_a_code_of_its_own_spelling() { ErrorKind::NotFound, ErrorKind::Protocol, ErrorKind::TimedOut, + ErrorKind::RuntimeMismatch, ErrorKind::Other, ] { let code = ErrorCode::from(kind); diff --git a/nvx-rs/src/host/artifacts/mod.rs b/nvx-rs/src/host/artifacts/mod.rs index 3deabc5d0..508c2b2d4 100644 --- a/nvx-rs/src/host/artifacts/mod.rs +++ b/nvx-rs/src/host/artifacts/mod.rs @@ -146,7 +146,7 @@ impl Artifacts { /// /// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) otherwise, with the /// code [`ErrorCode::ArtifactMissing`] for a missing file. It does not check the hypervisor; - /// [`probe_hypervisor`](super::platform::probe_hypervisor) does. + /// [`probe_hypervisor`](super::runtime::probe_hypervisor) does. pub fn probe(&self) -> Result<()> { if !platform::SUPPORTED { return Err(Error::unavailable( diff --git a/nvx-rs/src/host/mod.rs b/nvx-rs/src/host/mod.rs index 0e4bc2254..14b7299ee 100644 --- a/nvx-rs/src/host/mod.rs +++ b/nvx-rs/src/host/mod.rs @@ -2,6 +2,10 @@ //! //! - [`artifacts`] locates the OpenVMM executable and the guest kernel and initramfs, and every //! artifact of the guests that `nvx.py` runs, in a repository checkout or a release. +//! - [`runtime`] holds what every launch shares: the paths of those three files and their +//! [`RuntimeIdentity`](runtime::RuntimeIdentity), the hypervisor, which +//! [`probe_hypervisor`](runtime::probe_hypervisor) checks, and the +//! [`LaunchOptions`](runtime::LaunchOptions). //! - [`launch`] builds OpenVMM's command line from a [`LaunchSpec`](launch::LaunchSpec): the //! managed lifecycle, the workload identity, the kernel command line, and the network and //! filesystem options. @@ -73,6 +77,7 @@ pub(crate) mod paths; pub mod platform; pub mod process; pub mod report; +pub mod runtime; pub mod sandbox; pub mod seal; pub mod session; diff --git a/nvx-rs/src/host/platform/mod.rs b/nvx-rs/src/host/platform/mod.rs index 3b4c990c6..b990f92d6 100644 --- a/nvx-rs/src/host/platform/mod.rs +++ b/nvx-rs/src/host/platform/mod.rs @@ -12,8 +12,9 @@ use std::io; use std::path::Path; use std::time::Duration; -use crate::error::{Error, Result}; -use crate::hypervisor::Hypervisor; +#[cfg(windows)] +use crate::error::Error; +use crate::error::Result; pub(crate) mod inherit; #[cfg(target_os = "linux")] @@ -36,14 +37,6 @@ use windows as imp; pub(crate) use imp::*; pub use imp::{SUPPORTED, create_private_dir, file_identity, process_start_time}; -/// Checks that `hypervisor` is present and accessible on this host: its device on Linux, and the -/// Windows Hypervisor Platform on Windows. -/// -/// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) otherwise. -pub fn probe_hypervisor(hypervisor: Hypervisor) -> Result<()> { - imp::probe_hypervisor(hypervisor).map_err(Error::unavailable) -} - /// Refuses a VMM working directory that exceeds Windows' process-creation limit, without /// accessing the filesystem. Other hosts have no such limit. pub(crate) fn check_working_dir(path: &Path) -> Result<()> { diff --git a/nvx-rs/src/host/store/identity.rs b/nvx-rs/src/host/runtime/mod.rs similarity index 75% rename from nvx-rs/src/host/store/identity.rs rename to nvx-rs/src/host/runtime/mod.rs index d0b3f715c..90ba8edd2 100644 --- a/nvx-rs/src/host/store/identity.rs +++ b/nvx-rs/src/host/runtime/mod.rs @@ -1,12 +1,78 @@ -//! The runtime identity of a VM: the identities of its VMM, kernel, and initramfs, and the -//! versions of the contracts between its host, its VMM, and its guest. +//! What every launch shares: a [`Runtime`], the OpenVMM executable, guest kernel, and guest +//! initramfs that it runs, with their [`RuntimeIdentity`], the hypervisor, which +//! [`probe_hypervisor`] checks, and the [`LaunchOptions`]. +//! +//! The runtime identity of a VM is the identities of its VMM, kernel, and initramfs, and the +//! versions of the contracts between its host, its VMM, and its guest. The sandbox +//! [`store`](crate::host::store) records it, and re-exports its types. use std::fmt; +use std::path::PathBuf; +use std::time::Duration; use serde::{Deserialize, Serialize}; +use super::platform; use crate::contract; use crate::error::{Error, Result}; +use crate::hypervisor::Hypervisor; + +/// What every launch shares: the runtime's files and their identity, the hypervisor, and the +/// options of OpenVMM that do not depend on the VM. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Runtime { + /// The OpenVMM executable. + pub openvmm: PathBuf, + /// The guest kernel. + pub kernel: PathBuf, + /// The guest initramfs. + pub initramfs: PathBuf, + /// The identity of the three files and of the contracts that they speak, which the caller + /// takes from their install or staging metadata: NVX compares identities and never reads the + /// files. + pub identity: RuntimeIdentity, + /// The hypervisor that OpenVMM runs the VM on. + pub hypervisor: Hypervisor, + /// The CPU profile that OpenVMM presents to the guest, by OpenVMM's name for it, or `None` + /// for OpenVMM's default. + pub cpu_profile: Option, + /// Windows only: start OpenVMM outside the caller's job object, so that the VM survives the + /// caller. The job must permit breakaway. + pub windows_breakaway: bool, + /// The options of OpenVMM that every launch shares. + pub options: LaunchOptions, +} + +/// The options of OpenVMM that every launch shares. The boot console is not one of them: each VM +/// attaches its own. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct LaunchOptions { + /// What OpenVMM does when the guest powers off. + pub guest_shutdown: GuestPowerAction, + /// How long OpenVMM waits for the host to authenticate on the control console, or `None` for + /// OpenVMM's default. + pub control_auth_timeout: Option, +} + +/// What OpenVMM does when the guest powers off. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum GuestPowerAction { + /// OpenVMM exits, which ends the VM: the default, on which stopping a VM relies. + #[default] + Exit, + /// OpenVMM halts the VM and keeps running. + Halt, + /// OpenVMM restarts the guest. + Reset, +} + +/// Checks that `hypervisor` is present and accessible on this host: its device on Linux, and the +/// Windows Hypervisor Platform on Windows. +/// +/// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) otherwise. +pub fn probe_hypervisor(hypervisor: Hypervisor) -> Result<()> { + platform::probe_hypervisor(hypervisor).map_err(Error::unavailable) +} /// Longest [`ArtifactIdentity`] and control contract revision, in bytes. pub const MAX_IDENTITY_LEN: usize = 128; @@ -159,7 +225,7 @@ impl fmt::Display for IdentityPart { /// build of the host's own code keeps the VM running and reattaches to it. A different runtime /// identity would drive the VM through artifacts or contracts that did not start it, so the VM /// must be stopped instead, through its verified process identity; see -/// [`RuntimeRecord::check_identity`](super::RuntimeRecord::check_identity). +/// [`RuntimeRecord::check_identity`](crate::host::store::RuntimeRecord::check_identity). /// /// The caller takes the artifact identities from install or staging metadata, and /// [`RuntimeIdentity::new`] adds the contract versions that this crate speaks. @@ -233,13 +299,5 @@ impl RuntimeIdentity { } } -/// How the runtime identity of a record compares with the current one. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum IdentityCheck { - /// The identities are equal: the host may reattach to the VM and keep it running. - Matches, - /// The identities differ in the parts that this lists, as - /// [`RuntimeIdentity::differences`] returns them: the VM must be stopped, and only through - /// its verified process identity. - Mismatched(Vec), -} +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/runtime/tests.rs b/nvx-rs/src/host/runtime/tests.rs new file mode 100644 index 000000000..bda10ef06 --- /dev/null +++ b/nvx-rs/src/host/runtime/tests.rs @@ -0,0 +1,148 @@ +//! Tests of the runtime identities, of the values that every launch shares, and of the probe of a +//! hypervisor. + +use super::*; +use crate::ErrorKind; +/// Returns the identity `sha256:` and 64 copies of `digit`. +fn artifact(digit: char) -> ArtifactIdentity { + ArtifactIdentity::sha256(&digit.to_string().repeat(64)).unwrap() +} + +/// Returns a runtime identity under this crate's contracts. +fn runtime() -> RuntimeIdentity { + RuntimeIdentity::new(artifact('a'), artifact('b'), artifact('c')) +} + +#[test] +fn artifact_identities_are_printable_tokens() { + assert_eq!( + ArtifactIdentity::new("openvmm-1.2+build.7") + .unwrap() + .as_str(), + "openvmm-1.2+build.7" + ); + for invalid in [ + String::new(), + "with space".to_owned(), + "tab\there".to_owned(), + "caf\u{e9}".to_owned(), + "x".repeat(MAX_IDENTITY_LEN + 1), + ] { + let error = ArtifactIdentity::new(invalid.clone()).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{invalid:?}"); + } + assert!(ArtifactIdentity::new("x".repeat(MAX_IDENTITY_LEN)).is_ok()); + + let digest = "AB".repeat(32); + assert_eq!( + ArtifactIdentity::sha256(&digest).unwrap().to_string(), + format!("sha256:{}", "ab".repeat(32)) + ); + for invalid in ["ab".repeat(31), "g".repeat(64), "ab".repeat(33)] { + assert_eq!( + ArtifactIdentity::sha256(&invalid).unwrap_err().kind(), + ErrorKind::Unsupported + ); + } + // The serde form checks the token too. + assert!(serde_json::from_value::(serde_json::json!("a b")).is_err()); +} + +#[test] +fn runtime_identities_bind_this_crates_contracts() { + let identity = runtime(); + assert_eq!(identity.boot_contract, contract::BOOT_CONTRACT_VERSION); + assert_eq!( + identity.control_contract, + contract::CONTROL_CONTRACT_REVISION + ); + assert_eq!( + identity.control_session, + contract::CONTROL_SESSION_PROTOCOL_VERSION + ); + identity.check().unwrap(); + assert!(identity.differences(&runtime()).is_empty()); + + let mut other = runtime(); + other.kernel = artifact('d'); + other.control_session += 1; + assert_eq!( + identity.differences(&other), + [IdentityPart::Kernel, IdentityPart::ControlSession] + ); + other = RuntimeIdentity::new(artifact('e'), artifact('b'), artifact('f')); + other.boot_contract += 1; + other.control_contract = "another-revision".to_owned(); + assert_eq!( + identity.differences(&other), + [ + IdentityPart::OpenVmm, + IdentityPart::Initramfs, + IdentityPart::BootContract, + IdentityPart::ControlContract + ] + ); + let names: Vec<_> = IdentityPart::ALL.iter().map(ToString::to_string).collect(); + assert_eq!( + names, + [ + "OpenVMM", + "kernel", + "initramfs", + "boot contract", + "control contract", + "control session" + ] + ); + for invalid in ["", "two words"] { + other.control_contract = invalid.to_owned(); + assert_eq!(other.check().unwrap_err().kind(), ErrorKind::Unsupported); + } +} + +/// Returns a runtime that supplies each field that every launch shares, with the default launch +/// options. +fn shared() -> Runtime { + Runtime { + openvmm: PathBuf::from("openvmm"), + kernel: PathBuf::from("vmlinux"), + initramfs: PathBuf::from("initramfs.cpio.gz"), + identity: runtime(), + hypervisor: Hypervisor::Kvm, + cpu_profile: Some("auto".to_owned()), + windows_breakaway: false, + options: LaunchOptions::default(), + } +} + +#[test] +fn by_default_a_guest_that_powers_off_ends_its_vm() { + assert_eq!(GuestPowerAction::default(), GuestPowerAction::Exit); + assert_eq!( + shared().options, + LaunchOptions { + guest_shutdown: GuestPowerAction::Exit, + control_auth_timeout: None, + } + ); +} + +#[test] +fn a_hypervisor_that_the_host_never_has_is_unavailable() { + let foreign: &[Hypervisor] = if cfg!(target_os = "linux") { + &[Hypervisor::Whp] + } else if cfg!(windows) { + &[Hypervisor::Kvm, Hypervisor::Mshv] + } else { + &[Hypervisor::Kvm, Hypervisor::Mshv, Hypervisor::Whp] + }; + for &hypervisor in foreign { + let error = probe_hypervisor(hypervisor).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unavailable, "{hypervisor}"); + assert_eq!( + error.to_string(), + platform::probe_hypervisor(hypervisor).unwrap_err(), + "{hypervisor}" + ); + } +} diff --git a/nvx-rs/src/host/store/mod.rs b/nvx-rs/src/host/store/mod.rs index 68c366a8f..912005938 100644 --- a/nvx-rs/src/host/store/mod.rs +++ b/nvx-rs/src/host/store/mod.rs @@ -260,7 +260,24 @@ mod caller; pub(crate) mod files; -mod identity; +/// The identities of runtimes, which [`crate::host::runtime`] defines, and how the one of a record +/// compares with the current one. +mod identity { + pub use crate::host::runtime::{ + ArtifactIdentity, IdentityPart, MAX_IDENTITY_LEN, RuntimeIdentity, + }; + + /// How the runtime identity of a record compares with the current one. + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum IdentityCheck { + /// The identities are equal: the host may reattach to the VM and keep it running. + Matches, + /// The identities differ in the parts that this lists, as + /// [`RuntimeIdentity::differences`] returns them: the VM must be stopped, and only through + /// its verified process identity. + Mismatched(Vec), + } +} mod launch; pub(crate) mod lease; mod lifecycle; diff --git a/nvx-rs/src/host/store/tests.rs b/nvx-rs/src/host/store/tests.rs index 25670c3cd..bffe25bf6 100644 --- a/nvx-rs/src/host/store/tests.rs +++ b/nvx-rs/src/host/store/tests.rs @@ -1,5 +1,5 @@ -//! Tests of the store's identities, records, lock, and launch bookkeeping. The launches of real -//! processes and the recovery of interrupted ones are tested in `tests/store.rs`. +//! Tests of the store's records, lock, and launch bookkeeping. The launches of real processes and +//! the recovery of interrupted ones are tested in `tests/store.rs`. use std::fs; use std::sync::mpsc; @@ -46,93 +46,6 @@ fn write(store: &SandboxStore, name: &str, value: &serde_json::Value) { fs::write(store.path(name), serde_json::to_vec(value).unwrap()).unwrap(); } -#[test] -fn artifact_identities_are_printable_tokens() { - assert_eq!( - ArtifactIdentity::new("openvmm-1.2+build.7") - .unwrap() - .as_str(), - "openvmm-1.2+build.7" - ); - for invalid in [ - String::new(), - "with space".to_owned(), - "tab\there".to_owned(), - "caf\u{e9}".to_owned(), - "x".repeat(MAX_IDENTITY_LEN + 1), - ] { - let error = ArtifactIdentity::new(invalid.clone()).unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unsupported, "{invalid:?}"); - } - assert!(ArtifactIdentity::new("x".repeat(MAX_IDENTITY_LEN)).is_ok()); - - let digest = "AB".repeat(32); - assert_eq!( - ArtifactIdentity::sha256(&digest).unwrap().to_string(), - format!("sha256:{}", "ab".repeat(32)) - ); - for invalid in ["ab".repeat(31), "g".repeat(64), "ab".repeat(33)] { - assert_eq!( - ArtifactIdentity::sha256(&invalid).unwrap_err().kind(), - ErrorKind::Unsupported - ); - } - // The serde form checks the token too. - assert!(serde_json::from_value::(serde_json::json!("a b")).is_err()); -} - -#[test] -fn runtime_identities_bind_this_crates_contracts() { - let identity = runtime(); - assert_eq!(identity.boot_contract, contract::BOOT_CONTRACT_VERSION); - assert_eq!( - identity.control_contract, - contract::CONTROL_CONTRACT_REVISION - ); - assert_eq!( - identity.control_session, - contract::CONTROL_SESSION_PROTOCOL_VERSION - ); - identity.check().unwrap(); - assert!(identity.differences(&runtime()).is_empty()); - - let mut other = runtime(); - other.kernel = artifact('d'); - other.control_session += 1; - assert_eq!( - identity.differences(&other), - [IdentityPart::Kernel, IdentityPart::ControlSession] - ); - other = RuntimeIdentity::new(artifact('e'), artifact('b'), artifact('f')); - other.boot_contract += 1; - other.control_contract = "another-revision".to_owned(); - assert_eq!( - identity.differences(&other), - [ - IdentityPart::OpenVmm, - IdentityPart::Initramfs, - IdentityPart::BootContract, - IdentityPart::ControlContract - ] - ); - let names: Vec<_> = IdentityPart::ALL.iter().map(ToString::to_string).collect(); - assert_eq!( - names, - [ - "OpenVMM", - "kernel", - "initramfs", - "boot contract", - "control contract", - "control session" - ] - ); - for invalid in ["", "two words"] { - other.control_contract = invalid.to_owned(); - assert_eq!(other.check().unwrap_err().kind(), ErrorKind::Unsupported); - } -} - #[test] fn records_keep_their_json_form() { let journal = LaunchJournal::prepared(