diff --git a/nvx-rs/README.md b/nvx-rs/README.md index b774b43e4..f50fef227 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -45,10 +45,10 @@ 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 +│ ├── host/ feature `host` (default): per-domain errors, 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 @@ -117,6 +117,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::error`](#nvxhosterror) | Per-domain errors of the host's operations, each with kinds of its own: configuration, launch, transport, snapshot, restore, and report | | [`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 | @@ -378,6 +379,27 @@ 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::error` + +The errors of the host's operations, one for each domain, which share no +classification with `nvx::Error` or with one another: + +| Error | Domain | Kinds | +| --- | --- | --- | +| `ConfigError` | The checks of a VM's configuration and of the values in it, which read nothing on the host | `Invalid`, `Unsupported` | +| `LaunchError` | A VM's launch, stop, verification, and reattachment, and the probe of its hypervisor | `Unavailable`, `Refused`, `Io`, `Exited`, `RuntimeMismatch`, `StillRunning` | +| `TransportError` | The connection to a VM's control endpoint, and the session over it | `TimedOut`, `Exited`, `Refused`, `Closed`, `Reset`, `Protocol`, `Io` | +| `SnapshotError` | A snapshot's capture, opening, and acceptance of a VM | `Io`, `Malformed`, `Incompatible`, `CaptureFailed` | +| `RestoreError` | A VM's restore from a snapshot | `OperationFailed`, `Expired`, `Cancelled`, `Exited`, `Protocol` | +| `ReportError` | The reading of OpenVMM's outcome report of a VM | `Io`, `Malformed` | + +Each error has a kind from an enum of its own domain, such as +`ConfigErrorKind`, which its `kind` method returns; a message that callers can +show as is, which it displays; and an optional source, which +`std::error::Error::source` returns. The kinds are non-exhaustive, so that each +domain can classify new failures. The host's other operations return +`nvx::Error`, as [Errors](#errors) describes. + ### `nvx::host::artifacts` `Artifacts` (also `nvx::Artifacts`): the OpenVMM executable, guest kernel, and @@ -2504,6 +2526,9 @@ optional cause. Messages call an instance `sandbox NAME`, after the name that `Instance::create` or `Instance::open` gives it. +The per-domain errors of [`nvx::host::error`](#nvxhosterror) are separate: each +has the kinds of its own domain, and none has a code. + ### Error codes An `ErrorCode` is finer than an `ErrorKind`: callers tell errors apart by their diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 161dbb90d..511dc39e8 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -1680,6 +1680,63 @@ fn nvx::host::artifacts::check_manifest fn nvx::host::artifacts::openvmm_executable mod nvx::host::cli fn nvx::host::cli::main +mod nvx::host::error +struct nvx::host::error::ConfigError +method nvx::host::error::ConfigError::kind +method nvx::host::error::ConfigError::new +method nvx::host::error::ConfigError::with_source +enum nvx::host::error::ConfigErrorKind +variant nvx::host::error::ConfigErrorKind::Invalid +variant nvx::host::error::ConfigErrorKind::Unsupported +struct nvx::host::error::LaunchError +method nvx::host::error::LaunchError::kind +method nvx::host::error::LaunchError::new +method nvx::host::error::LaunchError::with_source +enum nvx::host::error::LaunchErrorKind +variant nvx::host::error::LaunchErrorKind::Exited +variant nvx::host::error::LaunchErrorKind::Io +variant nvx::host::error::LaunchErrorKind::Refused +variant nvx::host::error::LaunchErrorKind::RuntimeMismatch +variant nvx::host::error::LaunchErrorKind::StillRunning +variant nvx::host::error::LaunchErrorKind::Unavailable +struct nvx::host::error::ReportError +method nvx::host::error::ReportError::kind +method nvx::host::error::ReportError::new +method nvx::host::error::ReportError::with_source +enum nvx::host::error::ReportErrorKind +variant nvx::host::error::ReportErrorKind::Io +variant nvx::host::error::ReportErrorKind::Malformed +struct nvx::host::error::RestoreError +method nvx::host::error::RestoreError::kind +method nvx::host::error::RestoreError::new +method nvx::host::error::RestoreError::with_source +enum nvx::host::error::RestoreErrorKind +variant nvx::host::error::RestoreErrorKind::Cancelled +variant nvx::host::error::RestoreErrorKind::Exited +variant nvx::host::error::RestoreErrorKind::Expired +variant nvx::host::error::RestoreErrorKind::OperationFailed +variant nvx::host::error::RestoreErrorKind::Protocol +struct nvx::host::error::SnapshotError +method nvx::host::error::SnapshotError::kind +method nvx::host::error::SnapshotError::new +method nvx::host::error::SnapshotError::with_source +enum nvx::host::error::SnapshotErrorKind +variant nvx::host::error::SnapshotErrorKind::CaptureFailed +variant nvx::host::error::SnapshotErrorKind::Incompatible +variant nvx::host::error::SnapshotErrorKind::Io +variant nvx::host::error::SnapshotErrorKind::Malformed +struct nvx::host::error::TransportError +method nvx::host::error::TransportError::kind +method nvx::host::error::TransportError::new +method nvx::host::error::TransportError::with_source +enum nvx::host::error::TransportErrorKind +variant nvx::host::error::TransportErrorKind::Closed +variant nvx::host::error::TransportErrorKind::Exited +variant nvx::host::error::TransportErrorKind::Io +variant nvx::host::error::TransportErrorKind::Protocol +variant nvx::host::error::TransportErrorKind::Refused +variant nvx::host::error::TransportErrorKind::Reset +variant nvx::host::error::TransportErrorKind::TimedOut mod nvx::host::filesystem struct nvx::host::filesystem::Bind field nvx::host::filesystem::Bind::child diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index 6535e67a1..5fadb65f7 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -671,6 +671,63 @@ fn nvx::host::artifacts::check_manifest fn nvx::host::artifacts::openvmm_executable mod nvx::host::cli fn nvx::host::cli::main +mod nvx::host::error +struct nvx::host::error::ConfigError +method nvx::host::error::ConfigError::kind +method nvx::host::error::ConfigError::new +method nvx::host::error::ConfigError::with_source +enum nvx::host::error::ConfigErrorKind +variant nvx::host::error::ConfigErrorKind::Invalid +variant nvx::host::error::ConfigErrorKind::Unsupported +struct nvx::host::error::LaunchError +method nvx::host::error::LaunchError::kind +method nvx::host::error::LaunchError::new +method nvx::host::error::LaunchError::with_source +enum nvx::host::error::LaunchErrorKind +variant nvx::host::error::LaunchErrorKind::Exited +variant nvx::host::error::LaunchErrorKind::Io +variant nvx::host::error::LaunchErrorKind::Refused +variant nvx::host::error::LaunchErrorKind::RuntimeMismatch +variant nvx::host::error::LaunchErrorKind::StillRunning +variant nvx::host::error::LaunchErrorKind::Unavailable +struct nvx::host::error::ReportError +method nvx::host::error::ReportError::kind +method nvx::host::error::ReportError::new +method nvx::host::error::ReportError::with_source +enum nvx::host::error::ReportErrorKind +variant nvx::host::error::ReportErrorKind::Io +variant nvx::host::error::ReportErrorKind::Malformed +struct nvx::host::error::RestoreError +method nvx::host::error::RestoreError::kind +method nvx::host::error::RestoreError::new +method nvx::host::error::RestoreError::with_source +enum nvx::host::error::RestoreErrorKind +variant nvx::host::error::RestoreErrorKind::Cancelled +variant nvx::host::error::RestoreErrorKind::Exited +variant nvx::host::error::RestoreErrorKind::Expired +variant nvx::host::error::RestoreErrorKind::OperationFailed +variant nvx::host::error::RestoreErrorKind::Protocol +struct nvx::host::error::SnapshotError +method nvx::host::error::SnapshotError::kind +method nvx::host::error::SnapshotError::new +method nvx::host::error::SnapshotError::with_source +enum nvx::host::error::SnapshotErrorKind +variant nvx::host::error::SnapshotErrorKind::CaptureFailed +variant nvx::host::error::SnapshotErrorKind::Incompatible +variant nvx::host::error::SnapshotErrorKind::Io +variant nvx::host::error::SnapshotErrorKind::Malformed +struct nvx::host::error::TransportError +method nvx::host::error::TransportError::kind +method nvx::host::error::TransportError::new +method nvx::host::error::TransportError::with_source +enum nvx::host::error::TransportErrorKind +variant nvx::host::error::TransportErrorKind::Closed +variant nvx::host::error::TransportErrorKind::Exited +variant nvx::host::error::TransportErrorKind::Io +variant nvx::host::error::TransportErrorKind::Protocol +variant nvx::host::error::TransportErrorKind::Refused +variant nvx::host::error::TransportErrorKind::Reset +variant nvx::host::error::TransportErrorKind::TimedOut mod nvx::host::filesystem struct nvx::host::filesystem::Bind field nvx::host::filesystem::Bind::child diff --git a/nvx-rs/src/host/error/config.rs b/nvx-rs/src/host/error/config.rs new file mode 100644 index 000000000..12c6efd5d --- /dev/null +++ b/nvx-rs/src/host/error/config.rs @@ -0,0 +1,74 @@ +//! [`ConfigError`]: an error of the checks of a VM's configuration and of the values in it. + +use std::fmt; + +/// What a [`ConfigError`] found wrong with a value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ConfigErrorKind { + /// The value breaks its own rules: it is malformed, such as a label with a space or an address + /// that is not of its form, it lies outside its range, or it combines fields that cannot go + /// together. + Invalid, + /// The value is well formed, but NVX cannot represent or enforce it, such as more egress rules + /// than OpenVMM takes, or a command line longer than its limit. + Unsupported, +} + +/// An error of the checks of a VM's configuration, or of a value in it, which read nothing on the +/// host: its [`kind`](Self::kind), a message, which it displays, and an optional source. +/// +/// The message is a complete sentence fragment, without a trailing period, which callers can show +/// as is. +#[derive(Debug)] +pub struct ConfigError { + /// What the check found wrong. + kind: ConfigErrorKind, + /// Complete, user-facing description without a trailing period. + message: String, + /// Underlying error, when retaining it aids diagnostics. + source: Option>, +} + +impl ConfigError { + /// Creates an error of `kind`, which `message` describes. + pub fn new(kind: ConfigErrorKind, message: impl Into) -> Self { + Self { + kind, + message: message.into(), + source: None, + } + } + + /// Attaches the underlying error that caused this one. + #[must_use] + pub fn with_source( + mut self, + source: impl Into>, + ) -> Self { + self.source = Some(source.into()); + self + } + + /// Returns what the check found wrong. + pub fn kind(&self) -> ConfigErrorKind { + self.kind + } +} + +impl fmt::Display for ConfigError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) + } +} + +impl std::error::Error for ConfigError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source + .as_deref() + .map(|source| source as &(dyn std::error::Error + 'static)) + } +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/error/config/tests.rs b/nvx-rs/src/host/error/config/tests.rs new file mode 100644 index 000000000..dbd660156 --- /dev/null +++ b/nvx-rs/src/host/error/config/tests.rs @@ -0,0 +1,25 @@ +use std::net::{AddrParseError, Ipv4Addr}; + +use super::*; + +#[test] +fn an_error_has_the_kind_that_it_was_created_with() { + for kind in [ConfigErrorKind::Invalid, ConfigErrorKind::Unsupported] { + assert_eq!(ConfigError::new(kind, "a refused value").kind(), kind); + } +} + +#[test] +fn an_error_displays_its_message_and_exposes_its_source() { + let message = "the guest network 10.0.0.256/24 is not an IPv4 address with a prefix length"; + let error = ConfigError::new(ConfigErrorKind::Invalid, message); + assert_eq!(error.to_string(), message); + assert!(std::error::Error::source(&error).is_none()); + + let cause = "10.0.0.256".parse::().unwrap_err(); + let error = error.with_source(cause.clone()); + assert_eq!(error.kind(), ConfigErrorKind::Invalid); + assert_eq!(error.to_string(), message); + let source = std::error::Error::source(&error).unwrap(); + assert_eq!(source.downcast_ref::(), Some(&cause)); +} diff --git a/nvx-rs/src/host/error/launch.rs b/nvx-rs/src/host/error/launch.rs new file mode 100644 index 000000000..6354c1204 --- /dev/null +++ b/nvx-rs/src/host/error/launch.rs @@ -0,0 +1,85 @@ +//! [`LaunchError`]: an error of a VM's launch, stop, verification, or reattachment, or of the probe +//! of its hypervisor. + +use std::fmt; + +/// What a [`LaunchError`] stopped at. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum LaunchErrorKind { + /// A runtime dependency is missing or unusable: the hypervisor, an artifact that the runtime + /// names, or a host that NVX does not support. + Unavailable, + /// The launch refuses its request as given, such as a VM directory that is not private, or a + /// configuration or a snapshot that it cannot launch. + Refused, + /// A host I/O operation failed, such as the creation of the VM's files or the start of its + /// process. + Io, + /// The VM's process exited, as when it fails to start, or when the process that a caller saved + /// is gone. + Exited, + /// The VM runs under another runtime identity than the caller's, so the caller may only stop + /// it, through its verified process. + RuntimeMismatch, + /// The VM's exit was not confirmed by the deadline, so its process may still run. + StillRunning, +} + +/// An error of a VM's launch, stop, verification, or reattachment, or of the probe of its +/// hypervisor: its [`kind`](Self::kind), a message, which it displays, and an optional source. +/// +/// The message is a complete sentence fragment, without a trailing period, which callers can show +/// as is. +#[derive(Debug)] +pub struct LaunchError { + /// What the operation stopped at. + kind: LaunchErrorKind, + /// Complete, user-facing description without a trailing period. + message: String, + /// Underlying error, when retaining it aids diagnostics. + source: Option>, +} + +impl LaunchError { + /// Creates an error of `kind`, which `message` describes. + pub fn new(kind: LaunchErrorKind, message: impl Into) -> Self { + Self { + kind, + message: message.into(), + source: None, + } + } + + /// Attaches the underlying error that caused this one. + #[must_use] + pub fn with_source( + mut self, + source: impl Into>, + ) -> Self { + self.source = Some(source.into()); + self + } + + /// Returns what the operation stopped at. + pub fn kind(&self) -> LaunchErrorKind { + self.kind + } +} + +impl fmt::Display for LaunchError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) + } +} + +impl std::error::Error for LaunchError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source + .as_deref() + .map(|source| source as &(dyn std::error::Error + 'static)) + } +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/error/launch/tests.rs b/nvx-rs/src/host/error/launch/tests.rs new file mode 100644 index 000000000..c3b9764a8 --- /dev/null +++ b/nvx-rs/src/host/error/launch/tests.rs @@ -0,0 +1,33 @@ +use std::io; + +use super::*; + +#[test] +fn an_error_has_the_kind_that_it_was_created_with() { + for kind in [ + LaunchErrorKind::Unavailable, + LaunchErrorKind::Refused, + LaunchErrorKind::Io, + LaunchErrorKind::Exited, + LaunchErrorKind::RuntimeMismatch, + LaunchErrorKind::StillRunning, + ] { + assert_eq!(LaunchError::new(kind, "a failed launch").kind(), kind); + } +} + +#[test] +fn an_error_displays_its_message_and_exposes_its_source() { + let message = "cannot create the log of the VM in /vms/a"; + let error = LaunchError::new(LaunchErrorKind::Io, message); + assert_eq!(error.to_string(), message); + assert!(std::error::Error::source(&error).is_none()); + + let error = error.with_source(io::Error::new(io::ErrorKind::PermissionDenied, "denied")); + assert_eq!(error.kind(), LaunchErrorKind::Io); + assert_eq!(error.to_string(), message); + let source = std::error::Error::source(&error).unwrap(); + let cause = source.downcast_ref::().unwrap(); + assert_eq!(cause.kind(), io::ErrorKind::PermissionDenied); + assert_eq!(cause.to_string(), "denied"); +} diff --git a/nvx-rs/src/host/error/report.rs b/nvx-rs/src/host/error/report.rs new file mode 100644 index 000000000..0e0e724a0 --- /dev/null +++ b/nvx-rs/src/host/error/report.rs @@ -0,0 +1,71 @@ +//! [`ReportError`]: an error of the reading of OpenVMM's outcome report of a VM. + +use std::fmt; + +/// What a [`ReportError`] found wrong with an outcome report. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ReportErrorKind { + /// A host I/O operation failed, such as a read of the report. + Io, + /// The report is not of the form that OpenVMM writes, such as a file that is not JSON. + Malformed, +} + +/// An error of the reading of OpenVMM's outcome report of a VM: its [`kind`](Self::kind), a +/// message, which it displays, and an optional source. +/// +/// The message is a complete sentence fragment, without a trailing period, which callers can show +/// as is. +#[derive(Debug)] +pub struct ReportError { + /// What was found wrong. + kind: ReportErrorKind, + /// Complete, user-facing description without a trailing period. + message: String, + /// Underlying error, when retaining it aids diagnostics. + source: Option>, +} + +impl ReportError { + /// Creates an error of `kind`, which `message` describes. + pub fn new(kind: ReportErrorKind, message: impl Into) -> Self { + Self { + kind, + message: message.into(), + source: None, + } + } + + /// Attaches the underlying error that caused this one. + #[must_use] + pub fn with_source( + mut self, + source: impl Into>, + ) -> Self { + self.source = Some(source.into()); + self + } + + /// Returns what was found wrong. + pub fn kind(&self) -> ReportErrorKind { + self.kind + } +} + +impl fmt::Display for ReportError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) + } +} + +impl std::error::Error for ReportError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source + .as_deref() + .map(|source| source as &(dyn std::error::Error + 'static)) + } +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/error/report/tests.rs b/nvx-rs/src/host/error/report/tests.rs new file mode 100644 index 000000000..273155cdd --- /dev/null +++ b/nvx-rs/src/host/error/report/tests.rs @@ -0,0 +1,25 @@ +use super::*; + +#[test] +fn an_error_has_the_kind_that_it_was_created_with() { + for kind in [ReportErrorKind::Io, ReportErrorKind::Malformed] { + assert_eq!(ReportError::new(kind, "an unreadable report").kind(), kind); + } +} + +#[test] +fn an_error_displays_its_message_and_exposes_its_source() { + let message = "the outcome report /vms/a/outcome.json is not JSON"; + let error = ReportError::new(ReportErrorKind::Malformed, message); + assert_eq!(error.to_string(), message); + assert!(std::error::Error::source(&error).is_none()); + + let cause = serde_json::from_str::("{ truncated").unwrap_err(); + let cause_text = cause.to_string(); + let error = error.with_source(cause); + assert_eq!(error.kind(), ReportErrorKind::Malformed); + assert_eq!(error.to_string(), message); + let source = std::error::Error::source(&error).unwrap(); + let cause = source.downcast_ref::().unwrap(); + assert_eq!(cause.to_string(), cause_text); +} diff --git a/nvx-rs/src/host/error/restore.rs b/nvx-rs/src/host/error/restore.rs new file mode 100644 index 000000000..dd2e88f08 --- /dev/null +++ b/nvx-rs/src/host/error/restore.rs @@ -0,0 +1,79 @@ +//! [`RestoreError`]: an error of a VM's restore from a snapshot. + +use std::fmt; + +/// What ended a restore that failed with a [`RestoreError`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum RestoreErrorKind { + /// An operation that the caller performed for the restore failed, such as a connection to the + /// VM or a read from it. + OperationFailed, + /// The restore's deadline passed. + Expired, + /// The caller cancelled the restore. + Cancelled, + /// The VM's process exited before the restore ended. + Exited, + /// OpenVMM or the guest broke the restore's protocol, such as with a Ready that reports another + /// transaction. + Protocol, +} + +/// An error of a VM's restore from a snapshot: its [`kind`](Self::kind), a message, which it +/// displays, and an optional source. +/// +/// The message is a complete sentence fragment, without a trailing period, which callers can show +/// as is. +#[derive(Debug)] +pub struct RestoreError { + /// What ended the restore. + kind: RestoreErrorKind, + /// Complete, user-facing description without a trailing period. + message: String, + /// Underlying error, when retaining it aids diagnostics. + source: Option>, +} + +impl RestoreError { + /// Creates an error of `kind`, which `message` describes. + pub fn new(kind: RestoreErrorKind, message: impl Into) -> Self { + Self { + kind, + message: message.into(), + source: None, + } + } + + /// Attaches the underlying error that caused this one. + #[must_use] + pub fn with_source( + mut self, + source: impl Into>, + ) -> Self { + self.source = Some(source.into()); + self + } + + /// Returns what ended the restore. + pub fn kind(&self) -> RestoreErrorKind { + self.kind + } +} + +impl fmt::Display for RestoreError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) + } +} + +impl std::error::Error for RestoreError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source + .as_deref() + .map(|source| source as &(dyn std::error::Error + 'static)) + } +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/error/restore/tests.rs b/nvx-rs/src/host/error/restore/tests.rs new file mode 100644 index 000000000..e7ffc8753 --- /dev/null +++ b/nvx-rs/src/host/error/restore/tests.rs @@ -0,0 +1,32 @@ +use std::io; + +use super::*; + +#[test] +fn an_error_has_the_kind_that_it_was_created_with() { + for kind in [ + RestoreErrorKind::OperationFailed, + RestoreErrorKind::Expired, + RestoreErrorKind::Cancelled, + RestoreErrorKind::Exited, + RestoreErrorKind::Protocol, + ] { + assert_eq!(RestoreError::new(kind, "a failed restore").kind(), kind); + } +} + +#[test] +fn an_error_displays_its_message_and_exposes_its_source() { + let message = "the connection to the VM's control endpoint failed during the restore"; + let error = RestoreError::new(RestoreErrorKind::OperationFailed, message); + assert_eq!(error.to_string(), message); + assert!(std::error::Error::source(&error).is_none()); + + let error = error.with_source(io::Error::new(io::ErrorKind::ConnectionRefused, "refused")); + assert_eq!(error.kind(), RestoreErrorKind::OperationFailed); + assert_eq!(error.to_string(), message); + let source = std::error::Error::source(&error).unwrap(); + let cause = source.downcast_ref::().unwrap(); + assert_eq!(cause.kind(), io::ErrorKind::ConnectionRefused); + assert_eq!(cause.to_string(), "refused"); +} diff --git a/nvx-rs/src/host/error/snapshot.rs b/nvx-rs/src/host/error/snapshot.rs new file mode 100644 index 000000000..74b2d19c7 --- /dev/null +++ b/nvx-rs/src/host/error/snapshot.rs @@ -0,0 +1,80 @@ +//! [`SnapshotError`]: an error of a snapshot's capture, of its opening, or of its acceptance of a +//! VM. + +use std::fmt; + +/// What a [`SnapshotError`] found wrong with a snapshot or its capture. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum SnapshotErrorKind { + /// A host I/O operation failed, such as a read or a write of the snapshot's files, or a read of + /// the host's clock. + Io, + /// The snapshot is not in its strict format: an entry is missing, extra, not a regular file, or + /// not private, a length differs from its manifest's, or the manifest or the key is malformed + /// or does not match the other. + Malformed, + /// The snapshot cannot restore the VM here: the VM's configuration, the runtime, the host, or + /// the time is not one that the snapshot accepts. + Incompatible, + /// The capture produced no snapshot: its VM did not end with a successful capture. + CaptureFailed, +} + +/// An error of a snapshot's capture, of its opening, or of its acceptance of a VM: its +/// [`kind`](Self::kind), a message, which it displays, and an optional source. +/// +/// The message is a complete sentence fragment, without a trailing period, which callers can show +/// as is. +#[derive(Debug)] +pub struct SnapshotError { + /// What was found wrong. + kind: SnapshotErrorKind, + /// Complete, user-facing description without a trailing period. + message: String, + /// Underlying error, when retaining it aids diagnostics. + source: Option>, +} + +impl SnapshotError { + /// Creates an error of `kind`, which `message` describes. + pub fn new(kind: SnapshotErrorKind, message: impl Into) -> Self { + Self { + kind, + message: message.into(), + source: None, + } + } + + /// Attaches the underlying error that caused this one. + #[must_use] + pub fn with_source( + mut self, + source: impl Into>, + ) -> Self { + self.source = Some(source.into()); + self + } + + /// Returns what was found wrong. + pub fn kind(&self) -> SnapshotErrorKind { + self.kind + } +} + +impl fmt::Display for SnapshotError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) + } +} + +impl std::error::Error for SnapshotError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source + .as_deref() + .map(|source| source as &(dyn std::error::Error + 'static)) + } +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/error/snapshot/tests.rs b/nvx-rs/src/host/error/snapshot/tests.rs new file mode 100644 index 000000000..136d6ab8e --- /dev/null +++ b/nvx-rs/src/host/error/snapshot/tests.rs @@ -0,0 +1,31 @@ +use std::io; + +use super::*; + +#[test] +fn an_error_has_the_kind_that_it_was_created_with() { + for kind in [ + SnapshotErrorKind::Io, + SnapshotErrorKind::Malformed, + SnapshotErrorKind::Incompatible, + SnapshotErrorKind::CaptureFailed, + ] { + assert_eq!(SnapshotError::new(kind, "a refused snapshot").kind(), kind); + } +} + +#[test] +fn an_error_displays_its_message_and_exposes_its_source() { + let message = "cannot read the manifest of the snapshot in /snapshots/a"; + let error = SnapshotError::new(SnapshotErrorKind::Io, message); + assert_eq!(error.to_string(), message); + assert!(std::error::Error::source(&error).is_none()); + + let error = error.with_source(io::Error::new(io::ErrorKind::NotFound, "missing")); + assert_eq!(error.kind(), SnapshotErrorKind::Io); + assert_eq!(error.to_string(), message); + let source = std::error::Error::source(&error).unwrap(); + let cause = source.downcast_ref::().unwrap(); + assert_eq!(cause.kind(), io::ErrorKind::NotFound); + assert_eq!(cause.to_string(), "missing"); +} diff --git a/nvx-rs/src/host/error/transport.rs b/nvx-rs/src/host/error/transport.rs new file mode 100644 index 000000000..779d1f170 --- /dev/null +++ b/nvx-rs/src/host/error/transport.rs @@ -0,0 +1,84 @@ +//! [`TransportError`]: an error of the connection to a VM's control endpoint, or of the session +//! over it. + +use std::fmt; + +/// What ended a [`TransportError`]'s connection or operation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum TransportErrorKind { + /// A deadline passed, such as that of a connection while the endpoint does not exist yet, or + /// that of a read. + TimedOut, + /// The VM's process exited, so it will not serve its endpoint. + Exited, + /// The endpoint refused the host: a process other than the VM's serves it, or the session + /// refused the host's capability. + Refused, + /// The peer closed the stream, as OpenVMM does when the VM shuts down. + Closed, + /// The session was reset, which ended the host's attachment to it. + Reset, + /// The peer broke the protocol, or the stream ended inside a record. + Protocol, + /// The transport failed. + Io, +} + +/// An error of the connection to a VM's control endpoint, or of the session over it: its +/// [`kind`](Self::kind), a message, which it displays, and an optional source. +/// +/// The message is a complete sentence fragment, without a trailing period, which callers can show +/// as is. +#[derive(Debug)] +pub struct TransportError { + /// What ended the connection or the operation. + kind: TransportErrorKind, + /// Complete, user-facing description without a trailing period. + message: String, + /// Underlying error, when retaining it aids diagnostics. + source: Option>, +} + +impl TransportError { + /// Creates an error of `kind`, which `message` describes. + pub fn new(kind: TransportErrorKind, message: impl Into) -> Self { + Self { + kind, + message: message.into(), + source: None, + } + } + + /// Attaches the underlying error that caused this one. + #[must_use] + pub fn with_source( + mut self, + source: impl Into>, + ) -> Self { + self.source = Some(source.into()); + self + } + + /// Returns what ended the connection or the operation. + pub fn kind(&self) -> TransportErrorKind { + self.kind + } +} + +impl fmt::Display for TransportError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) + } +} + +impl std::error::Error for TransportError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source + .as_deref() + .map(|source| source as &(dyn std::error::Error + 'static)) + } +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/error/transport/tests.rs b/nvx-rs/src/host/error/transport/tests.rs new file mode 100644 index 000000000..f6dd6ce3b --- /dev/null +++ b/nvx-rs/src/host/error/transport/tests.rs @@ -0,0 +1,37 @@ +use std::io; + +use super::*; + +#[test] +fn an_error_has_the_kind_that_it_was_created_with() { + for kind in [ + TransportErrorKind::TimedOut, + TransportErrorKind::Exited, + TransportErrorKind::Refused, + TransportErrorKind::Closed, + TransportErrorKind::Reset, + TransportErrorKind::Protocol, + TransportErrorKind::Io, + ] { + assert_eq!( + TransportError::new(kind, "a failed connection").kind(), + kind + ); + } +} + +#[test] +fn an_error_displays_its_message_and_exposes_its_source() { + let message = "the transport to the control endpoint failed"; + let error = TransportError::new(TransportErrorKind::Io, message); + assert_eq!(error.to_string(), message); + assert!(std::error::Error::source(&error).is_none()); + + let error = error.with_source(io::Error::new(io::ErrorKind::ConnectionReset, "reset")); + assert_eq!(error.kind(), TransportErrorKind::Io); + assert_eq!(error.to_string(), message); + let source = std::error::Error::source(&error).unwrap(); + let cause = source.downcast_ref::().unwrap(); + assert_eq!(cause.kind(), io::ErrorKind::ConnectionReset); + assert_eq!(cause.to_string(), "reset"); +} diff --git a/nvx-rs/src/host/mod.rs b/nvx-rs/src/host/mod.rs index 2ba935d71..3fe73612e 100644 --- a/nvx-rs/src/host/mod.rs +++ b/nvx-rs/src/host/mod.rs @@ -41,6 +41,9 @@ //! and [`sandbox::scratch`] binds and claims a scratch image. A //! [`PreparedLaunch`](sandbox::PreparedLaunch) hands the VMM the files that those checks //! opened, which it inherits through the [`supervisor`]'s allowlist. +//! - [`error`] holds the errors of the host's domains, each with kinds of its own: a VM's +//! configuration, its launch, the transport to it, its snapshots and restores, and its outcome +//! report. //! - `cli` (feature `cli`) is the `nvx-rs` command-line interface, which runs microVMs and //! sandboxes, looks up and downloads artifacts, and compiles egress policies. //! @@ -63,6 +66,40 @@ pub mod artifacts; #[cfg(feature = "cli")] pub mod cli; +pub mod error { + //! The errors of the host's operations, one for each domain: + //! + //! - [`ConfigError`]: the checks of a VM's configuration and of the values in it, which read + //! nothing on the host; + //! - [`LaunchError`]: a VM's launch, stop, verification, and reattachment, and the probe of + //! its hypervisor; + //! - [`TransportError`]: the connection to a VM's control endpoint, and the session over it; + //! - [`SnapshotError`]: a snapshot's capture, opening, and acceptance of a VM; + //! - [`RestoreError`]: a VM's restore from a snapshot; + //! - [`ReportError`]: the reading of OpenVMM's outcome report of a VM. + //! + //! Each has a kind from a small enum of its own domain, such as [`ConfigErrorKind`], which its + //! `kind` method returns, a message, which it displays, and an optional source, which + //! [`source`](std::error::Error::source) returns. Each domain classifies only its own + //! failures, and its kinds are non-exhaustive, so that it can classify new ones. The host's + //! other operations return [`crate::Error`]. + //! + //! The source of each error is a file of its own in this module's folder, `host/error/`. + + mod config; + mod launch; + mod report; + mod restore; + mod snapshot; + mod transport; + + pub use config::{ConfigError, ConfigErrorKind}; + pub use launch::{LaunchError, LaunchErrorKind}; + pub use report::{ReportError, ReportErrorKind}; + pub use restore::{RestoreError, RestoreErrorKind}; + pub use snapshot::{SnapshotError, SnapshotErrorKind}; + pub use transport::{TransportError, TransportErrorKind}; +} pub mod filesystem; pub mod instance; pub mod launch;