use serde::{Deserialize, Serialize};
use tatara_lisp_derive::TataraDomain as DeriveTataraDomain;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize,
gen_platform::TypedDispatcher,
gen_platform::Discriminant,
gen_platform::IsVariant,
gen_platform::FromStrKind)]
#[discriminant(also_display)]
#[serde(tag = "kind")]
pub enum Hypervisor {
Vfkit,
VfkitDarwin,
Qemu,
Kasou,
Libkrun,
}
impl Default for Hypervisor {
fn default() -> Self {
Self::Vfkit
}
}
gen_platform::register_dispatcher!("tatara.hypervisor", Hypervisor);
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum GuestKernel {
Bridge { attr_path: String },
Custom { derivation: tatara_nix::Derivation },
DarwinIpsw { ipsw_path: String },
}
impl Default for GuestKernel {
fn default() -> Self {
Self::Bridge {
attr_path: "linuxPackages.kernel".into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum GuestRootfs {
System { name: String },
Image { derivation: tatara_nix::Derivation },
Bridge { attr_path: String },
}
impl Default for GuestRootfs {
fn default() -> Self {
Self::Bridge {
attr_path: "nixpkgs-images.minimal-rootfs".into(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum NetworkKind {
Nat,
Bridge,
None,
}
impl Default for NetworkKind {
fn default() -> Self {
Self::Nat
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkSpec {
#[serde(default)]
pub kind: NetworkKind,
#[serde(default)]
pub subnet: Option<String>,
#[serde(default)]
pub host_interface: Option<String>,
}
impl Default for NetworkSpec {
fn default() -> Self {
Self {
kind: NetworkKind::Nat,
subnet: None,
host_interface: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ShareSpec {
pub host: String,
pub guest: String,
#[serde(default)]
pub read_only: bool,
}
#[derive(DeriveTataraDomain, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[tatara(keyword = "defvm")]
pub struct VmSpec {
pub name: String,
#[serde(default = "default_cpus")]
pub cpus: u32,
#[serde(default = "default_mem_mib")]
pub memory_mib: u32,
#[serde(default)]
pub hypervisor: Hypervisor,
#[serde(default)]
pub kernel: GuestKernel,
#[serde(default)]
pub initrd: Option<tatara_nix::Derivation>,
#[serde(default)]
pub rootfs: GuestRootfs,
#[serde(default)]
pub network: NetworkSpec,
#[serde(default)]
pub shares: Vec<ShareSpec>,
#[serde(default = "default_cmdline")]
pub cmdline: Vec<String>,
}
fn default_cpus() -> u32 {
2
}
fn default_mem_mib() -> u32 {
2048
}
fn default_cmdline() -> Vec<String> {
vec!["console=hvc0".into(), "init=/bin/tatara-init".into()]
}
impl VmSpec {
pub fn plex_default(name: impl Into<String>) -> Self {
Self {
name: name.into(),
cpus: default_cpus(),
memory_mib: default_mem_mib(),
hypervisor: Hypervisor::Vfkit,
kernel: GuestKernel::default(),
initrd: None,
rootfs: GuestRootfs::default(),
network: NetworkSpec::default(),
shares: vec![],
cmdline: default_cmdline(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tatara_lisp::{domain::TataraDomain, read};
#[test]
fn minimal_defvm_parses() {
let forms = read(
r#"(defvm
:name "plex-guest"
:cpus 2
:memory-mib 2048)"#,
)
.unwrap();
let v = VmSpec::compile_from_sexp(&forms[0]).unwrap();
assert_eq!(v.name, "plex-guest");
assert_eq!(v.cpus, 2);
assert_eq!(v.memory_mib, 2048);
}
#[test]
fn full_defvm_parses_with_shares_and_network() {
let forms = read(
r#"(defvm
:name "plex-guest"
:cpus 4
:memory-mib 4096
:hypervisor (:kind "Vfkit")
:kernel (:kind "Bridge" :attr_path "linuxPackages.kernel")
:rootfs (:kind "Bridge" :attr_path "nixpkgs-images.minimal-rootfs")
:network (:kind "Nat" :subnet "10.200.0.0/24")
:shares ((:host "/Users/drzzln/code" :guest "/mnt/code" :read_only #f))
:cmdline ("console=hvc0" "init=/bin/tatara-init"))"#,
)
.unwrap();
let v = VmSpec::compile_from_sexp(&forms[0]).unwrap();
assert_eq!(v.cpus, 4);
assert_eq!(v.memory_mib, 4096);
assert_eq!(v.shares.len(), 1);
assert_eq!(v.shares[0].host, "/Users/drzzln/code");
assert_eq!(v.shares[0].guest, "/mnt/code");
match v.network.kind {
NetworkKind::Nat => (),
_ => panic!("expected Nat"),
}
}
#[test]
fn defaults_are_darwin_friendly() {
let v = VmSpec::plex_default("plex");
assert!(matches!(v.hypervisor, Hypervisor::Vfkit));
assert!(v.cmdline.iter().any(|a| a.contains("tatara-init")));
}
}