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
182 changes: 155 additions & 27 deletions tests/runtime-adapter/README.md

Large diffs are not rendered by default.

213 changes: 173 additions & 40 deletions tests/runtime-adapter/src/bin/runtime-adapter-host.rs

Large diffs are not rendered by default.

17 changes: 10 additions & 7 deletions tests/runtime-adapter/src/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,21 @@ const PAUSE_POLL: Duration = Duration::from_millis(10);
/// then, and before what it does next.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Step {
/// `checked`: the launch has checked the bindings of its images, and holds the checked files,
/// which claim the scratch image, but has begun nothing in the sandbox's store, and has spawned
/// nothing. A launch without a layered root, which checks nothing, reaches it too.
/// `checked`: the launch has checked the bindings of its images, and the export of its shares,
/// if any, and holds the checked files, which claim the scratch image, but has begun nothing
/// in the sandbox's store, and has spawned nothing. A launch without a layered root, which
/// checks nothing, reaches it too.
Checked,
/// `spawned`: the VMM runs, and the launch's journal records it, but the launch has not
/// connected to the VM.
Spawned,
/// `ready`: the guest has assembled the layered root, and the launch has released the checked
/// files. A launch without a layered root never reaches it.
/// `ready`: the guest agent, of the build that the host ships, has assembled the layered root,
/// and the launch has released the checked files, but has sent neither the mapping table nor
/// `Start`. A launch without a layered root never reaches it.
Ready,
/// `commit`: the guest agent has answered, and the launch is about to commit, so its journal
/// still records the VMM, which no runtime record names yet.
/// `commit`: the guest has started the sandbox, if the VM has a layered root, the guest agent
/// has answered, and the launch is about to commit, so its journal still records the VMM,
/// which no runtime record names yet.
Commit,
}

Expand Down
116 changes: 113 additions & 3 deletions tests/runtime-adapter/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,43 @@
//! control session between the host and the guest, and exchanges the adapter's own
//! [framing](crate::frame) with the guest agent, through `nvx::host::nvxs` alone, over any platform
//! transport.
//!
//! # The host's order
//!
//! A VM with a layered root is ready for workloads once its guest assembled the root and applied
//! the host's mapping table. The host runs that order over a [`Client`] in two steps, between
//! which it may do what the root's readiness allows, such as releasing the files that it handed
//! the VMM:
//!
//! 1. [`ready`] sends the root configuration, and waits for the guest agent's `Ready`, whose
//! build must be the one that the host ships: the host sends a guest agent of another build
//! nothing more, since it cannot tell what that build would do with it.
//! 2. [`start_sandbox`] sends the mapping table, in order, in parts that the guest applies one
//! after the other, then `Start`, which the guest confirms with `Started`.
//!
//! A part that the guest fails ends the order: the host sends no more of the table, nor `Start`,
//! so the guest starts no workload on a partial table, and asks the guest to stop, which tears
//! its root down, as NVX's managed lifecycle stops a guest whose mappings failed. The caller then
//! waits for the VM to end, and ends a VM whose guest did not stop, as it ends one after any other
//! failure of the order, an [`OrderError`].

mod map_reply;
mod order_error;

use std::fmt;
use std::time::Instant;

use nvx::host::nvxs::{HostConnection, HostConnectionError};
use nvx::host::platform::Transport;
use nvx::protocol::CAPABILITY_LEN;
use nvx::protocol::nvxs::Identity;
use nvx::protocol::{CAPABILITY_LEN, HostMap};

use crate::frame::{BodyError, Decoder, FrameError, Message};
use crate::probe::{Facts, Probe, ProbeReply};
use crate::root::{Ready, RootConfig, RootFailure, RootReply};
use crate::root::{MapFailure, Ready, RootConfig, RootFailure, RootReply, encode_maps};

pub use map_reply::MapReply;
pub use order_error::OrderError;

/// A failure of a [`Client`].
#[derive(Debug)]
Expand Down Expand Up @@ -91,7 +116,8 @@ impl From<FrameError> for ClientError {
/// message in as many of the session's records as it takes, and [`receive`](Self::receive)
/// returns the guest agent's next message, whether it spans records or shares one with the next
/// message. [`ping`](Self::ping), [`echo`](Self::echo), [`configure_root`](Self::configure_root),
/// [`probe`](Self::probe), and [`stop`](Self::stop) each send a request and wait for its answer.
/// [`map`](Self::map), [`start`](Self::start), [`probe`](Self::probe), and [`stop`](Self::stop)
/// each send a request and wait for its answer.
///
/// Besides the session's own buffers, the client holds at most the rest of one of the guest's
/// records and one partial message, whose header [`Decoder`] checks before it takes any byte of
Expand Down Expand Up @@ -246,6 +272,35 @@ impl Client {
}
}

/// Sends `part`, a part of the host's mapping table as [`encode_maps`] encodes it, in a
/// `Maps`, and waits until `deadline` for the guest agent's answer: `Mapped` once it applied the
/// part's mappings, or `MapFailed` once it refused the part or failed to apply one of them,
/// after which it refuses the rest of the table and the start.
///
/// A part of more than [`MAX_BODY`](crate::frame::MAX_BODY) bytes fails before anything is
/// sent, and the client stays usable.
pub fn map(&mut self, part: &[u8], deadline: Instant) -> Result<MapReply, ClientError> {
let request = Message::Maps(part.to_vec());
match self.request(&request, deadline)? {
Message::Mapped => Ok(MapReply::Mapped),
Message::MapFailed(body) => {
self.decoded(MapFailure::decode(&body).map(MapReply::Failed))
}
reply => Err(self.mismatched(&request, &reply)),
}
}

/// Sends `Start`, and waits until `deadline` for the guest agent's `Started`, with which it
/// confirms that it started the sandbox, once it applied every mapping that the root
/// configuration announced.
pub fn start(&mut self, deadline: Instant) -> Result<(), ClientError> {
let request = Message::Start;
match self.request(&request, deadline)? {
Message::Started => Ok(()),
reply => Err(self.mismatched(&request, &reply)),
}
}

/// Sends `probe` to the guest agent of a VM with a layered root, and waits until `deadline`
/// for its answer: the facts that the probe found, or the problem.
pub fn probe(&mut self, probe: &Probe, deadline: Instant) -> Result<ProbeReply, ClientError> {
Expand Down Expand Up @@ -330,3 +385,58 @@ impl fmt::Debug for Client {
.finish_non_exhaustive()
}
}

/// Runs the first step of the host's order of a VM with a layered root over `client`: sends the
/// root configuration `config`, and waits until `deadline` for the guest agent's `Ready`, which it
/// returns once it names `build`, the guest agent's build that the host ships.
///
/// Fails with [`OrderError::Root`] once the guest reports that a stage of the root failed, with
/// [`OrderError::Build`] for a guest agent of another build, to which it sends nothing more, and
/// with [`OrderError::Client`] once the client fails.
pub fn ready(
client: &mut Client,
config: &RootConfig,
build: &str,
deadline: Instant,
) -> Result<Ready, OrderError> {
match client
.configure_root(config, deadline)
.map_err(OrderError::Client)?
{
RootReply::Ready(ready) if ready.build() == build => Ok(ready),
RootReply::Ready(ready) => Err(OrderError::Build {
reported: ready.build().to_owned(),
expected: build.to_owned(),
}),
RootReply::Failed(failure) => Err(OrderError::Root(failure)),
}
}

/// Runs the rest of the host's order over `client`, once the guest agent is [`ready`]: sends the
/// mapping table `table`, which the root configuration announced, in the parts that
/// [`encode_maps`] splits it into, each once the guest applied the one before, then `Start`, and
/// waits until `deadline` for `Started`. A table of no mapping sends no part.
///
/// Fails with [`OrderError::Table`], before it sends anything, for a table that does not encode;
/// with [`OrderError::Mapping`] once the guest fails a part, after which it sends no more of the
/// table, nor `Start`, but asks the guest to stop; and with [`OrderError::Client`] once the client
/// fails.
pub fn start_sandbox(
client: &mut Client,
table: &[HostMap],
deadline: Instant,
) -> Result<(), OrderError> {
let parts = encode_maps(table).map_err(OrderError::Table)?;
for part in &parts {
match client.map(part, deadline).map_err(OrderError::Client)? {
MapReply::Mapped => {}
MapReply::Failed(failure) => {
// The guest starts no workload on a partial table, and tears its root down as it
// stops, which unmounts the mappings that it applied.
let stop = client.stop(deadline);
return Err(OrderError::Mapping { failure, stop });
}
}
}
client.start(deadline).map_err(OrderError::Client)
}
14 changes: 14 additions & 0 deletions tests/runtime-adapter/src/host/map_reply.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//! How the guest agent answers a part of the host's mapping table, which
//! [`Client::map`](super::Client::map) returns: a [`MapReply`].

use crate::root::MapFailure;

/// How the guest agent answered a part of the host's mapping table.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MapReply {
/// `Mapped`: it applied the part's mappings.
Mapped,
/// `MapFailed`: it refused the part, or failed to apply one of its mappings, after which it
/// refuses the rest of the table and the start.
Failed(MapFailure),
}
76 changes: 76 additions & 0 deletions tests/runtime-adapter/src/host/order_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! How the [host's order](super#the-hosts-order) of a VM with a layered root fails, which
//! [`ready`](super::ready) and [`start_sandbox`](super::start_sandbox) return: an
//! [`OrderError`].

use std::fmt;

use super::ClientError;
use crate::frame::BodyError;
use crate::root::{MapFailure, RootFailure};

/// A failure of the [host's order](super#the-hosts-order) of a VM with a layered root: what the
/// guest agent reported, or how the client failed, and what the host did about it.
#[derive(Debug)]
#[non_exhaustive]
pub enum OrderError {
/// The guest agent reported that a stage of the root failed, after which it ends.
Root(RootFailure),
/// The guest agent assembled the root, but is of another build than the one that the host
/// ships, so the host sent it nothing more.
Build {
/// The build that the guest agent reported in its `Ready`.
reported: String,
/// The build that the host ships.
expected: String,
},
/// The guest agent failed the mapping table, so the host sent it no more of the table, nor
/// `Start`, and asked it to stop, which tears its root down.
Mapping {
/// How the table failed.
failure: MapFailure,
/// How the stop ended, as [`Client::stop`](super::Client::stop) returns it: the failure
/// of the root's teardown, if the guest reported one before it confirmed the stop.
stop: Result<Option<RootFailure>, ClientError>,
},
/// The mapping table does not encode, so the host sent nothing of it.
Table(BodyError),
/// The client failed, which ended it.
Client(ClientError),
}

impl fmt::Display for OrderError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Root(failure) => failure.fmt(formatter),
Self::Build { reported, expected } => write!(
formatter,
"the guest agent is of build {reported}, not of the host's build {expected}"
),
Self::Mapping { failure, stop } => {
failure.fmt(formatter)?;
match stop {
Ok(None) => Ok(()),
Ok(Some(teardown)) => write!(formatter, "; then {teardown}"),
Err(error) => {
write!(
formatter,
"; the guest agent did not confirm the stop: {error}"
)
}
}
}
Self::Table(error) => error.fmt(formatter),
Self::Client(error) => error.fmt(formatter),
}
}
}

impl std::error::Error for OrderError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Table(error) => Some(error),
Self::Client(error) => Some(error),
Self::Root(_) | Self::Build { .. } | Self::Mapping { .. } => None,
}
}
}
Loading
Loading