1use serde::{Deserialize, Serialize};
4use tatara_lisp_derive::TataraDomain as DeriveTataraDomain;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize,
7 gen_platform::TypedDispatcher,
8 gen_platform::Discriminant,
9 gen_platform::IsVariant,
10 gen_platform::FromStrKind)]
11#[discriminant(also_display)]
12#[serde(tag = "kind")]
13pub enum Hypervisor {
14 Vfkit,
17 VfkitDarwin,
21 Qemu,
23 Kasou,
26 Libkrun,
30}
31
32impl Default for Hypervisor {
33 fn default() -> Self {
34 Self::Vfkit
35 }
36}
37
38gen_platform::register_dispatcher!("tatara.hypervisor", Hypervisor);
44
45#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(tag = "kind")]
47pub enum GuestKernel {
48 Bridge { attr_path: String },
50 Custom { derivation: tatara_nix::Derivation },
52 DarwinIpsw { ipsw_path: String },
57}
58
59impl Default for GuestKernel {
60 fn default() -> Self {
61 Self::Bridge {
62 attr_path: "linuxPackages.kernel".into(),
63 }
64 }
65}
66
67#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(tag = "kind")]
69pub enum GuestRootfs {
70 System { name: String },
73 Image { derivation: tatara_nix::Derivation },
75 Bridge { attr_path: String },
77}
78
79impl Default for GuestRootfs {
80 fn default() -> Self {
81 Self::Bridge {
82 attr_path: "nixpkgs-images.minimal-rootfs".into(),
83 }
84 }
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
89pub enum NetworkKind {
90 Nat,
92 Bridge,
94 None,
96}
97
98impl Default for NetworkKind {
99 fn default() -> Self {
100 Self::Nat
101 }
102}
103
104#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct NetworkSpec {
107 #[serde(default)]
108 pub kind: NetworkKind,
109 #[serde(default)]
110 pub subnet: Option<String>,
111 #[serde(default)]
112 pub host_interface: Option<String>,
113}
114
115impl Default for NetworkSpec {
116 fn default() -> Self {
117 Self {
118 kind: NetworkKind::Nat,
119 subnet: None,
120 host_interface: None,
121 }
122 }
123}
124
125#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct ShareSpec {
128 pub host: String,
130 pub guest: String,
132 #[serde(default)]
134 pub read_only: bool,
135}
136
137#[derive(DeriveTataraDomain, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151#[tatara(keyword = "defvm")]
152pub struct VmSpec {
153 pub name: String,
154 #[serde(default = "default_cpus")]
155 pub cpus: u32,
156 #[serde(default = "default_mem_mib")]
157 pub memory_mib: u32,
158 #[serde(default)]
159 pub hypervisor: Hypervisor,
160 #[serde(default)]
161 pub kernel: GuestKernel,
162 #[serde(default)]
163 pub initrd: Option<tatara_nix::Derivation>,
164 #[serde(default)]
165 pub rootfs: GuestRootfs,
166 #[serde(default)]
167 pub network: NetworkSpec,
168 #[serde(default)]
169 pub shares: Vec<ShareSpec>,
170 #[serde(default = "default_cmdline")]
172 pub cmdline: Vec<String>,
173}
174
175fn default_cpus() -> u32 {
176 2
177}
178
179fn default_mem_mib() -> u32 {
180 2048
181}
182
183fn default_cmdline() -> Vec<String> {
184 vec!["console=hvc0".into(), "init=/bin/tatara-init".into()]
185}
186
187impl VmSpec {
188 pub fn plex_default(name: impl Into<String>) -> Self {
191 Self {
192 name: name.into(),
193 cpus: default_cpus(),
194 memory_mib: default_mem_mib(),
195 hypervisor: Hypervisor::Vfkit,
196 kernel: GuestKernel::default(),
197 initrd: None,
198 rootfs: GuestRootfs::default(),
199 network: NetworkSpec::default(),
200 shares: vec![],
201 cmdline: default_cmdline(),
202 }
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use tatara_lisp::{domain::TataraDomain, read};
210
211 #[test]
212 fn minimal_defvm_parses() {
213 let forms = read(
216 r#"(defvm
217 :name "plex-guest"
218 :cpus 2
219 :memory-mib 2048)"#,
220 )
221 .unwrap();
222 let v = VmSpec::compile_from_sexp(&forms[0]).unwrap();
223 assert_eq!(v.name, "plex-guest");
224 assert_eq!(v.cpus, 2);
225 assert_eq!(v.memory_mib, 2048);
226 }
227
228 #[test]
229 fn full_defvm_parses_with_shares_and_network() {
230 let forms = read(
231 r#"(defvm
232 :name "plex-guest"
233 :cpus 4
234 :memory-mib 4096
235 :hypervisor (:kind "Vfkit")
236 :kernel (:kind "Bridge" :attr_path "linuxPackages.kernel")
237 :rootfs (:kind "Bridge" :attr_path "nixpkgs-images.minimal-rootfs")
238 :network (:kind "Nat" :subnet "10.200.0.0/24")
239 :shares ((:host "/Users/drzzln/code" :guest "/mnt/code" :read_only #f))
240 :cmdline ("console=hvc0" "init=/bin/tatara-init"))"#,
241 )
242 .unwrap();
243 let v = VmSpec::compile_from_sexp(&forms[0]).unwrap();
244 assert_eq!(v.cpus, 4);
245 assert_eq!(v.memory_mib, 4096);
246 assert_eq!(v.shares.len(), 1);
247 assert_eq!(v.shares[0].host, "/Users/drzzln/code");
248 assert_eq!(v.shares[0].guest, "/mnt/code");
249 match v.network.kind {
250 NetworkKind::Nat => (),
251 _ => panic!("expected Nat"),
252 }
253 }
254
255 #[test]
256 fn defaults_are_darwin_friendly() {
257 let v = VmSpec::plex_default("plex");
258 assert!(matches!(v.hypervisor, Hypervisor::Vfkit));
259 assert!(v.cmdline.iter().any(|a| a.contains("tatara-init")));
260 }
261}