use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration;
use arcbox_vm_driver::net::{
AttachMode, GuestNetwork, HostIngress, NetworkIdentity, NetworkLease, NetworkMode,
NetworkPolicy, NetworkReconcile,
};
use arcbox_vm_driver::{
CheckpointFormat, CheckpointImage, CheckpointKind, DiskSource, IsolationSpec, NicSpec, Prepare,
PreparedVm, Staging, VmDriver, VmHandle, VmId,
};
use chrono::{DateTime, Utc};
use tokio::sync::broadcast;
use tracing::{debug, error, info, warn};
use uuid::Uuid;
use crate::agent::{ExecInputMsg, ExitStatus, OutputChunk, PortWait, StartCommand};
use crate::agent::{GuestAgent, Readiness};
use crate::config::RuntimeConfig;
use crate::environment::NodeEnvironment;
use crate::error::{ComputerError, Result};
use crate::lifecycle::actor::{
Command, ComputerActor, ComputerSeed, ComputerSnapshot, Deadlines, Mailbox, Seeded,
};
use crate::lifecycle::event::{Provision, RestoreOrigin};
use crate::lifecycle::flows::{BootLaunch, ComputerFlows, ComputerServices, Launch, RestoreLaunch};
use crate::lifecycle::runtime::{ComputerRuntime, Runtime};
use crate::sandbox::reconcile::JournaledVmm;
use crate::snapshot::{SnapshotCatalog, SnapshotMeta};
use crate::snapshot_cow::{CowHandle, CowManager};
use crate::template_catalog::TemplateCatalog;
pub(crate) mod boot;
mod checkpoint;
pub(crate) mod cleanup;
pub(crate) mod events;
mod execution;
mod files;
mod lifecycle;
pub(crate) mod pause;
pub(crate) mod policy;
pub(crate) mod pool;
pub(crate) mod reconcile;
pub(crate) mod record;
pub(crate) mod spec;
mod storage;
mod templates;
mod timers;
pub(crate) mod types;
pub(crate) mod warm;
pub(crate) mod workload;
pub use execution::{
ExecutionChannel, ExecutionOutput, ExecutionSnapshot, ExecutionSpec, StdinState,
};
pub use pause::reason as pause_reason;
pub(crate) use spec::ROOTFS_DISK_ID;
pub(crate) use types::NetworkAttachment;
pub use types::{
CheckpointInfo, CheckpointSummary, ComputerEvent, ComputerId, ComputerInfo, ComputerMountSpec,
ComputerNetworkInfo, ComputerNetworkSpec, ComputerSpec, ComputerState, ComputerSummary,
IdleAction, LifecycleUpdate, RestoreComputerSpec, TemplateWarmRef,
};
const EVENT_CHANNEL_CAPACITY: usize = 256;
type ReconcileResult = std::result::Result<(), Arc<str>>;
#[derive(Clone)]
pub(crate) struct ComputerRef {
mailbox: Mailbox,
snapshot: tokio::sync::watch::Receiver<ComputerSnapshot>,
incarnation: Uuid,
}
pub(crate) type Computers = Arc<RwLock<HashMap<ComputerId, ComputerRef>>>;
pub struct ComputerManager {
computers: Computers,
records: Arc<record::ComputerRecordStore>,
services: Arc<ComputerServices>,
snapshots: Arc<SnapshotCatalog>,
templates: Arc<TemplateCatalog>,
config: Arc<RuntimeConfig>,
events: Arc<events::EventBus>,
cow_manager: Arc<CowManager>,
pool: Arc<pool::SlotPool>,
warm: Arc<warm::WarmCache>,
executions: Arc<execution::ExecutionRegistry>,
reconcile_done: tokio::sync::watch::Receiver<Option<ReconcileResult>>,
timers_enabled: tokio::sync::watch::Sender<bool>,
}
impl ComputerManager {
pub fn new(config: RuntimeConfig, environment: NodeEnvironment) -> Result<Self> {
let NodeEnvironment {
driver,
network,
agent,
cow_manager,
} = environment;
let capabilities = driver.capabilities();
for (missing, capability, need) in [
(
driver.prepare().is_none(),
"prepare",
"the boot and pool flows spawn the VMM ahead of the guest",
),
(
!capabilities.staging,
"staging",
"every flow brings the files its computer boots from into the area the VMM \
can reach, and pause takes the disk back out of it",
),
(
!capabilities.vsock,
"vsock",
"the guest agent is reached over vsock",
),
(
matches!(agent.readiness(), Readiness::DialOut { .. })
&& !capabilities.vsock_listen,
"vsock_listen",
"the readiness gate is the guest's vsock dial-out",
),
] {
if missing {
return Err(ComputerError::Config(format!(
"VM driver `{}` has no {capability} capability; {need}",
driver.name()
)));
}
}
let records = Arc::new(record::ComputerRecordStore::new(Path::new(
&config.firecracker.data_dir,
))?);
drop(records.load_all()?);
if network.reconcile().is_none() {
return Err(ComputerError::Config(
"guest network has no reconcile capability; the quarantine ledger is how a host \
releases the addresses a previous process held"
.into(),
));
}
let snapshots = Arc::new(SnapshotCatalog::new(&config.firecracker.data_dir));
let templates = Arc::new(TemplateCatalog::new(&config.firecracker.data_dir));
let events = Arc::new(events::EventBus::new(EVENT_CHANNEL_CAPACITY));
let config = Arc::new(config);
let (reconcile_tx, reconcile_done) = tokio::sync::watch::channel(None);
let executions = Arc::new(execution::ExecutionRegistry::default());
let computers: Computers = Arc::new(RwLock::new(HashMap::new()));
let pool = Arc::new(pool::SlotPool::default());
let (timers_enabled, timers_gate) = tokio::sync::watch::channel(false);
let services = Arc::new(ComputerServices {
driver,
network,
agents: agent,
config: Arc::clone(&config),
cow_manager: Arc::clone(&cow_manager),
records: Arc::clone(&records),
snapshots: Arc::clone(&snapshots),
events: Arc::clone(&events),
pool: Arc::clone(&pool),
});
if tokio::runtime::Handle::try_current().is_ok() {
let config = Arc::clone(&config);
let cow_manager = Arc::clone(&cow_manager);
let snapshots = Arc::clone(&snapshots);
let records = Arc::clone(&records);
let computers = Arc::clone(&computers);
let services = Arc::clone(&services);
let network = Arc::clone(&services.network);
let driver = Arc::clone(&services.driver);
tokio::spawn(async move {
let result = async {
let mut swept = reconcile::sweep_orphans(
&config,
driver.as_ref(),
&*network,
&cow_manager,
&snapshots,
&records,
)
.await?;
let mut runtime = swept.take_runtime();
let mut inactive = Vec::new();
let normalized = reconcile::normalize_durable_records(
&records,
Path::new(&config.firecracker.data_dir),
Some(&mut runtime),
&mut inactive,
);
reconcile::release_unclaimed(&mut runtime, &*network, &cow_manager).await;
if let Err(error) = normalized {
reconcile::release_instances(&mut inactive, &*network, &cow_manager).await;
return Err(error);
}
reconcile::seed_computers(inactive, &computers, &services, &timers_gate);
reconcile::finalize_sweep(swept).await?;
reconcile_capability(&*network).replay_complete();
Ok::<_, ComputerError>(())
}
.await
.map_err(|error| Arc::<str>::from(error.to_string()));
let _ = reconcile_tx.send(Some(result));
});
execution::spawn_teardown_purge(Arc::clone(&executions), events.subscribe());
} else {
let mut inactive = Vec::new();
reconcile::normalize_durable_records(
&records,
Path::new(&config.firecracker.data_dir),
None,
&mut inactive,
)?;
reconcile_capability(&*services.network).replay_complete();
reconcile::seed_computers(inactive, &computers, &services, &timers_enabled.subscribe());
let _ = reconcile_tx.send(Some(Ok(())));
}
Ok(Self {
computers,
records,
services,
snapshots,
templates,
config,
events,
cow_manager,
pool,
warm: Arc::new(warm::WarmCache::default()),
executions,
reconcile_done,
timers_enabled,
})
}
#[must_use]
pub fn into_shared(self) -> Arc<Self> {
self.timers_enabled.send_replace(true);
Arc::new(self)
}
pub(super) fn spawn_context(
&self,
) -> (Arc<ComputerServices>, tokio::sync::watch::Receiver<bool>) {
(Arc::clone(&self.services), self.timers_enabled.subscribe())
}
pub(super) fn computer(&self, id: &ComputerId) -> Result<ComputerRef> {
self.check_reconcile()?;
self.computers
.read()
.unwrap()
.get(id)
.cloned()
.ok_or_else(|| ComputerError::NotFound(id.clone()))
}
pub(super) fn mailbox(&self, id: &ComputerId) -> Result<Mailbox> {
Ok(self.computer(id)?.mailbox)
}
pub(super) fn snapshot(&self, id: &ComputerId) -> Result<ComputerSnapshot> {
Ok(self.computer(id)?.snapshot.borrow().clone())
}
pub(super) async fn await_reconcile(&self) -> Result<()> {
let mut rx = self.reconcile_done.clone();
let result = rx
.wait_for(Option::is_some)
.await
.map_err(|error| {
ComputerError::Other(format!("sandbox reconciliation stopped: {error}"))
})?
.clone()
.expect("wait_for returned only after reconciliation completed");
result.map_err(|error| {
ComputerError::Other(format!(
"sandbox durable-state reconciliation failed: {error}"
))
})
}
fn check_reconcile(&self) -> Result<()> {
let result = self.reconcile_done.borrow().clone();
match result {
None => Err(ComputerError::Other(
"sandbox durable-state reconciliation is still running".into(),
)),
Some(Ok(())) => Ok(()),
Some(Err(error)) => Err(ComputerError::Other(format!(
"sandbox durable-state reconciliation failed: {error}"
))),
}
}
pub async fn pending_network_cleanups(&self) -> Result<Vec<(String, String)>> {
self.await_reconcile().await?;
Ok(self
.reconcile_network()
.pending_cleanups()
.await
.map_err(ComputerError::from)?
.into_iter()
.map(|(vm, token)| (vm.as_str().to_owned(), token))
.collect())
}
pub async fn validate_network_cleanup(&self, id: &str, token: &str) -> Result<NetworkLease> {
self.await_reconcile().await?;
Ok(self
.reconcile_network()
.validate_cleanup(&VmId::new(id)?, token)
.await?)
}
pub async fn finalize_network_cleanup(&self, id: &str, token: &str) -> Result<()> {
self.await_reconcile().await?;
Ok(self
.reconcile_network()
.finalize_cleanup(&VmId::new(id)?, token)
.await?)
}
pub async fn wait_startup_cleanup_complete(&self) {
self.reconcile_network()
.wait_startup_cleanup_complete()
.await;
}
pub async fn startup_cleanup_token(&self) -> Result<Option<String>> {
self.await_reconcile().await?;
Ok(self.reconcile_network().startup_cleanup_token().await)
}
pub async fn validate_startup_cleanup(&self, token: &str) -> Result<()> {
self.await_reconcile().await?;
Ok(self
.reconcile_network()
.validate_startup_cleanup(token)
.await?)
}
pub async fn finalize_startup_cleanup(&self, token: &str) -> Result<()> {
self.await_reconcile().await?;
Ok(self
.reconcile_network()
.finalize_startup_cleanup(token)
.await?)
}
pub(super) fn reconcile_network(&self) -> &dyn NetworkReconcile {
reconcile_capability(&*self.services.network)
}
pub fn computer_network_identity(&self, id: &str) -> Result<ComputerNetworkIdentity> {
let snapshot = self.snapshot(&id.to_owned())?;
let lease = snapshot
.lease
.as_ref()
.ok_or_else(|| ComputerError::WrongState {
id: id.to_owned(),
expected: "sandbox with an active network allocation".into(),
actual: snapshot.state.to_string(),
})?;
Ok(ComputerNetworkIdentity {
ip: lease.ipv4()?,
cleanup_token: lease.cleanup_token.clone(),
expose: self.services.network.host_ingress(lease)?,
})
}
}
pub(super) trait LeaseExt {
fn ipv4(&self) -> Result<std::net::Ipv4Addr>;
fn gateway_ipv4(&self) -> Result<std::net::Ipv4Addr>;
}
impl LeaseExt for NetworkLease {
fn ipv4(&self) -> Result<std::net::Ipv4Addr> {
ipv4(self.ip)
}
fn gateway_ipv4(&self) -> Result<std::net::Ipv4Addr> {
ipv4(self.gateway)
}
}
pub(super) fn ipv4(address: std::net::IpAddr) -> Result<std::net::Ipv4Addr> {
match address {
std::net::IpAddr::V4(v4) => Ok(v4),
std::net::IpAddr::V6(v6) => Err(ComputerError::Network(format!(
"the guest network offered {v6}; this manager's sandboxes are IPv4-only"
))),
}
}
pub(super) fn netmask(prefix_len: u8) -> std::net::Ipv4Addr {
let prefix = prefix_len.min(32);
if prefix == 0 {
std::net::Ipv4Addr::UNSPECIFIED
} else {
std::net::Ipv4Addr::from(!0u32 << (32 - prefix))
}
}
pub(super) const fn computer_network_policy() -> NetworkPolicy {
NetworkPolicy {
mode: NetworkMode::Nat,
}
}
pub(super) const fn attach_mode(net_invariant: bool) -> AttachMode {
if net_invariant {
AttachMode::Invariant
} else {
AttachMode::LegacySnapshot
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ComputerNetworkIdentity {
pub ip: std::net::Ipv4Addr,
pub cleanup_token: String,
pub expose: HostIngress,
}
pub(super) fn reconcile_capability(network: &dyn GuestNetwork) -> &dyn NetworkReconcile {
network
.reconcile()
.expect("SandboxManager::new requires the guest network's reconcile capability")
}
pub(crate) fn prepare_capability(driver: &dyn VmDriver) -> &dyn Prepare {
driver
.prepare()
.expect("SandboxManager::new requires the driver's Prepare capability")
}
pub(crate) fn staging_capability(staging: Option<&dyn Staging>) -> &dyn Staging {
staging.expect("SandboxManager::new requires the driver's Staging capability")
}
pub(crate) fn journaled_pid(prepared: &dyn PreparedVm) -> Option<i32> {
prepared
.record()
.process
.and_then(|process| i32::try_from(process.pid).ok())
}
pub(crate) fn journaled_vmm(prepared: &dyn PreparedVm) -> Option<JournaledVmm> {
prepared.record().process.map(|process| JournaledVmm {
api_socket: process.api_socket,
jail: process.jail.map(Into::into),
})
}
pub(crate) fn isolation_spec(config: &RuntimeConfig) -> Result<IsolationSpec> {
config
.firecracker
.jailer
.as_ref()
.map_or(Ok(IsolationSpec::None), IsolationSpec::try_from)
}
pub(crate) fn preserved_cow_file(config: &RuntimeConfig, id: &str) -> PathBuf {
PathBuf::from(&config.firecracker.data_dir)
.join("cow")
.join(format!("arcbox-cow-{id}.img"))
}
pub(crate) fn catalogued_checkpoint(meta: &SnapshotMeta) -> Result<CheckpointImage> {
let dir = meta.vmstate_path.parent().ok_or_else(|| {
ComputerError::Snapshot(format!(
"checkpoint {} records a vmstate at {}, which is in no directory",
meta.id,
meta.vmstate_path.display()
))
})?;
Ok(CheckpointImage {
dir: dir.to_path_buf(),
format: CheckpointFormat::new(&meta.format),
kind: CheckpointKind::Full,
})
}
pub(super) fn validate_id(kind: &str, id: &str) -> Result<()> {
if id.is_empty() {
return Err(ComputerError::Config(format!("{kind} must not be empty")));
}
if !id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
{
return Err(ComputerError::Config(format!(
"invalid {kind} {id:?}: only ASCII letters, digits, '-' and '_' are allowed"
)));
}
Ok(())
}
pub(super) fn validate_new_computer_id(
id: &str,
driver: &dyn VmDriver,
config: &RuntimeConfig,
) -> Result<()> {
VmId::new(id)?;
if let Some(budget) = driver.id_budget(&isolation_spec(config)?)
&& id.len() > budget
{
return Err(ComputerError::Config(format!(
"invalid sandbox id {id:?}: at most {budget} characters under this \
driver's layout (its sockets must fit the AF_UNIX path limit)"
)));
}
Ok(())
}
pub(crate) fn reserve_actor(
computers: &Computers,
id: &ComputerId,
runtime: ComputerRuntime,
) -> Result<ActorReservation> {
let mut map = computers.write().unwrap();
if map.contains_key(id) {
return Err(ComputerError::AlreadyExists(id.clone()));
}
let incarnation = Uuid::new_v4();
let (mailbox_tx, commands) = tokio::sync::mpsc::unbounded_channel();
let runtime = Arc::new(Mutex::new(runtime));
let (snapshot_tx, snapshot) = tokio::sync::watch::channel(ComputerSnapshot::project(
&runtime.lock().unwrap(),
ComputerState::Starting,
Deadlines::default(),
));
map.insert(
id.clone(),
ComputerRef {
mailbox: Mailbox::new(mailbox_tx.clone()),
snapshot,
incarnation,
},
);
drop(map);
Ok(ActorReservation {
computers: Arc::clone(computers),
id: id.clone(),
incarnation,
runtime,
mailbox: Mailbox::new(mailbox_tx),
commands: Some(commands),
snapshot_tx: Some(snapshot_tx),
})
}
pub(crate) struct ActorReservation {
computers: Computers,
id: ComputerId,
incarnation: Uuid,
runtime: Runtime,
mailbox: Mailbox,
commands: Option<tokio::sync::mpsc::UnboundedReceiver<Command>>,
snapshot_tx: Option<tokio::sync::watch::Sender<ComputerSnapshot>>,
}
impl ActorReservation {
pub(crate) fn runtime(&self) -> &Runtime {
&self.runtime
}
pub(crate) fn spawn(mut self, seed: ActorSpawn) -> Mailbox {
let ActorSpawn {
services,
timers_enabled,
generation,
deadlines,
launch,
seeded,
} = seed;
let mailbox = self.mailbox.clone();
let flows = Arc::new(ComputerFlows::new(
self.id.clone(),
Arc::clone(&self.runtime),
Arc::clone(&services),
&mailbox,
launch,
));
let unregister = {
let computers = Arc::clone(&self.computers);
let id = self.id.clone();
let incarnation = self.incarnation;
Arc::new(move || forget_computer(&computers, &id, incarnation))
};
let vm_dir = self.runtime.lock().unwrap().vm_dir.clone();
let actor = ComputerActor::new(
ComputerSeed {
id: self.id.clone(),
runtime: Arc::clone(&self.runtime),
unregister,
generation,
vm_dir,
records: Arc::clone(&services.records),
events: Arc::clone(&services.events),
tasks: flows,
deadlines,
timers_enabled,
seeded,
},
self.commands.take().expect("spawned once"),
self.snapshot_tx.take().expect("spawned once"),
);
if tokio::runtime::Handle::try_current().is_ok() {
tokio::spawn(actor.run());
}
mailbox
}
}
impl Drop for ActorReservation {
fn drop(&mut self) {
if self.commands.is_some() {
forget_computer(&self.computers, &self.id, self.incarnation);
}
}
}
pub(crate) struct ActorSpawn {
pub(crate) services: Arc<ComputerServices>,
pub(crate) timers_enabled: tokio::sync::watch::Receiver<bool>,
pub(crate) generation: Option<Uuid>,
pub(crate) deadlines: Deadlines,
pub(crate) launch: Launch,
pub(crate) seeded: Seeded,
}
fn forget_computer(computers: &Computers, id: &ComputerId, incarnation: Uuid) {
let mut map = computers.write().unwrap();
if map
.get(id)
.is_some_and(|current| current.incarnation == incarnation)
{
map.remove(id);
}
}
fn still_registered(computers: &Computers, id: &ComputerId, incarnation: Uuid) -> bool {
computers
.read()
.unwrap()
.get(id)
.is_some_and(|current| current.incarnation == incarnation)
}
#[cfg(test)]
mod tests {
use arcbox_vm_driver::testkit::FakeDriver;
use super::*;
fn placeholder(id: &str) -> ComputerRuntime {
ComputerRuntime::new(
id.to_owned(),
ComputerSpec::default(),
None,
PathBuf::from("/tmp/x"),
)
}
#[test]
fn construction_requires_the_drivers_prepare_and_vsock_capabilities() {
use arcbox_vm_driver::testkit::FakeDriver;
use arcbox_vm_driver::{DriverCapabilities, VmDriver as _};
let dir = tempfile::tempdir().unwrap();
let mut config = RuntimeConfig::default();
config.firecracker.data_dir = dir.path().to_string_lossy().into_owned();
let environment = |driver: FakeDriver| NodeEnvironment {
driver: Arc::new(driver),
agent: Arc::new(crate::agent::VmProtoAgentFactory::default()),
..crate::testkit::fake_environment(&config).unwrap()
};
let all = FakeDriver::new().capabilities();
for (name, capabilities) in [
(
"prepare",
DriverCapabilities {
prepare: false,
..all.clone()
},
),
(
"vsock",
DriverCapabilities {
vsock: false,
..all.clone()
},
),
(
"vsock_listen",
DriverCapabilities {
vsock_listen: false,
..all
},
),
] {
let driver = FakeDriver::builder().capabilities(capabilities).build();
let error = ComputerManager::new(config.clone(), environment(driver))
.err()
.unwrap_or_else(|| panic!("a driver without {name} is refused"));
assert!(matches!(error, ComputerError::Config(_)), "{error}");
assert!(error.to_string().contains(name), "{error}");
}
let manager = ComputerManager::new(config.clone(), environment(FakeDriver::new()))
.expect("a driver with every needed capability is accepted");
assert_eq!(manager.services.driver.name(), "fake");
}
#[test]
fn a_claimed_id_rejects_a_concurrent_duplicate() {
let computers: Computers = Arc::new(RwLock::new(HashMap::new()));
let first = reserve_actor(&computers, &"dup".to_owned(), placeholder("dup")).unwrap();
assert!(matches!(
reserve_actor(&computers, &"dup".to_owned(), placeholder("dup")),
Err(ComputerError::AlreadyExists(_))
));
drop(first);
assert!(!computers.read().unwrap().contains_key("dup"));
}
fn jailed_config() -> RuntimeConfig {
let mut config = RuntimeConfig::default();
config.firecracker.jailer = Some(crate::config::JailerConfig {
uid: 0,
gid: 0,
chroot_base_dir: Some("/srv/jailer".into()),
netns: None,
new_pid_ns: false,
cgroup_version: None,
parent_cgroup: None,
});
config
}
const CONTROL_PLANE_ID: &str = "inst-019e409e-7546-7a3e-8b2c-1f2e3d4c5b6a";
#[test]
fn validate_id_accepts_safe_ids_and_rejects_traversal() {
for ok in ["sandbox1", "a-b_c", "0f3e9d16-1234", "A_B-9"] {
assert!(validate_id("id", ok).is_ok(), "{ok} should be valid");
}
assert!(validate_id("id", &"a".repeat(60)).is_ok());
assert!(validate_id("id", "inst_019e409e-7546").is_ok());
for bad in ["", "..", ".", "a/b", "a\\b", "a b", "a.b", "a\0b", "../etc"] {
assert!(
validate_id("id", bad).is_err(),
"{bad:?} should be rejected"
);
}
}
#[test]
fn a_new_sandbox_id_is_held_to_the_drivers_own_budget() {
let mut config = jailed_config();
let budget = CONTROL_PLANE_ID.len();
let driver = FakeDriver::builder().jailed_id_budget(budget).build();
assert!(validate_new_computer_id(CONTROL_PLANE_ID, &driver, &config).is_ok());
assert!(validate_new_computer_id(&"a".repeat(budget), &driver, &config).is_ok());
assert!(validate_new_computer_id(&"a".repeat(budget + 1), &driver, &config).is_err());
let tighter = FakeDriver::builder().jailed_id_budget(budget - 1).build();
assert!(validate_new_computer_id(CONTROL_PLANE_ID, &tighter, &config).is_err());
config.firecracker.jailer = None;
assert!(validate_new_computer_id(&"a".repeat(budget + 1), &tighter, &config).is_ok());
}
#[test]
fn an_underscored_id_is_refused_at_ingress_in_every_spawn_mode() {
let mut config = jailed_config();
let driver = FakeDriver::builder()
.jailed_id_budget(CONTROL_PLANE_ID.len())
.build();
for _ in 0..2 {
let error =
validate_new_computer_id(&CONTROL_PLANE_ID.replacen('-', "_", 1), &driver, &config)
.expect_err("firecracker would refuse to run under this id");
assert!(
matches!(&error, ComputerError::Config(message) if message.contains('_')),
"the error should name the offending character, got {error}"
);
assert!(validate_new_computer_id(CONTROL_PLANE_ID, &driver, &config).is_ok());
config.firecracker.jailer = None;
}
}
#[test]
fn a_dropped_claim_frees_the_id() {
let computers: Computers = Arc::new(RwLock::new(HashMap::new()));
{
let _claim = reserve_actor(&computers, &"tmp".to_owned(), placeholder("tmp")).unwrap();
assert!(computers.read().unwrap().contains_key("tmp"));
}
assert!(!computers.read().unwrap().contains_key("tmp"));
assert!(reserve_actor(&computers, &"tmp".to_owned(), placeholder("tmp")).is_ok());
}
#[test]
fn a_departing_actor_does_not_evict_its_replacement() {
let computers: Computers = Arc::new(RwLock::new(HashMap::new()));
let departing = reserve_actor(&computers, &"same".to_owned(), placeholder("same")).unwrap();
let incarnation = departing.incarnation;
std::mem::forget(departing);
assert!(matches!(
reserve_actor(&computers, &"same".to_owned(), placeholder("same")),
Err(ComputerError::AlreadyExists(_))
));
forget_computer(&computers, &"same".to_owned(), incarnation);
let replacement = reserve_actor(&computers, &"same".to_owned(), placeholder("same"))
.expect("the id is free once the departing actor let it go");
forget_computer(&computers, &"same".to_owned(), incarnation);
assert!(
computers.read().unwrap().contains_key("same"),
"the stale incarnation must not evict the replacement"
);
drop(replacement);
assert!(!computers.read().unwrap().contains_key("same"));
}
#[test]
fn registration_is_per_incarnation() {
let computers: Computers = Arc::new(RwLock::new(HashMap::new()));
let id = "box".to_owned();
let departing = reserve_actor(&computers, &id, placeholder("box")).unwrap();
let incarnation = departing.incarnation;
assert!(still_registered(&computers, &id, incarnation));
assert!(!still_registered(&computers, &id, Uuid::new_v4()));
std::mem::forget(departing);
assert!(still_registered(&computers, &id, incarnation));
forget_computer(&computers, &id, incarnation);
assert!(!still_registered(&computers, &id, incarnation));
let replacement = reserve_actor(&computers, &id, placeholder("box")).unwrap();
assert!(!still_registered(&computers, &id, incarnation));
assert!(still_registered(&computers, &id, replacement.incarnation));
}
}