5 Commits

Author SHA1 Message Date
f892178d5d wget -> reqwest, now we can have openssl :) 2025-07-21 17:48:26 +02:00
cb62ac0ed8 remove system archive feature
Just compress the initrd with zstd.
Remove rsmount dependency, mtab is easy enough to parse.
2025-07-21 17:12:44 +02:00
0d9d087afd use shared libs, enabling openssl in init 2025-07-21 03:25:48 +02:00
e484802284 bootstrap: chore: extract fn mount_modules 2025-07-18 08:19:17 +02:00
423a9c53e6 move configs to dkl crate 2025-07-17 16:48:38 +02:00
17 changed files with 1601 additions and 565 deletions

1535
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "init"
version = "2.4.1"
version = "2.4.2"
edition = "2024"
[profile.release]
@ -13,7 +13,7 @@ codegen-units = 1
[dependencies]
libc = { version = "0.2", default-features = false }
env_logger = "0.11.3"
eyre = "0.6.12"
eyre = { version = "0.6.12" }
itertools = "0.14.0"
log = "0.4.21"
nix = { version = "0.30.1", features = ["feature", "mount", "process", "reboot", "signal"] }
@ -24,9 +24,9 @@ serde_yaml = "0.9.34"
shell-escape = "0.1.5"
tokio = { version = "1.38.0", features = ["rt", "net", "fs", "process", "io-std", "io-util", "sync", "macros", "signal"] }
termios = "0.3.3"
zstd = "0.13.3"
unix_mode = "0.1.4"
cpio = "0.4.1"
lz4 = "1.28.1"
base64 = "0.22.1"
sys-info = "0.9.1"
dkl = { git = "https://novit.tech/direktil/dkl", version = "1.0.0" }
openssl = "0.10.73"
reqwest = { version = "0.12.22", features = ["native-tls"] }

View File

@ -1,12 +1,12 @@
from rust:1.88.0-alpine as rust
run apk add --no-cache git musl-dev libudev-zero-dev # pkgconfig cryptsetup-dev lvm2-dev clang-dev clang-static
run apk add --no-cache git musl-dev libudev-zero-dev openssl-dev cryptsetup-dev lvm2-dev clang-libs clang-dev
workdir /src
copy . .
run --mount=type=cache,id=novit-rs,target=/usr/local/cargo/registry \
--mount=type=cache,id=novit-rs-target,sharing=private,target=/src/target \
cargo build --release && cp target/release/init /
RUSTFLAGS="-C target-feature=-crt-static" cargo install --path . --root /dist
# ------------------------------------------------------------------------
from alpine:3.22.0 as initrd
@ -17,20 +17,15 @@ workdir /system
run . /etc/os-release \
&& wget -O- https://dl-cdn.alpinelinux.org/alpine/v${VERSION_ID%.*}/releases/x86_64/alpine-minirootfs-${VERSION_ID}-x86_64.tar.gz |tar zxv
run apk add --no-cache --update -p . musl coreutils \
run apk add --no-cache --update -p . musl libgcc coreutils \
lvm2 lvm2-extra lvm2-dmeventd udev cryptsetup \
e2fsprogs lsblk openssl openssh-server wireguard-tools-wg-quick \
&& rm -rf usr/share/apk var/cache/apk etc/motd
copy etc/sshd_config etc/ssh/sshd_config
run mkdir /layer \
&& mv dev /layer \
# && find |cpio -H newc -o |lz4 >/layer/system.alz4
&& find |cpio -H newc -o |zstd -19 >/layer/system.azstd
copy --from=rust /dist/bin/init /system/init
workdir /layer
copy --from=rust /init init
run mkdir -p bin run var/log; cd bin && for cmd in init-version init-connect bootstrap; do ln -s ../init $cmd; done
# check viability

View File

@ -14,8 +14,7 @@ cpio --quiet --extract --file $base_initrd --directory $dir
(cd $dir && find * |cpio --create -H newc -R 0:0) >test-initrd.cpio
cpio --quiet -tF test-initrd.cpio
if cpio -tF test-initrd.cpio 2>&1 |grep bytes.of.junk; then echo "bad cpio archive"; exit 1; fi
lz4 -l9v test-initrd.cpio && mv test-initrd.cpio.lz4 test-initrd.cpio
zstd -12 -T0 -vf test-initrd.cpio && mv test-initrd.cpio.zst test-initrd.cpio

View File

@ -1 +0,0 @@
pub mod config;

View File

@ -1,192 +0,0 @@
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modules: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resolv_conf: Option<String>,
#[serde(default)]
pub vpns: Map<String, String>,
pub networks: Vec<Network>,
pub auths: Vec<Auth>,
#[serde(default)]
pub ssh: SSHServer,
#[serde(default)]
pub pre_lvm_crypt: Vec<CryptDev>,
#[serde(default)]
pub lvm: Vec<LvmVG>,
#[serde(default)]
pub crypt: Vec<CryptDev>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signer_public_key: Option<String>,
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Network {
pub name: String,
pub interfaces: Vec<NetworkInterface>,
pub script: String,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct NetworkInterface {
pub var: String,
pub n: i16,
pub regexps: Vec<String>,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct SSHServer {
pub listen: String,
pub user_ca: Option<String>,
}
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<LvmLV>,
}
#[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<Filesystem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raid: Option<Raid>,
#[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<String>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct CryptDev {
pub name: String,
#[serde(flatten)]
pub filter: DevFilter,
pub optional: Option<bool>,
}
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<u8>,
pub stripes: Option<u8>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Bootstrap {
pub dev: String,
pub seed: Option<String>,
}

View File

@ -0,0 +1 @@

View File

@ -5,7 +5,8 @@ use std::os::unix::fs::symlink;
use tokio::sync::Mutex;
use tokio::{fs, process::Command};
use crate::{bootstrap::config::Config, cmd::version::version_string, dklog, input, utils};
use crate::{cmd::version::version_string, dklog, input, utils};
use dkl::bootstrap::Config;
mod bootstrap;
mod dmcrypt;
@ -61,24 +62,6 @@ pub async fn run() {
log::set_max_level(log::LevelFilter::Debug);
}
// extract system archive
retry_or_ignore(async || {
if fs::try_exists("system.azstd").await? {
info!("unpacking system.azstd");
let zarch = fs::read("system.azstd").await?;
let arch = zstd::Decoder::new(zarch.as_slice())?;
extract_cpio(arch).await
} else if fs::try_exists("system.alz4").await? {
info!("unpacking system.alz4");
let zarch = fs::read("system.alz4").await?;
let arch = lz4::Decoder::new(zarch.as_slice())?;
extract_cpio(arch).await
} else {
return Ok(());
}
})
.await;
// load config
let cfg: Config = retry(async || {
let cfg = (fs::read("config.yaml").await)
@ -95,24 +78,8 @@ pub async fn run() {
// tokio::spawn(child_reaper());
// mount modules
if let Some(ref modules) = cfg.modules {
retry_or_ignore(async || {
info!("mounting modules");
mount(Some(modules), "/modules", "squashfs", None).await;
fs::create_dir_all("/lib/modules").await?;
let modules_path = &format!("/modules/lib/modules/{kernel_version}");
if !std::fs::exists(modules_path)? {
return Err(format_err!(
"invalid modules package: {modules_path} should exist"
));
}
symlink(modules_path, format!("/lib/modules/{kernel_version}"))?;
Ok(())
})
.await;
if let Some(modules) = cfg.modules.as_deref() {
retry_or_ignore(async || mount_modules(modules, &kernel_version).await).await;
} else {
warn!("modules NOT mounted (not configured)");
}
@ -180,6 +147,23 @@ pub async fn run() {
use std::path::Path;
async fn mount_modules(modules: &str, kernel_version: &str) -> Result<()> {
info!("mounting modules");
mount(Some(modules), "/modules", "squashfs", None).await;
fs::create_dir_all("/lib/modules").await?;
let modules_path = &format!("/modules/lib/modules/{kernel_version}");
if !std::fs::exists(modules_path)? {
return Err(format_err!(
"invalid modules package: {modules_path} should exist"
));
}
symlink(modules_path, format!("/lib/modules/{kernel_version}"))?;
Ok(())
}
async fn chmod(path: impl AsRef<Path>, mode: u32) -> std::io::Result<()> {
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
@ -188,85 +172,18 @@ async fn chmod(path: impl AsRef<Path>, mode: u32) -> std::io::Result<()> {
fs::set_permissions(path, perms).await
}
async fn extract_cpio(mut arch: impl std::io::Read) -> Result<()> {
loop {
let rd = cpio::NewcReader::new(&mut arch)?;
let entry = rd.entry();
if entry.is_trailer() {
return Ok(());
}
let path = entry.name().to_string();
if let Err(e) = extract_cpio_entry(rd, &path).await {
return Err(format_err!("failed to extract {path}: {e}"));
}
}
}
async fn extract_cpio_entry<R: std::io::Read>(
rd: cpio::NewcReader<R>,
path: impl AsRef<Path>,
) -> Result<()> {
use std::os::unix::fs::chown;
use unix_mode::Type;
let entry = rd.entry();
let path = path.as_ref();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
let mode = entry.mode();
let uid = entry.uid();
let gid = entry.gid();
match Type::from(mode) {
Type::Dir => {
fs::create_dir_all(path).await?;
}
Type::File => {
let mut data = vec![];
rd.to_writer(&mut data)?;
fs::write(path, data).await?;
}
Type::Symlink => {
let mut data = vec![];
rd.to_writer(&mut data)?;
let target = &Path::new(std::str::from_utf8(&data)?);
tokio::fs::symlink(target, path).await?;
return Ok(());
}
_ => {
warn!("{path:?}: unknown file type: {:?}", Type::from(mode));
return Ok(());
}
}
chmod(path, mode).await?;
chown(path, Some(uid), Some(gid))?;
Ok(())
}
async fn mount(src: Option<&str>, dst: &str, fstype: &str, opts: Option<&str>) {
if let Err(e) = fs::create_dir_all(dst).await {
error!("failed to create dir {dst}: {e}");
}
retry_or_ignore(async || {
let mut is_file = false;
if let Some(src) = src {
retry_or_ignore(async || {
is_file = (fs::metadata(src).await)
.map_err(|e| format_err!("stat {src} failed: {e}"))?
.is_file();
Ok(())
})
.await;
match fstype {
"ext4" => {
exec("fsck.ext4", &["-p", src]).await;
@ -280,7 +197,6 @@ async fn mount(src: Option<&str>, dst: &str, fstype: &str, opts: Option<&str>) {
args.extend(["-o", opts]);
}
retry_or_ignore(async || {
// if it's a file, we need to use a loopdev
if is_file {
// loopdev crate has annoying dependencies, just use the normal mount program

View File

@ -1,24 +1,24 @@
use eyre::{format_err, Result};
use log::{info, warn};
use std::path::Path;
use tokio::{
fs,
io::{AsyncBufReadExt, BufReader},
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
};
use dkl::{
self,
apply::{self, chroot, set_perms},
bootstrap::Config,
};
use super::{exec, mount, retry, retry_or_ignore, try_exec};
use crate::bootstrap::config::Config;
use crate::{dkl, utils};
use crate::utils;
pub async fn bootstrap(cfg: Config) {
let verifier = retry(async || Verifier::from_config(&cfg)).await;
let bs = cfg.bootstrap;
retry_or_ignore(async || {
mount(Some(&bs.dev), "/bootstrap", "ext4", None).await;
Ok(())
})
.await;
let boot_version = utils::param("version").unwrap_or("current");
let base_dir = &format!("/bootstrap/{boot_version}");
@ -50,7 +50,7 @@ pub async fn bootstrap(cfg: Config) {
})
.await;
retry_or_ignore(async || apply_files(&sys_cfg.files, "/system").await).await;
retry_or_ignore(async || apply::files(&sys_cfg.files, "/system").await).await;
apply_groups(&sys_cfg.groups, "/system").await;
apply_users(&sys_cfg.users, "/system").await;
@ -84,42 +84,29 @@ impl Verifier {
return Ok(Self { pubkey });
}
async fn verify_path(&self, path: &str) -> Result<()> {
async fn verify_path(&self, path: &str) -> Result<Vec<u8>> {
let data = (fs::read(path).await).map_err(|e| format_err!("failed to read {path}: {e}"))?;
let Some(ref pubkey) = self.pubkey else {
return Ok(());
return Ok(data);
};
info!("verifying {path}");
let mut pubkey = std::io::Cursor::new(pubkey);
let sig = &format!("{path}.sig");
let sig = (fs::read(sig).await).map_err(|e| format_err!("failed to read {sig}: {e}"))?;
let sig = format!("{path}.sig");
use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier};
let pubkey = PKey::public_key_from_der(pubkey)?;
use std::process::Stdio;
use tokio::process::Command;
let sig_ok = Verifier::new(MessageDigest::sha512(), &pubkey)?
.verify_oneshot(&sig, &data)
.map_err(|e| format_err!("verify failed: {e}"))?;
let mut openssl = Command::new("openssl")
.stdin(Stdio::piped())
.args(&[
"dgst",
"-sha512",
"-verify",
"/dev/stdin",
"-signature",
&sig,
path,
])
.spawn()?;
tokio::io::copy(&mut pubkey, openssl.stdin.as_mut().unwrap()).await?;
let status = openssl.wait().await?;
if status.success() {
Ok(())
if sig_ok {
Ok(data)
} else {
Err(format_err!(
"signature verification failed for {path}: {status}"
))
Err(format_err!("signature verification failed for {path}"))
}
}
}
@ -152,19 +139,27 @@ async fn seed_config(
return Err(format_err!("{cfg_path} does not exist after seeding"));
}
verifier.verify_path(&cfg_path).await?;
Ok(fs::read(cfg_path).await?)
verifier.verify_path(&cfg_path).await
}
async fn fetch_bootstrap(seed_url: &str, output_file: &str) -> Result<()> {
let tmp_file = &format!("{output_file}.new");
let _ = fs::remove_file(tmp_file).await;
try_exec("wget", &["-O", tmp_file, seed_url]).await?;
let seed_url: reqwest::Url = seed_url.parse()?;
fs::rename(tmp_file, output_file)
.await
.map_err(|e| format_err!("seed rename failed: {e}"))?;
info!(
"fetching {output_file} from {}",
seed_url.host_str().unwrap_or("<no host>")
);
let resp = reqwest::get(seed_url).await?;
if !resp.status().is_success() {
return Err(format_err!("HTTP request failed: {}", resp.status()));
}
let data = (resp.bytes().await).map_err(|e| format_err!("HTTP download failed: {e}"))?;
(fs::write(output_file, &data).await)
.map_err(|e| format_err!("output file write failed: {e}"))?;
Ok(())
}
@ -193,19 +188,24 @@ async fn mount_system(cfg: &dkl::Config, bs_dir: &str, verifier: &Verifier) {
let mut lower_dir = String::new();
for layer in &cfg.layers {
let src = if layer == "modules" {
"/modules.sqfs".to_string()
let src = retry(async || {
if layer == "modules" {
let src = "/modules.sqfs";
(fs::read(src).await).map_err(|e| format_err!("read {src} failed: {e}"))
} else {
let p = format!("{bs_dir}/{layer}.fs");
retry(async || verifier.verify_path(&p).await).await;
p
};
verifier.verify_path(&format!("{bs_dir}/{layer}.fs")).await
}
})
.await;
let tgt = &format!("{mem_dir}/{layer}.fs");
retry(async || {
info!("copying layer {layer} from {src}");
fs::copy(&src, tgt).await?;
Ok(())
info!("copying layer {layer}");
let mut out = (fs::File::create(tgt).await)
.map_err(|e| format_err!("create {tgt} failed: {e}"))?;
(out.write_all(&src).await).map_err(|e| format_err!("write failed: {e}"))?;
(out.flush().await).map_err(|e| format_err!("write failed: {e}"))
})
.await;
@ -228,15 +228,8 @@ async fn mount_system(cfg: &dkl::Config, bs_dir: &str, verifier: &Verifier) {
})
.await;
mount(
None,
"/system",
"overlay",
Some(&format!(
"lowerdir={lower_dir},upperdir={upper_dir},workdir={work_dir}"
)),
)
.await;
let opts = format!("lowerdir={lower_dir},upperdir={upper_dir},workdir={work_dir}");
mount(None, "/system", "overlay", Some(&opts)).await;
// make root rshared (default in systemd, required by Kubernetes 1.10+)
// equivalent to "mount --make-rshared /"
@ -250,47 +243,6 @@ async fn mount_system(cfg: &dkl::Config, bs_dir: &str, verifier: &Verifier) {
.await;
}
fn chroot(root: &str, path: &str) -> String {
format!("{root}/{}", path.trim_start_matches(|c| c == '/'))
}
async fn apply_files(files: &[dkl::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::dkl::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(())
}
async fn set_perms(path: impl AsRef<Path>, mode: Option<u32>) -> 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(())
}
async fn apply_groups(groups: &[dkl::Group], root: &str) {
for group in groups {
let mut args = vec![root, "groupadd", "-r"];

View File

@ -1,4 +1,4 @@
use eyre::{format_err, Result};
use eyre::{Result, format_err};
use log::{error, info, warn};
use std::collections::BTreeSet as Set;
use std::process::Stdio;
@ -6,11 +6,11 @@ use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use tokio::sync::Mutex;
use super::{retry_or_ignore, USED_DEVS};
use super::{USED_DEVS, retry_or_ignore};
use crate::blockdev::{is_uninitialized, uninitialize};
use crate::bootstrap::config::{CryptDev, DevFilter};
use crate::fs::walk_dir;
use crate::input;
use dkl::bootstrap::{CryptDev, DevFilter};
pub async fn setup(devs: &[CryptDev]) {
if devs.is_empty() {

View File

@ -1,11 +1,11 @@
use eyre::{format_err, Result};
use eyre::{Result, format_err};
use log::{error, info, warn};
use tokio::process::Command;
use super::{exec, retry, retry_or_ignore, USED_DEVS};
use crate::bootstrap::config::{Config, Filesystem, LvSize, LvmLV, LvmVG, TAKE_ALL};
use super::{USED_DEVS, exec, retry, retry_or_ignore};
use crate::fs::walk_dir;
use crate::{blockdev, lvm};
use dkl::bootstrap::{Config, Filesystem, LvSize, LvmLV, LvmVG, TAKE_ALL};
pub async fn setup(cfg: &Config) {
if cfg.lvm.is_empty() {

View File

@ -3,12 +3,12 @@ use log::{info, warn};
use std::collections::BTreeSet as Set;
use tokio::process::Command;
use super::{format_err, retry_or_ignore, Config, Result};
use super::{Result, format_err, retry_or_ignore};
use crate::{
bootstrap::config,
udev,
utils::{select_n_by_regex, NameAliases},
utils::{NameAliases, select_n_by_regex},
};
use dkl::bootstrap::{Config, Network};
pub async fn setup(cfg: &Config) {
if cfg.networks.is_empty() {
@ -23,7 +23,7 @@ pub async fn setup(cfg: &Config) {
}
}
async fn setup_network(net: &config::Network, assigned: &mut Set<String>) -> Result<()> {
async fn setup_network(net: &Network, assigned: &mut Set<String>) -> Result<()> {
info!("setting up network {}", net.name);
let netdevs = get_interfaces()?

View File

@ -7,7 +7,7 @@ use tokio::net;
use tokio::process::Command;
use super::retry_or_ignore;
use crate::bootstrap::config::{Config, SSHServer};
use dkl::bootstrap::{Config, SSHServer};
pub async fn start(cfg: &Config) {
retry_or_ignore(async || {

View File

@ -1,61 +0,0 @@
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Config {
pub layers: Vec<String>,
pub root_user: RootUser,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mounts: Vec<Mount>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub files: Vec<File>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub groups: Vec<Group>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub users: Vec<User>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct RootUser {
#[serde(skip_serializing_if = "Option::is_none")]
pub password_hash: Option<String>,
pub authorized_keys: Vec<String>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Mount {
pub r#type: Option<String>,
pub dev: String,
pub path: String,
pub options: Option<String>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Group {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub gid: Option<u32>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct User {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub uid: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gid: Option<u32>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct File {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<u32>,
#[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),
}

View File

@ -3,7 +3,7 @@ use std::fmt::Display;
use std::sync::{Arc, LazyLock};
use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net;
use tokio::sync::{oneshot, watch, Mutex};
use tokio::sync::{Mutex, oneshot, watch};
pub async fn read_line(prompt: impl Display) -> String {
read(prompt, false).await

View File

@ -1,11 +1,9 @@
pub mod bootstrap;
pub mod blockdev;
pub mod cmd;
pub mod dklog;
pub mod fs;
pub mod input;
pub mod lsblk;
pub mod lvm;
pub mod udev;
pub mod utils;
pub mod input;
pub mod blockdev;
pub mod fs;
pub mod dkl;
pub mod dklog;

View File

@ -1,4 +1,4 @@
use eyre::{format_err, Result};
use eyre::{Result, format_err};
use tokio::process::Command;
#[derive(Debug, serde::Deserialize, serde::Serialize)]