use crate::config::HypervisorConfig;
use crate::{handle_entry, handle_entry_default, handle_entry_ref};
use crate::{RtckError, RtckResult};
use log::*;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tokio::net::UnixStream;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct JailerAsync {
bin: String,
pub(crate) id: String,
exec_file: String,
uid: u32,
gid: u32,
chroot_base_dir: String,
daemonize: bool,
jailer_workspace_dir: Option<PathBuf>,
socket: Option<String>,
socket_path_export: Option<PathBuf>,
lock_path: Option<String>,
lock_path_export: Option<PathBuf>,
config_path: Option<String>,
config_path_jailed: Option<PathBuf>,
log_path: Option<String>,
log_path_export: Option<PathBuf>,
metrics_path: Option<String>,
metrics_path_export: Option<PathBuf>,
}
impl JailerAsync {
pub(crate) fn get_uid(&self) -> u32 {
self.uid
}
pub(crate) fn get_gid(&self) -> u32 {
self.gid
}
pub(crate) fn get_firecracker_exec_file(&self) -> &String {
&self.exec_file
}
pub(crate) fn get_socket_path_exported(&self) -> Option<&PathBuf> {
self.socket_path_export.as_ref()
}
pub(crate) fn get_log_path_exported(&self) -> Option<&PathBuf> {
self.log_path_export.as_ref()
}
#[allow(unused)]
pub(crate) fn get_metrics_path_exported(&self) -> Option<&PathBuf> {
self.metrics_path_export.as_ref()
}
pub(crate) fn get_config_path_exported(&self) -> Option<&String> {
self.config_path.as_ref()
}
pub(crate) fn get_jailer_workspace_dir(&self) -> Option<&PathBuf> {
self.jailer_workspace_dir.as_ref()
}
}
impl JailerAsync {
pub(crate) fn from_config(config: &HypervisorConfig) -> RtckResult<Self> {
let jailer_config = config.jailer_config.as_ref();
let jailer_config = match jailer_config {
Some(c) => c,
None => {
let msg = "Missing jailer config";
error!("{msg}");
return Err(RtckError::Config(msg.into()));
}
};
let id = config.id.clone().unwrap_or({
info!("`id` not assigned in jailer configuration, allocating a random one");
uuid::Uuid::new_v4().into()
});
let socket = config.socket_path.clone().unwrap_or({
info!("`socket` not assigned in jailer configuration, allocating a random one");
"run/firecracker.socket".into()
});
const DEFAULT_CHROOT_BASE_DIR: &'static str = "/srv/jailer";
Ok(Self {
bin: handle_entry(&jailer_config.jailer_bin, "jailer binary")?,
id,
exec_file: handle_entry(&jailer_config.exec_file, "firecracker executable file")?,
uid: handle_entry(&jailer_config.uid, "jailer uid")?,
gid: handle_entry(&jailer_config.gid, "jailer gid")?,
chroot_base_dir: handle_entry_default(
&jailer_config.chroot_base_dir,
DEFAULT_CHROOT_BASE_DIR.into(),
),
daemonize: jailer_config.daemonize.unwrap_or(false),
jailer_workspace_dir: None,
socket: Some(socket),
socket_path_export: None,
lock_path: config.lock_path.clone(),
lock_path_export: None,
config_path: config.frck_export_path.clone(),
config_path_jailed: None,
log_path: config.log_path.clone(),
log_path_export: None,
metrics_path: config.metrics_path.clone(),
metrics_path_export: None,
})
}
pub(crate) async fn jail(&mut self) -> RtckResult<PathBuf> {
let id = &self.id;
let temp_binding = PathBuf::from(&self.exec_file);
let exec_file_name =
*handle_entry_ref(&temp_binding.file_name(), "firecracker executable file")?;
let chroot_base_dir = &self.chroot_base_dir;
const ROOT_FOLDER_NAME: &'static str = "root";
let instance_dir = PathBuf::from(chroot_base_dir).join(exec_file_name).join(id);
let jailer_workspace_dir = instance_dir.join(ROOT_FOLDER_NAME);
if jailer_workspace_dir.exists() {
let msg = "Conflict instance name, please choose another one";
error!("{msg}");
return Err(RtckError::Jailer(msg.into()));
}
self.jailer_workspace_dir = Some(jailer_workspace_dir.clone());
const DEFAULT_SOCKET_PATH_UNDER_JAILER: &'static str = "run/firecracker.socket";
self.socket_path_export = self.get_x_path_under_jailer(
&self.socket,
DEFAULT_SOCKET_PATH_UNDER_JAILER,
"socket",
&jailer_workspace_dir,
)?;
const DEFAULT_LOCK_PATH_UNDER_JAILER: &'static str = "run/firecracker.lock";
self.lock_path_export = self.get_x_path_under_jailer(
&self.lock_path,
DEFAULT_LOCK_PATH_UNDER_JAILER,
"lock",
&jailer_workspace_dir,
)?;
const DEFAULT_LOG_PATH_UNDER_JAILER: &'static str = "run/firecracker.log";
self.log_path_export = self.get_x_path_under_jailer(
&self.log_path,
DEFAULT_LOG_PATH_UNDER_JAILER,
"log",
&jailer_workspace_dir,
)?;
const DEFAULT_METRICS_PATH_UNDER_JAILER: &'static str = "run/firecracker.metrics";
self.metrics_path_export = self.get_x_path_under_jailer(
&self.metrics_path,
DEFAULT_METRICS_PATH_UNDER_JAILER,
"metrics",
&jailer_workspace_dir,
)?;
Ok(instance_dir)
}
fn get_x_path_under_jailer(
&self,
x: &Option<String>,
default_path_under_jailer: &'static str,
name: &'static str,
jailer_workspace_dir: &PathBuf,
) -> RtckResult<Option<PathBuf>> {
let path = handle_entry_default(x, default_path_under_jailer.to_string());
let path = PathBuf::from(path);
let path = if path.is_absolute() {
path.strip_prefix("/").map_err(|e| {
let msg = format!("Fail to strip prefix of {name} path when jailing: {e}");
error!("{msg}");
RtckError::Jailer(msg)
})?
} else {
path.as_path()
};
Ok(Some(jailer_workspace_dir.join(path)))
}
pub(crate) async fn launch(&self) -> RtckResult<tokio::process::Child> {
let mut cmd = tokio::process::Command::new(&self.bin);
cmd.args(vec!["--id", &self.id]);
cmd.args(vec!["--uid", &self.uid.to_string()]);
cmd.args(vec!["--gid", &self.gid.to_string()]);
cmd.args(vec!["--exec-file", &self.exec_file.to_string()]);
cmd.args(vec!["--chroot-base-dir", &self.chroot_base_dir]);
if self.daemonize {
cmd.arg("--daemonize");
}
cmd.arg("--");
match &self.socket {
None => (),
Some(path) => {
cmd.args(vec!["--api-sock", path]);
}
}
match &self.config_path {
None => (),
Some(path) => {
cmd.args(vec!["--config-file", path]);
}
}
cmd.spawn().map_err(|e| {
let msg = format!("Fail to spawn jailer: {e}");
error!("{msg}");
RtckError::Jailer(msg)
})
}
pub(crate) async fn waiting_socket(&self, timeout: tokio::time::Duration) -> RtckResult<()> {
let socket_path = handle_entry(&self.socket_path_export, "exported socket path")?;
let socket_path = socket_path.as_os_str();
tokio::time::timeout(timeout, async {
while tokio::fs::try_exists(socket_path).await.is_err() {}
})
.await
.map_err(|e| {
let msg = format!("Remote socket timeout: {e}");
error!("{msg}");
RtckError::Jailer(msg)
})
}
pub(crate) async fn connect(&self, retry: usize) -> RtckResult<UnixStream> {
let mut trying = retry;
let socket = handle_entry_ref(&self.socket_path_export, "exported socket path")?;
let stream = loop {
if trying == 0 {
let msg = format!("fail to connect unix socket after {retry} tries");
error!("{msg}");
return Err(RtckError::Firecracker(msg));
}
match UnixStream::connect(socket).await {
Ok(stream) => break stream,
Err(_) => {
trying -= 1;
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
continue;
}
}
};
Ok(stream)
}
}