use serde::{Deserialize, Serialize};
use tatara_lisp_derive::TataraDomain as DeriveTataraDomain;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum KernelSpec {
Bridge { attr_path: String },
Package { name: String },
Custom { derivation: tatara_nix::Derivation },
}
impl Default for KernelSpec {
fn default() -> Self {
Self::Bridge {
attr_path: "linuxPackages.kernel".into(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum BootloaderKind {
Grub,
SystemdBoot,
Uboot,
None,
}
impl Default for BootloaderKind {
fn default() -> Self {
Self::SystemdBoot
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BootloaderSpec {
#[serde(default)]
pub kind: BootloaderKind,
#[serde(default)]
pub device: String,
#[serde(default)]
pub kernel_cmdline: Vec<String>,
}
impl Default for BootloaderSpec {
fn default() -> Self {
Self {
kind: BootloaderKind::default(),
device: "/boot".into(),
kernel_cmdline: vec![],
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum InitSystem {
Tatara,
Systemd,
S6,
OpenRC,
}
impl Default for InitSystem {
fn default() -> Self {
Self::Tatara
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServiceSpec {
pub name: String,
pub exec: String,
#[serde(default = "default_true")]
pub enable: bool,
#[serde(default)]
pub extra: Vec<String>,
#[serde(default)]
pub package_refs: Vec<String>,
}
fn default_true() -> bool {
true
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserSpec {
pub name: String,
#[serde(default)]
pub uid: Option<u32>,
#[serde(default)]
pub gid: Option<u32>,
#[serde(default)]
pub home: Option<String>,
#[serde(default)]
pub shell: Option<String>,
#[serde(default)]
pub groups: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FilesystemSpec {
pub mount: String,
pub device: String,
pub fs_type: String,
#[serde(default)]
pub options: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct EnvSpec {
#[serde(default)]
pub timezone: Option<String>,
#[serde(default)]
pub locale: Option<String>,
#[serde(default)]
pub etc_files: Vec<EtcFile>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EtcFile {
pub path: String,
pub content: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SshdSpec {
#[serde(default = "default_ssh_port")]
pub port: u16,
#[serde(default)]
pub authorized_keys: Vec<String>,
#[serde(default)]
pub permit_root: bool,
#[serde(default)]
pub password_authentication: bool,
}
fn default_ssh_port() -> u16 {
22
}
impl Default for SshdSpec {
fn default() -> Self {
Self {
port: 22,
authorized_keys: vec![],
permit_root: false,
password_authentication: false,
}
}
}
#[derive(DeriveTataraDomain, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[tatara(keyword = "defsystem")]
pub struct SystemConfig {
pub hostname: String,
#[serde(default = "default_system")]
pub system: String,
#[serde(default)]
pub kernel: KernelSpec,
#[serde(default)]
pub bootloader: BootloaderSpec,
#[serde(default)]
pub init: InitSystem,
#[serde(default)]
pub services: Vec<ServiceSpec>,
#[serde(default)]
pub users: Vec<UserSpec>,
#[serde(default)]
pub filesystems: Vec<FilesystemSpec>,
#[serde(default)]
pub environment: EnvSpec,
#[serde(default)]
pub packages: Vec<String>,
#[serde(default)]
pub sshd: Option<SshdSpec>,
}
fn default_system() -> String {
"x86_64-linux".into()
}
#[cfg(test)]
mod tests {
use super::*;
use tatara_lisp::{domain::TataraDomain, read};
#[test]
fn minimal_system_compiles_from_lisp() {
let forms = read(
r#"(defsystem
:hostname "plex"
:system "x86_64-linux")"#,
)
.unwrap();
let s = SystemConfig::compile_from_sexp(&forms[0]).unwrap();
assert_eq!(s.hostname, "plex");
assert_eq!(s.system, "x86_64-linux");
assert!(matches!(s.init, InitSystem::Tatara));
assert!(matches!(s.bootloader.kind, BootloaderKind::SystemdBoot));
}
#[test]
fn full_system_compiles_from_lisp() {
let forms = read(
r#"(defsystem
:hostname "plex"
:system "x86_64-linux"
:services ((:name "nginx" :exec "nginx -g 'daemon off;'")
(:name "fumi" :exec "/bin/fumi" :enable #f))
:users ((:name "drzzln" :uid 1000))
:packages ("hello" "bash" "zsh"))"#,
)
.unwrap();
let s = SystemConfig::compile_from_sexp(&forms[0]).unwrap();
assert_eq!(s.services.len(), 2);
assert_eq!(s.services[1].name, "fumi");
assert!(!s.services[1].enable);
assert_eq!(s.users[0].name, "drzzln");
assert_eq!(s.packages, vec!["hello", "bash", "zsh"]);
}
}