use std::fmt;
use std::os::fd::OwnedFd;
use std::path::{Path, PathBuf};
use std::time::Duration;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast;
use crate::capability::{
Adopt, Balloon, Checkpoint, CheckpointImage, Console, DebugSnapshot, Detach, Prepare, Staging,
Vsock, VsockListen,
};
use crate::error::Result;
use crate::spec::{DiskSpec, IsolationSpec, NicSpec, VmId, VmSpec};
#[async_trait]
pub trait VmDriver: Send + Sync {
fn name(&self) -> &'static str;
fn capabilities(&self) -> DriverCapabilities;
async fn boot(&self, spec: VmSpec, runtime_dir: &Path) -> Result<Box<dyn VmHandle>>;
async fn restore(
&self,
image: &CheckpointImage,
spec: RestoreSpec,
runtime_dir: &Path,
) -> Result<Box<dyn VmHandle>>;
fn adopt(&self) -> Option<&dyn Adopt>;
fn prepare(&self) -> Option<&dyn Prepare> {
None
}
fn id_budget(&self, _isolation: &IsolationSpec) -> Option<usize> {
None
}
}
#[async_trait]
pub trait VmHandle: Send + Sync {
fn id(&self) -> &VmId;
fn record(&self) -> VmRecord;
fn state(&self) -> VmState;
fn events(&self) -> broadcast::Receiver<VmEvent>;
async fn shutdown(&self, mode: ShutdownMode) -> Result<ExitStatus>;
fn vsock(&self) -> Option<&dyn Vsock> {
None
}
fn checkpoint(&self) -> Option<&dyn Checkpoint> {
None
}
fn vsock_listener(&self) -> Option<&dyn VsockListen> {
None
}
fn detach(&self) -> Option<&dyn Detach> {
None
}
fn staging(&self) -> Option<&dyn Staging> {
None
}
fn balloon(&self) -> Option<&dyn Balloon> {
None
}
fn console(&self) -> Option<&dyn Console> {
None
}
fn debug(&self) -> Option<&dyn DebugSnapshot> {
None
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DriverCapabilities {
pub vsock: bool,
pub vsock_listen: bool,
pub checkpoint: bool,
pub diff_checkpoint: bool,
pub adopt: bool,
pub prepare: bool,
pub staging: bool,
pub balloon: bool,
pub console: bool,
pub debug: bool,
pub nested_virt: NestedVirt,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NestedVirt {
pub supported: bool,
pub reason: String,
}
impl NestedVirt {
pub fn supported() -> Self {
Self {
supported: true,
reason: String::new(),
}
}
pub fn unsupported(reason: impl Into<String>) -> Self {
Self {
supported: false,
reason: reason.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VmState {
Running,
Quiesced,
Exited(ExitStatus),
}
impl fmt::Display for VmState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Running => f.write_str("running"),
Self::Quiesced => f.write_str("quiesced"),
Self::Exited(status) => write!(f, "exited ({status})"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum VmEvent {
Exited(ExitStatus),
ResetRequested,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExitStatus {
pub code: Option<i32>,
pub signal: Option<i32>,
}
impl ExitStatus {
pub const fn exited(code: i32) -> Self {
Self {
code: Some(code),
signal: None,
}
}
pub const fn signaled(signal: i32) -> Self {
Self {
code: None,
signal: Some(signal),
}
}
pub const fn is_clean(&self) -> bool {
matches!(self.code, Some(0)) && self.signal.is_none()
}
}
impl fmt::Display for ExitStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (self.code, self.signal) {
(Some(code), None) => write!(f, "exit code {code}"),
(None, Some(signal)) => write!(f, "signal {signal}"),
(Some(code), Some(signal)) => write!(f, "exit code {code}, signal {signal}"),
(None, None) => f.write_str("unknown status"),
}
}
}
impl From<std::process::ExitStatus> for ExitStatus {
fn from(status: std::process::ExitStatus) -> Self {
use std::os::unix::process::ExitStatusExt as _;
Self {
code: status.code(),
signal: status.signal(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShutdownMode {
Graceful {
timeout: Duration,
},
Kill,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VmRecord {
pub id: VmId,
pub driver: String,
pub runtime_dir: PathBuf,
#[serde(default)]
pub process: Option<ProcessRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProcessRecord {
pub pid: u32,
#[serde(default)]
pub api_socket: Option<PathBuf>,
#[serde(default)]
pub jail: Option<JailRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JailRecord {
pub root: PathBuf,
pub uid: u32,
pub gid: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RestoreSpec {
pub id: VmId,
#[serde(default)]
pub nics: Vec<NicSpec>,
#[serde(default)]
pub disks: Vec<DiskSpec>,
#[serde(default)]
pub isolation: IsolationSpec,
}
#[derive(Debug)]
pub struct VsockConn {
pub fd: OwnedFd,
pub mode: IoMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IoMode {
Async,
Blocking,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exit_status_is_clean_only_for_code_zero_without_signal() {
assert!(ExitStatus::exited(0).is_clean());
assert!(!ExitStatus::exited(1).is_clean());
assert!(!ExitStatus::signaled(9).is_clean());
assert!(
!ExitStatus {
code: Some(0),
signal: Some(15)
}
.is_clean()
);
assert!(
!ExitStatus {
code: None,
signal: None
}
.is_clean()
);
}
#[test]
fn exit_status_display_names_what_ended_the_vm() {
assert_eq!(ExitStatus::exited(3).to_string(), "exit code 3");
assert_eq!(ExitStatus::signaled(9).to_string(), "signal 9");
assert_eq!(
ExitStatus {
code: None,
signal: None
}
.to_string(),
"unknown status"
);
assert_eq!(
VmState::Exited(ExitStatus::exited(0)).to_string(),
"exited (exit code 0)"
);
}
#[test]
fn process_exit_status_maps_code_and_signal() {
use std::os::unix::process::ExitStatusExt as _;
let exited = std::process::ExitStatus::from_raw(3 << 8);
assert_eq!(ExitStatus::from(exited), ExitStatus::exited(3));
let killed = std::process::ExitStatus::from_raw(9);
assert_eq!(ExitStatus::from(killed), ExitStatus::signaled(9));
}
#[test]
fn vm_record_round_trips_and_defaults_the_process() {
let record = VmRecord {
id: VmId::new("vm-1").unwrap(),
driver: "fc".into(),
runtime_dir: "/run/arcbox/vm-1".into(),
process: Some(ProcessRecord {
pid: 4242,
api_socket: Some("/run/arcbox/vm-1/api.sock".into()),
jail: Some(JailRecord {
root: "/srv/jailer/firecracker/vm-1/root".into(),
uid: 123,
gid: 456,
}),
}),
};
let json = serde_json::to_string(&record).unwrap();
assert_eq!(serde_json::from_str::<VmRecord>(&json).unwrap(), record);
let in_process: VmRecord = serde_json::from_value(serde_json::json!({
"id": "vm-2",
"driver": "hv",
"runtime_dir": "/run/arcbox/vm-2",
}))
.unwrap();
assert_eq!(in_process.process, None);
let older: VmRecord = serde_json::from_value(serde_json::json!({
"id": "vm-3",
"driver": "fc",
"runtime_dir": "/run/arcbox/vm-3",
"process": { "pid": 7, "api_socket": "/run/arcbox/vm-3/api.sock" },
}))
.unwrap();
assert_eq!(older.process.unwrap().jail, None);
}
}