diff --git a/src/apply.rs b/src/apply.rs new file mode 100644 index 0000000..f4ccd7f --- /dev/null +++ b/src/apply.rs @@ -0,0 +1,45 @@ +use eyre::Result; +use log::info; +use std::path::Path; +use tokio::fs; + +pub async fn files(files: &[crate::File], root: &str) -> Result<()> { + for file in files { + let path = chroot(root, &file.path); + let path = Path::new(&path); + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).await?; + } + + use crate::FileKind as K; + match &file.kind { + K::Content(content) => fs::write(path, content.as_bytes()).await?, + K::Dir(true) => fs::create_dir(path).await?, + K::Dir(false) => {} // shouldn't happen, but semantic is to ignore + K::Symlink(tgt) => fs::symlink(tgt, path).await?, + } + + match file.kind { + K::Symlink(_) => {} + _ => set_perms(path, file.mode).await?, + } + + info!("created {}", file.path); + } + + Ok(()) +} + +pub async fn set_perms(path: impl AsRef, mode: Option) -> std::io::Result<()> { + if let Some(mode) = mode.filter(|m| *m != 0) { + use std::os::unix::fs::PermissionsExt; + let mode = std::fs::Permissions::from_mode(mode); + fs::set_permissions(path, mode).await?; + } + Ok(()) +} + +pub fn chroot(root: &str, path: &str) -> String { + format!("{root}/{}", path.trim_start_matches(|c| c == '/')) +} diff --git a/src/bin/dls.rs b/src/bin/dls.rs index 70eef8e..5aaf339 100644 --- a/src/bin/dls.rs +++ b/src/bin/dls.rs @@ -1,5 +1,5 @@ use clap::{CommandFactory, Parser, Subcommand}; -use eyre::{format_err, Result}; +use eyre::{Result, format_err}; use futures_util::StreamExt; use tokio::io::AsyncWriteExt; diff --git a/src/bootstrap.rs b/src/bootstrap.rs new file mode 100644 index 0000000..46abf30 --- /dev/null +++ b/src/bootstrap.rs @@ -0,0 +1,192 @@ +use std::collections::BTreeMap as Map; + +pub const TAKE_ALL: i16 = -1; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Config { + pub anti_phishing_code: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub keymap: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub modules: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub resolv_conf: Option, + + #[serde(default)] + pub vpns: Map, + + pub networks: Vec, + + pub auths: Vec, + #[serde(default)] + pub ssh: SSHServer, + + #[serde(default)] + pub pre_lvm_crypt: Vec, + #[serde(default)] + pub lvm: Vec, + #[serde(default)] + pub crypt: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + pub signer_public_key: Option, + + pub bootstrap: Bootstrap, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Auth { + pub name: String, + #[serde(alias = "sshKey")] + #[serde(skip_serializing_if = "Option::is_none")] + pub ssh_key: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub password: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Network { + pub name: String, + pub interfaces: Vec, + pub script: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct NetworkInterface { + pub var: String, + pub n: i16, + pub regexps: Vec, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct SSHServer { + pub listen: String, + pub user_ca: Option, +} +impl Default for SSHServer { + fn default() -> Self { + Self { + listen: "[::]:22".to_string(), + user_ca: None, + } + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct LvmVG { + #[serde(alias = "vg")] + pub name: String, + pub pvs: LvmPV, + + #[serde(default)] + pub defaults: LvmLVDefaults, + + pub lvs: Vec, +} + +#[derive(Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct LvmLVDefaults { + #[serde(default)] + pub fs: Filesystem, + #[serde(default)] + pub raid: Raid, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Filesystem { + Ext4, + Xfs, + Btrfs, + Other(String), +} + +impl Filesystem { + pub fn fstype(&self) -> &str { + use Filesystem as F; + match self { + F::Ext4 => "ext4", + F::Xfs => "xfs", + F::Btrfs => "btrfs", + F::Other(t) => t, + } + } +} + +impl Default for Filesystem { + fn default() -> Self { + Filesystem::Ext4 + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct LvmLV { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub fs: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub raid: Option, + #[serde(flatten)] + pub size: LvSize, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum LvSize { + Size(String), + Extents(String), +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct LvmPV { + pub n: i16, + pub regexps: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct CryptDev { + pub name: String, + #[serde(flatten)] + pub filter: DevFilter, + pub optional: Option, +} +impl CryptDev { + pub fn optional(&self) -> bool { + self.optional.unwrap_or_else(|| self.filter.is_prefix()) + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum DevFilter { + Dev(String), + Prefix(String), +} +impl DevFilter { + pub fn is_dev(&self) -> bool { + match self { + Self::Dev(_) => true, + _ => false, + } + } + pub fn is_prefix(&self) -> bool { + match self { + Self::Prefix(_) => true, + _ => false, + } + } +} + +#[derive(Debug, Default, Clone, serde::Deserialize, serde::Serialize)] +pub struct Raid { + pub mirrors: Option, + pub stripes: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Bootstrap { + pub dev: String, + pub seed: Option, +} diff --git a/src/lib.rs b/src/lib.rs index 6b9c50d..309b880 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,65 @@ +pub mod apply; +pub mod bootstrap; pub mod dls; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Config { + pub layers: Vec, + pub root_user: RootUser, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub mounts: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub files: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub groups: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub users: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct RootUser { + #[serde(skip_serializing_if = "Option::is_none")] + pub password_hash: Option, + pub authorized_keys: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Mount { + pub r#type: Option, + pub dev: String, + pub path: String, + pub options: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Group { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub gid: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct User { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub uid: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub gid: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct File { + pub path: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + #[serde(flatten)] + pub kind: FileKind, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum FileKind { + Content(String), + Symlink(String), + Dir(bool), +}