use std::path::PathBuf;
use clap::{Parser, ValueEnum};
#[derive(Debug, Parser)]
#[command(
name = "squib",
version,
about = "macOS-native microVM monitor with a Firecracker-compatible API",
long_about = None,
)]
#[allow(clippy::struct_excessive_bools)] pub(crate) struct Args {
#[arg(long, value_name = "PATH", default_value = "/run/firecracker.socket")]
pub(crate) api_sock: Option<PathBuf>,
#[arg(long, value_name = "ID", default_value = "anonymous")]
pub(crate) id: String,
#[arg(long, value_name = "PATH")]
pub(crate) config_file: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
pub(crate) metadata: Option<PathBuf>,
#[arg(long)]
pub(crate) no_api: bool,
#[arg(long, value_name = "PATH", conflicts_with = "no_seccomp")]
pub(crate) seccomp_filter: Option<PathBuf>,
#[arg(long, conflicts_with = "seccomp_filter")]
pub(crate) no_seccomp: bool,
#[arg(long, value_name = "PATH")]
pub(crate) log_path: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = LogLevel::Info)]
pub(crate) level: LogLevel,
#[arg(long, value_name = "MODULE")]
pub(crate) module: Option<String>,
#[arg(long)]
pub(crate) show_level: bool,
#[arg(long)]
pub(crate) show_log_origin: bool,
#[arg(long, value_name = "PATH")]
pub(crate) metrics_path: Option<PathBuf>,
#[arg(long, value_name = "BYTES", default_value_t = 51_200)]
pub(crate) http_api_max_payload_size: u32,
#[arg(long, value_name = "BYTES")]
pub(crate) mmds_size_limit: Option<u32>,
#[arg(long)]
pub(crate) boot_timer: bool,
#[arg(long)]
pub(crate) enable_pci: bool,
#[arg(long, value_name = "MICROS")]
pub(crate) start_time_us: Option<u64>,
#[arg(long, value_name = "MICROS")]
pub(crate) start_time_cpu_us: Option<u64>,
#[arg(long, value_name = "MICROS")]
pub(crate) parent_cpu_time_us: Option<u64>,
#[arg(long)]
pub(crate) snapshot_version: bool,
#[arg(long, value_name = "PATH")]
pub(crate) describe_snapshot: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = NetworkMode::Shared)]
pub(crate) network: NetworkMode,
#[arg(long, value_name = "PATH", env = "SQUIB_GVPROXY_PATH")]
pub(crate) gvproxy_path: Option<PathBuf>,
#[arg(long, value_name = "IFNAME")]
pub(crate) bridged_iface: Option<String>,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
#[value(rename_all = "PascalCase")]
pub(crate) enum LogLevel {
Off,
Error,
Warning,
Info,
Debug,
Trace,
}
impl LogLevel {
pub(crate) fn as_directive(self) -> &'static str {
match self {
Self::Off => "off",
Self::Error => "error",
Self::Warning => "warn",
Self::Info => "info",
Self::Debug => "debug",
Self::Trace => "trace",
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
#[value(rename_all = "lowercase")]
pub(crate) enum NetworkMode {
Shared,
Bridged,
Host,
Userspace,
}
impl NetworkMode {
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn to_net_mode(self) -> Result<squib_net::NetMode, &'static str> {
match self {
Self::Shared => Ok(squib_net::NetMode::SHARED),
Self::Host => Ok(squib_net::NetMode::HOST),
Self::Userspace => Ok(squib_net::NetMode::USERSPACE),
Self::Bridged => {
#[cfg(feature = "bridged")]
{
Ok(squib_net::NetMode::BRIDGED)
}
#[cfg(not(feature = "bridged"))]
{
Err(
"--network=bridged requires a binary built with `--features bridged` and \
signed with `com.apple.vm.networking`. See specs/30-networking.md § 5.",
)
}
}
}
}
}
#[cfg(test)]
mod tests {
use clap::CommandFactory;
use super::*;
#[test]
fn cli_parses_minimal_invocation() {
let args = Args::try_parse_from(["squib"]).unwrap();
assert_eq!(args.id, "anonymous");
assert!(matches!(args.network, NetworkMode::Shared));
}
#[test]
fn cli_parses_full_firecracker_compat_invocation() {
let args = Args::try_parse_from([
"squib",
"--api-sock",
"/tmp/fc.sock",
"--id",
"vm1",
"--config-file",
"/tmp/vm.json",
"--log-path",
"/tmp/fc.log",
"--level",
"Debug",
"--show-level",
"--show-log-origin",
"--metrics-path",
"/tmp/fc.metrics",
"--boot-timer",
"--http-api-max-payload-size",
"65536",
])
.unwrap();
assert_eq!(args.id, "vm1");
assert_eq!(args.http_api_max_payload_size, 65_536);
assert!(args.boot_timer);
assert!(args.show_level);
}
#[test]
fn seccomp_flags_are_mutually_exclusive() {
let err =
Args::try_parse_from(["squib", "--seccomp-filter", "/x", "--no-seccomp"]).unwrap_err();
assert!(err.to_string().contains("cannot be used with"));
}
#[test]
fn cli_command_definition_is_well_formed() {
Args::command().debug_assert();
}
#[test]
fn cli_should_parse_userspace_network_mode_with_gvproxy_path() {
let args = Args::try_parse_from([
"squib",
"--network",
"userspace",
"--gvproxy-path",
"/opt/squib/gvproxy",
])
.unwrap();
assert!(matches!(args.network, NetworkMode::Userspace));
assert_eq!(
args.gvproxy_path.as_deref().and_then(|p| p.to_str()),
Some("/opt/squib/gvproxy")
);
}
#[test]
fn cli_should_translate_shared_into_squib_net_shared_mode() {
let m = NetworkMode::Shared.to_net_mode().unwrap();
assert!(matches!(m, squib_net::NetMode::Vmnet(_)));
assert!(!m.needs_restricted_entitlement());
}
#[test]
fn cli_should_reject_bridged_without_feature_flag() {
let r = NetworkMode::Bridged.to_net_mode();
#[cfg(not(feature = "bridged"))]
{
assert!(r.is_err());
}
#[cfg(feature = "bridged")]
{
assert!(r.is_ok());
}
}
}