mod layout;
mod network;
mod ready;
mod spec;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use a3s_box_core::config::{BoxConfig, TeeConfig};
use a3s_box_core::error::{BoxError, Result};
use a3s_box_core::event::{BoxEvent, EventEmitter};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use tracing::Instrument;
use libc;
#[cfg(unix)]
use crate::grpc::ExecClient;
use crate::network::PasstManager;
#[cfg(unix)]
use crate::tee::TeeExtension;
use crate::vmm::{VmController, VmHandler, VmmProvider, DEFAULT_SHUTDOWN_TIMEOUT_MS};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BoxState {
Created,
Ready,
Busy,
Compacting,
Stopped,
}
pub(crate) struct BoxLayout {
pub(crate) rootfs_path: PathBuf,
pub(crate) exec_socket_path: PathBuf,
pub(crate) pty_socket_path: PathBuf,
pub(crate) attest_socket_path: PathBuf,
pub(crate) workspace_path: PathBuf,
pub(crate) console_output: Option<PathBuf>,
pub(crate) oci_config: Option<crate::oci::OciImageConfig>,
pub(crate) tee_instance_config: Option<crate::vmm::TeeInstanceConfig>,
}
pub struct VmManager {
pub(crate) config: BoxConfig,
pub(crate) box_id: String,
pub(crate) state: Arc<RwLock<BoxState>>,
pub(crate) event_emitter: EventEmitter,
pub(crate) provider: Option<Box<dyn VmmProvider>>,
pub(crate) handler: Arc<RwLock<Option<Box<dyn VmHandler>>>>,
#[cfg(unix)]
pub(crate) exec_client: Option<ExecClient>,
pub(crate) passt_manager: Option<PasstManager>,
pub(crate) home_dir: PathBuf,
pub(crate) anonymous_volumes: Vec<String>,
#[cfg(unix)]
pub(crate) tee: Option<Box<dyn TeeExtension>>,
pub(crate) rootfs_provider: Box<dyn crate::rootfs::RootfsProvider>,
pub(crate) exec_socket_path: Option<PathBuf>,
pub(crate) pty_socket_path: Option<PathBuf>,
pub(crate) prom: Option<crate::prom::RuntimeMetrics>,
pub(crate) shim_exit_code: Option<i32>,
}
impl VmManager {
pub fn new(config: BoxConfig, event_emitter: EventEmitter) -> Self {
let box_id = uuid::Uuid::new_v4().to_string();
let home_dir = a3s_box_core::dirs_home();
Self {
config,
box_id,
state: Arc::new(RwLock::new(BoxState::Created)),
event_emitter,
provider: None,
handler: Arc::new(RwLock::new(None)),
#[cfg(unix)]
exec_client: None,
passt_manager: None,
home_dir,
anonymous_volumes: Vec::new(),
#[cfg(unix)]
tee: None,
rootfs_provider: crate::rootfs::default_provider(),
exec_socket_path: None,
pty_socket_path: None,
prom: None,
shim_exit_code: None,
}
}
pub fn with_box_id(config: BoxConfig, event_emitter: EventEmitter, box_id: String) -> Self {
let home_dir = a3s_box_core::dirs_home();
Self {
config,
box_id,
state: Arc::new(RwLock::new(BoxState::Created)),
event_emitter,
provider: None,
handler: Arc::new(RwLock::new(None)),
#[cfg(unix)]
exec_client: None,
passt_manager: None,
home_dir,
anonymous_volumes: Vec::new(),
#[cfg(unix)]
tee: None,
rootfs_provider: crate::rootfs::default_provider(),
exec_socket_path: None,
pty_socket_path: None,
prom: None,
shim_exit_code: None,
}
}
pub fn with_provider(
config: BoxConfig,
event_emitter: EventEmitter,
provider: Box<dyn VmmProvider>,
) -> Self {
let box_id = uuid::Uuid::new_v4().to_string();
let home_dir = a3s_box_core::dirs_home();
Self {
config,
box_id,
state: Arc::new(RwLock::new(BoxState::Created)),
event_emitter,
provider: Some(provider),
handler: Arc::new(RwLock::new(None)),
#[cfg(unix)]
exec_client: None,
passt_manager: None,
home_dir,
anonymous_volumes: Vec::new(),
#[cfg(unix)]
tee: None,
rootfs_provider: crate::rootfs::default_provider(),
exec_socket_path: None,
pty_socket_path: None,
prom: None,
shim_exit_code: None,
}
}
pub fn box_id(&self) -> &str {
&self.box_id
}
pub async fn state(&self) -> BoxState {
*self.state.read().await
}
#[cfg(unix)]
pub fn exec_client(&self) -> Option<&ExecClient> {
self.exec_client.as_ref()
}
pub fn exec_socket_path(&self) -> Option<&Path> {
self.exec_socket_path.as_deref()
}
pub fn pty_socket_path(&self) -> Option<&Path> {
self.pty_socket_path.as_deref()
}
pub fn set_provider(&mut self, provider: Box<dyn VmmProvider>) {
self.provider = Some(provider);
}
pub fn set_rootfs_provider(&mut self, provider: Box<dyn crate::rootfs::RootfsProvider>) {
self.rootfs_provider = provider;
}
pub fn rootfs_provider_name(&self) -> &str {
self.rootfs_provider.name()
}
pub fn set_metrics(&mut self, metrics: crate::prom::RuntimeMetrics) {
self.prom = Some(metrics);
}
pub fn metrics_prom(&self) -> Option<&crate::prom::RuntimeMetrics> {
self.prom.as_ref()
}
pub fn anonymous_volumes(&self) -> &[String] {
&self.anonymous_volumes
}
pub fn exit_code(&self) -> Option<i32> {
self.shim_exit_code
}
#[cfg(unix)]
#[tracing::instrument(skip(self, cmd), fields(box_id = %self.box_id))]
pub async fn exec_command(
&self,
cmd: Vec<String>,
timeout_ns: u64,
) -> Result<a3s_box_core::exec::ExecOutput> {
let state = self.state.read().await;
match *state {
BoxState::Ready | BoxState::Busy | BoxState::Compacting => {}
BoxState::Created => {
return Err(BoxError::ExecError("VM not yet booted".to_string()));
}
BoxState::Stopped => {
return Err(BoxError::ExecError("VM is stopped".to_string()));
}
}
drop(state);
let client = self
.exec_client
.as_ref()
.ok_or_else(|| BoxError::ExecError("Exec client not connected".to_string()))?;
let request = a3s_box_core::exec::ExecRequest {
cmd,
timeout_ns,
env: vec![],
working_dir: None,
stdin: None,
user: None,
streaming: false,
};
let exec_start = std::time::Instant::now();
let result = client.exec_command(&request).await;
if let Some(ref prom) = self.prom {
prom.exec_total.inc();
prom.exec_duration
.observe(exec_start.elapsed().as_secs_f64());
if result.is_err() || result.as_ref().is_ok_and(|o| o.exit_code != 0) {
prom.exec_errors_total.inc();
}
}
result
}
pub async fn boot(&mut self) -> Result<()> {
let boot_span = tracing::info_span!("vm_boot", box_id = %self.box_id);
{
let state = self.state.read().await;
if *state != BoxState::Created {
return Err(BoxError::StateError("VM already booted".to_string()));
}
}
let boot_start = std::time::Instant::now();
tracing::info!(parent: &boot_span, box_id = %self.box_id, "Booting VM");
let layout = self
.prepare_layout()
.instrument(tracing::info_span!(parent: &boot_span, "prepare_layout"))
.await?;
let resolv_content = a3s_box_core::dns::generate_resolv_conf(&self.config.dns);
let resolv_path = layout.rootfs_path.join("etc/resolv.conf");
tokio::fs::write(&resolv_path, &resolv_content)
.await
.map_err(BoxError::IoError)?;
tracing::debug!(parent: &boot_span, dns = %resolv_content.trim(), "Configured guest DNS");
let mut spec = self.build_instance_spec(&layout)?;
let bridge_network = match &self.config.network {
a3s_box_core::NetworkMode::Bridge { network } => Some(network.clone()),
_ => None,
};
if let Some(network_name) = bridge_network {
let net_config = self.setup_bridge_network(&network_name)?;
self.write_hosts_file(&layout, &network_name)?;
let ip_cidr = format!("{}/{}", net_config.ip_address, net_config.prefix_len);
spec.entrypoint
.env
.push(("A3S_NET_IP".to_string(), ip_cidr));
spec.entrypoint.env.push((
"A3S_NET_GATEWAY".to_string(),
net_config.gateway.to_string(),
));
spec.entrypoint.env.push((
"A3S_NET_DNS".to_string(),
net_config
.dns_servers
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join(","),
));
spec.network = Some(net_config);
}
if self.provider.is_none() {
let shim_path = VmController::find_shim()?;
let controller = VmController::new(shim_path)?;
self.provider = Some(Box::new(controller));
}
let handler = {
let provider = self
.provider
.as_ref()
.ok_or_else(|| BoxError::BoxBootError {
message: "VMM provider not initialized".to_string(),
hint: Some("Ensure VmManager has a provider set before boot".to_string()),
})?;
let vm_start_span = tracing::info_span!(parent: &boot_span, "vm_start");
async { provider.start(&spec).await }
.instrument(vm_start_span)
.await?
};
*self.handler.write().await = Some(handler);
{
let wait_span = tracing::info_span!(parent: &boot_span, "wait_for_ready");
async {
self.wait_for_vm_running().await?;
#[cfg(unix)]
self.wait_for_exec_ready(&layout.exec_socket_path).await?;
Ok::<(), BoxError>(())
}
.instrument(wait_span)
.await?;
}
self.exec_socket_path = Some(layout.exec_socket_path.clone());
self.pty_socket_path = Some(layout.pty_socket_path.clone());
#[cfg(unix)]
if !matches!(self.config.tee, TeeConfig::None) {
self.tee = Some(Box::new(crate::tee::SnpTeeExtension::new(
self.box_id.clone(),
layout.attest_socket_path.clone(),
)));
}
*self.state.write().await = BoxState::Ready;
if let Some(ref prom) = self.prom {
let boot_duration = boot_start.elapsed().as_secs_f64();
prom.vm_boot_duration.observe(boot_duration);
prom.vm_created_total.inc();
prom.vm_count.with_label_values(&["ready"]).inc();
}
self.event_emitter.emit(BoxEvent::empty("box.ready"));
tracing::info!(parent: &boot_span, box_id = %self.box_id, "VM ready");
Ok(())
}
pub async fn destroy(&mut self) -> Result<()> {
self.destroy_with_options(libc::SIGTERM, DEFAULT_SHUTDOWN_TIMEOUT_MS)
.await
}
pub async fn destroy_with_timeout(&mut self, timeout_ms: u64) -> Result<()> {
self.destroy_with_options(libc::SIGTERM, timeout_ms).await
}
#[tracing::instrument(skip(self), fields(box_id = %self.box_id))]
pub async fn destroy_with_options(&mut self, signal: i32, timeout_ms: u64) -> Result<()> {
let mut state = self.state.write().await;
if *state == BoxState::Stopped {
return Ok(());
}
tracing::info!(box_id = %self.box_id, signal, timeout_ms, "Destroying VM");
if let Some(mut handler) = self.handler.write().await.take() {
handler.stop(signal, timeout_ms)?;
self.shim_exit_code = handler.exit_code();
}
if let Some(ref mut passt) = self.passt_manager {
passt.stop();
}
self.passt_manager = None;
*state = BoxState::Stopped;
let box_dir = self.home_dir.join("boxes").join(&self.box_id);
if let Err(e) = self
.rootfs_provider
.cleanup(&box_dir, self.config.persistent)
{
tracing::warn!(
box_id = %self.box_id,
error = %e,
"Failed to cleanup rootfs provider"
);
}
if let Some(ref prom) = self.prom {
prom.vm_destroyed_total.inc();
prom.vm_count.with_label_values(&["ready"]).dec();
}
self.event_emitter.emit(BoxEvent::empty("box.stopped"));
Ok(())
}
pub async fn set_busy(&self) -> Result<()> {
let mut state = self.state.write().await;
if *state != BoxState::Ready {
return Err(BoxError::StateError("VM not ready".to_string()));
}
*state = BoxState::Busy;
Ok(())
}
pub async fn set_ready(&self) -> Result<()> {
let mut state = self.state.write().await;
if *state != BoxState::Busy && *state != BoxState::Compacting {
return Err(BoxError::StateError("Invalid state transition".to_string()));
}
*state = BoxState::Ready;
Ok(())
}
pub async fn set_compacting(&self) -> Result<()> {
let mut state = self.state.write().await;
if *state != BoxState::Busy {
return Err(BoxError::StateError("VM not busy".to_string()));
}
*state = BoxState::Compacting;
Ok(())
}
#[cfg(unix)]
pub async fn pause(&self) -> Result<()> {
let state = self.state.read().await;
match *state {
BoxState::Ready | BoxState::Busy | BoxState::Compacting => {}
BoxState::Created => {
return Err(BoxError::StateError("VM not yet booted".to_string()));
}
BoxState::Stopped => {
return Err(BoxError::StateError("VM is stopped".to_string()));
}
}
drop(state);
if let Some(pid) = self.pid().await {
let ret = unsafe { libc::kill(pid as i32, libc::SIGSTOP) };
if ret != 0 {
let err = std::io::Error::last_os_error();
return Err(BoxError::ExecError(format!(
"Failed to send SIGSTOP to pid {}: {}",
pid, err
)));
}
tracing::info!(box_id = %self.box_id, pid, "VM paused");
Ok(())
} else {
Err(BoxError::StateError(
"VM has no running process".to_string(),
))
}
}
#[cfg(unix)]
pub async fn resume(&self) -> Result<()> {
if let Some(pid) = self.pid().await {
let ret = unsafe { libc::kill(pid as i32, libc::SIGCONT) };
if ret != 0 {
let err = std::io::Error::last_os_error();
return Err(BoxError::ExecError(format!(
"Failed to send SIGCONT to pid {}: {}",
pid, err
)));
}
tracing::info!(box_id = %self.box_id, pid, "VM resumed");
Ok(())
} else {
Err(BoxError::StateError(
"VM has no running process".to_string(),
))
}
}
pub async fn health_check(&self) -> Result<bool> {
let state = self.state.read().await;
match *state {
BoxState::Ready | BoxState::Busy | BoxState::Compacting => {
if let Some(ref handler) = *self.handler.read().await {
Ok(handler.is_running())
} else {
Ok(false)
}
}
_ => Ok(false),
}
}
pub async fn metrics(&self) -> Option<crate::vmm::VmMetrics> {
let vm_metrics = self
.handler
.read()
.await
.as_ref()
.map(|handler| handler.metrics())?;
if let Some(ref prom) = self.prom {
prom.vm_cpu_percent
.with_label_values(&[&self.box_id])
.set(vm_metrics.cpu_percent.unwrap_or(0.0) as f64);
prom.vm_memory_bytes
.with_label_values(&[&self.box_id])
.set(vm_metrics.memory_bytes.unwrap_or(0) as f64);
}
Some(vm_metrics)
}
pub async fn pid(&self) -> Option<u32> {
self.handler
.read()
.await
.as_ref()
.map(|handler| handler.pid())
}
#[cfg(unix)]
pub fn tee(&self) -> Option<&dyn TeeExtension> {
self.tee.as_deref()
}
#[cfg(unix)]
pub fn require_tee(&self) -> Result<&dyn TeeExtension> {
self.tee.as_deref().ok_or_else(|| {
BoxError::AttestationError("TEE is not configured for this box".to_string())
})
}
#[cfg(unix)]
pub async fn update_resources(
&self,
update: &crate::resize::ResourceUpdate,
) -> Result<crate::resize::ResizeResult> {
crate::resize::validate_update(update)?;
let mut result = crate::resize::ResizeResult {
applied: Vec::new(),
rejected: Vec::new(),
};
if !update.has_tier2_changes() {
return Ok(result);
}
let commands = update.build_cgroup_commands();
for cmd_str in &commands {
let shell_cmd = vec!["sh".to_string(), "-c".to_string(), cmd_str.clone()];
match self.exec_command(shell_cmd, 5_000_000_000).await {
Ok(output) if output.exit_code == 0 => {
result.applied.push(cmd_str.clone());
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
let reason = if stderr.trim().is_empty() {
format!("exit code {}", output.exit_code)
} else {
stderr.trim().to_string()
};
tracing::warn!(
box_id = %self.box_id,
cmd = %cmd_str,
exit_code = output.exit_code,
stderr = %stderr,
"Cgroup update failed inside guest"
);
result.rejected.push((cmd_str.clone(), reason));
}
Err(e) => {
tracing::warn!(
box_id = %self.box_id,
cmd = %cmd_str,
error = %e,
"Failed to exec cgroup update in guest"
);
result.rejected.push((cmd_str.clone(), e.to_string()));
}
}
}
Ok(result)
}
}
pub(crate) fn fnv1a_hash(input: &str) -> u64 {
let mut hash: u64 = 0xcbf29ce484222325;
for byte in input.bytes() {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}