#[cfg(feature = "net")]
use microsandbox_network::policy::NetworkPolicy;
use super::config::SnapshotRestoreMode;
use super::{ExternalMountRestorePolicy, MountBuilder, Sandbox, SandboxBuilder, SecurityProfile};
use crate::size::Mebibytes;
use crate::snapshot::SnapshotReference;
use crate::{MicrosandboxError, MicrosandboxResult, Operation, UnsupportedReason};
pub struct RestoreBuilder {
pub(crate) inner: SandboxBuilder,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct RestoreBootOverrides {
pub(crate) security: bool,
}
impl RestoreBootOverrides {
pub(crate) fn validate_scope(
self,
scope: crate::snapshot::SnapshotScope,
mode: SnapshotRestoreMode,
) -> MicrosandboxResult<()> {
if scope != crate::snapshot::SnapshotScope::Full || mode == SnapshotRestoreMode::DiskOnly {
return Ok(());
}
if self.security {
return Err(MicrosandboxError::unsupported(
Operation::SnapshotOps,
UnsupportedReason::NotAvailable(
"security profile overrides require a disk snapshot or disk-only restore (disk_only() / --disk-only); full restore resumes the captured guest security profile".into(),
),
));
}
Ok(())
}
}
impl Sandbox {
pub fn restore(snapshot: impl Into<String>) -> RestoreBuilder {
Self::restore_ref(SnapshotReference::auto(snapshot))
}
pub fn restore_ref(reference: impl Into<SnapshotReference>) -> RestoreBuilder {
RestoreBuilder::new(reference.into())
}
}
impl RestoreBuilder {
fn new(reference: SnapshotReference) -> Self {
let mut inner = SandboxBuilder::new("").with_snapshot_reference(reference);
inner.config.spec.mounts.clear();
inner.config.spec.network.ports.clear();
inner.config.spec.vsock = Default::default();
inner.config.spec.runtime.user = None;
Self { inner }
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.inner.config.spec.name = name.into();
self
}
pub fn cpus(mut self, count: u8) -> Self {
self.inner = self.inner.cpus(count);
self
}
pub fn memory(mut self, size: impl Into<Mebibytes>) -> Self {
self.inner = self.inner.memory(size);
self
}
#[cfg(feature = "net")]
pub fn network_policy(mut self, policy: NetworkPolicy) -> Self {
self.inner = self.inner.network(|network| network.policy(policy));
self
}
#[cfg(feature = "net")]
pub fn max_tcp_connections(mut self, limit: usize) -> Self {
self.inner = self
.inner
.network(|network| network.max_tcp_connections(limit));
self
}
#[cfg(feature = "net")]
pub fn max_udp_connections(mut self, limit: usize) -> Self {
self.inner = self
.inner
.network(|network| network.max_udp_connections(limit));
self
}
#[cfg(feature = "net")]
#[deprecated(note = "use max_tcp_connections instead")]
pub fn max_connections(self, limit: usize) -> Self {
self.max_tcp_connections(limit)
}
#[cfg(feature = "net")]
pub fn disable_network(mut self) -> Self {
self.inner = self.inner.disable_network();
self
}
pub fn security(mut self, profile: SecurityProfile) -> Self {
self.inner = self.inner.security(profile);
self.inner.config.restore_boot_overrides.security = true;
self
}
pub fn max_duration(mut self, secs: u64) -> Self {
self.inner = self.inner.max_duration(secs);
self
}
pub fn idle_timeout(mut self, secs: u64) -> Self {
self.inner = self.inner.idle_timeout(secs);
self
}
pub fn log_level(mut self, level: crate::LogLevel) -> Self {
self.inner = self.inner.log_level(level);
self
}
pub fn forked(mut self) -> Self {
self.inner = self.inner.forked();
self
}
pub fn disk_only(mut self) -> Self {
self.inner = self.inner.disk_only();
self
}
pub fn snapshot_base(mut self, base: impl Into<String>) -> Self {
self.inner = self.inner.snapshot_base(base);
self
}
pub async fn restore(self) -> MicrosandboxResult<Sandbox> {
self.inner.create_detached().await
}
#[cfg(feature = "local")]
pub fn restore_with_progress(
self,
) -> MicrosandboxResult<(
crate::CreationProgressHandle,
tokio::task::JoinHandle<MicrosandboxResult<Sandbox>>,
)> {
self.inner.create_detached_with_progress()
}
}
macro_rules! resource_methods {
($builder:ty) => {
impl $builder {
pub fn user(mut self, user: impl Into<String>) -> Self {
self.inner = self.inner.user(user);
self
}
pub fn volume(
mut self,
guest: impl Into<String>,
configure: impl FnOnce(MountBuilder) -> MountBuilder,
) -> Self {
match configure(MountBuilder::new(guest)).build_restore() {
Ok(Ok(mount)) => {
let guest = mount.guest();
self.inner.config.restore_resources.captured.remove(guest);
self.inner
.config
.restore_resources
.mapped
.insert(guest.into());
self.inner
.config
.spec
.mounts
.retain(|existing| existing.guest() != guest);
self.inner.config.spec.mounts.push(mount);
}
Ok(Err(guest)) => {
self.inner.config.restore_resources.mapped.remove(&guest);
self.inner
.config
.spec
.mounts
.retain(|existing| existing.guest() != guest);
self.inner.config.restore_resources.captured.insert(guest);
}
Err(error) => {
self.inner.build_error = Some(error);
}
}
self
}
pub fn vsock(mut self, path: impl AsRef<std::path::Path>, port: u32) -> Self {
self.inner = self.inner.vsock(path, port);
self
}
pub fn vsock_dgram(mut self, path: impl AsRef<std::path::Path>, port: u32) -> Self {
self.inner = self.inner.vsock_dgram(path, port);
self
}
pub fn dangerously_inherit_resources(mut self) -> Self {
self.inner.config.restore_resources.inherit = true;
self
}
pub fn external_mount_policy(mut self, policy: ExternalMountRestorePolicy) -> Self {
self.inner = self.inner.external_mount_policy(policy);
self
}
#[cfg(feature = "net")]
pub fn port(mut self, host: u16, guest: u16) -> Self {
self.inner = self.inner.port(host, guest);
self
}
#[cfg(feature = "net")]
pub fn port_bind(mut self, bind: std::net::IpAddr, host: u16, guest: u16) -> Self {
self.inner = self.inner.port_bind(bind, host, guest);
self
}
#[cfg(feature = "net")]
pub fn port_udp(mut self, host: u16, guest: u16) -> Self {
self.inner = self.inner.port_udp(host, guest);
self
}
#[cfg(feature = "net")]
pub fn port_udp_bind(mut self, bind: std::net::IpAddr, host: u16, guest: u16) -> Self {
self.inner = self.inner.port_udp_bind(bind, host, guest);
self
}
}
};
}
resource_methods!(RestoreBuilder);
resource_methods!(super::branch::BranchBuilder);
resource_methods!(super::branch::BranchManyBuilder);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_explicit_guest_security_changes_are_refused_for_full_execution() {
use crate::snapshot::SnapshotScope;
for security in [false, true] {
let overrides = RestoreBootOverrides { security };
assert!(
overrides
.validate_scope(SnapshotScope::Disk, SnapshotRestoreMode::Full)
.is_ok()
);
assert!(
overrides
.validate_scope(SnapshotScope::Full, SnapshotRestoreMode::DiskOnly)
.is_ok()
);
assert_eq!(
overrides
.validate_scope(SnapshotScope::Full, SnapshotRestoreMode::Full)
.is_err(),
security
);
}
}
#[test]
fn security_setter_keeps_explicit_intent_even_for_default_profile() {
for profile in [SecurityProfile::Default, SecurityProfile::Restricted] {
let restore = Sandbox::restore("saved").name("child").security(profile);
assert!(restore.inner.config.restore_boot_overrides.security);
assert_eq!(restore.inner.config.spec.security_profile, profile);
}
}
#[test]
fn destination_geometry_and_lifecycle_are_not_lost() {
let restore = Sandbox::restore("saved")
.name("child")
.cpus(2)
.memory(2048)
.max_duration(600)
.idle_timeout(120);
assert_eq!(restore.inner.config.spec.resources.cpus, 2);
assert_eq!(restore.inner.config.spec.resources.memory_mib, 2048);
assert_eq!(
restore.inner.config.spec.lifecycle.max_duration_secs,
Some(600)
);
assert_eq!(
restore.inner.config.spec.lifecycle.idle_timeout_secs,
Some(120)
);
assert!(!restore.inner.config.restore_boot_overrides.security);
}
#[cfg(feature = "net")]
#[test]
fn destination_network_controls_apply_without_changing_guest_identity() {
let restore = Sandbox::restore("saved")
.name("child")
.network_policy(NetworkPolicy::none())
.max_tcp_connections(8)
.max_udp_connections(4);
let network = restore.inner.config.local_network_config().unwrap();
assert!(network.enabled);
assert_eq!(
serde_json::to_value(network.policy).unwrap(),
serde_json::to_value(NetworkPolicy::none()).unwrap()
);
assert_eq!(network.max_tcp_connections, Some(8.into()));
assert_eq!(network.max_udp_connections, Some(4.into()));
assert!(network.interface.mac.is_none());
assert!(!restore.inner.config.restore_boot_overrides.security);
let unlimited = Sandbox::restore("saved")
.name("child")
.max_tcp_connections(0)
.max_udp_connections(0);
assert_eq!(
unlimited.inner.config.spec.network.max_tcp_connections,
Some(0)
);
assert_eq!(
unlimited.inner.config.spec.network.max_udp_connections,
Some(0)
);
assert_eq!(
unlimited
.inner
.config
.local_network_config()
.unwrap()
.max_tcp_connections,
Some(microsandbox_network::config::ConnectionLimit::Unlimited)
);
assert_eq!(
unlimited
.inner
.config
.local_network_config()
.unwrap()
.max_udp_connections,
Some(microsandbox_network::config::ConnectionLimit::Unlimited)
);
let disabled = Sandbox::restore("saved").name("child").disable_network();
assert!(!disabled.inner.config.spec.network.enabled);
}
#[cfg(feature = "net")]
#[test]
#[allow(deprecated)]
fn destination_connection_limits_preserve_omission_and_tcp_alias() {
let defaults = Sandbox::restore("saved");
assert_eq!(defaults.inner.config.spec.network.max_tcp_connections, None);
assert_eq!(defaults.inner.config.spec.network.max_udp_connections, None);
let legacy = Sandbox::restore("saved").max_connections(0);
assert_eq!(
legacy.inner.config.spec.network.max_tcp_connections,
Some(0)
);
assert_eq!(legacy.inner.config.spec.network.max_udp_connections, None);
}
#[test]
fn restore_starts_without_host_bindings() {
let restore = Sandbox::restore("saved").name("child");
assert!(restore.inner.config.spec.mounts.is_empty());
assert!(restore.inner.config.spec.network.ports.is_empty());
assert!(!restore.inner.config.restore_resources.inherit);
assert!(restore.inner.config.spec.runtime.user.is_none());
}
#[test]
fn restore_tracks_authorized_and_captured_volumes() {
let restore = Sandbox::restore("saved")
.name("child")
.volume("/data", |v| v.bind("/tmp/explicit-restore-binding"))
.volume("/private", |v| v.captured());
assert!(
restore
.inner
.config
.restore_resources
.mapped
.contains("/data")
);
assert!(
restore
.inner
.config
.restore_resources
.captured
.contains("/private")
);
let restore = restore.volume("/data", |v| v.captured());
assert!(
!restore
.inner
.config
.restore_resources
.mapped
.contains("/data")
);
assert!(restore.inner.config.spec.mounts.is_empty());
}
}