Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01a0073e78 | |||
| ac9d7e8d9d | |||
| 148aa0cc44 | |||
| eb81cd3b5c | |||
| f892178d5d | |||
| cb62ac0ed8 | |||
| 0d9d087afd | |||
| e484802284 | |||
| 423a9c53e6 |
1831
Cargo.lock
generated
1831
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "init"
|
||||
version = "2.4.1"
|
||||
version = "2.5.1"
|
||||
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,10 @@ 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"] }
|
||||
glob = "0.3.3"
|
||||
|
||||
19
Dockerfile
19
Dockerfile
@ -1,15 +1,15 @@
|
||||
from rust:1.88.0-alpine as rust
|
||||
from rust:1.91.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
|
||||
from alpine:3.22.2 as initrd
|
||||
run apk add zstd lz4
|
||||
|
||||
workdir /system
|
||||
@ -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
|
||||
@ -39,6 +34,6 @@ run chroot . init-version
|
||||
run find * |cpio -H newc -oF /initrd
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
from alpine:3.22.0
|
||||
from alpine:3.22.2
|
||||
copy --from=initrd /initrd /
|
||||
entrypoint ["base64","/initrd"]
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ modd.conf {}
|
||||
prep: cargo test
|
||||
prep: cargo build
|
||||
prep: debug/init-version
|
||||
#prep: cargo run --bin test
|
||||
}
|
||||
|
||||
target/debug/init Dockerfile {
|
||||
|
||||
@ -1 +0,0 @@
|
||||
pub mod config;
|
||||
@ -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>,
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
pub mod bootstrap;
|
||||
pub mod connect_boot;
|
||||
pub mod init;
|
||||
pub mod init_input;
|
||||
pub mod version;
|
||||
|
||||
129
src/cmd/init.rs
129
src/cmd/init.rs
@ -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)");
|
||||
}
|
||||
@ -139,6 +106,7 @@ pub async fn run() {
|
||||
// Wireguard VPNs
|
||||
for (name, conf) in &cfg.vpns {
|
||||
retry_or_ignore(async || {
|
||||
info!("starting VPN {name}");
|
||||
let dir = "/etc/wireguard";
|
||||
fs::create_dir_all(dir).await?;
|
||||
|
||||
@ -180,6 +148,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 +173,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 +198,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
|
||||
|
||||
@ -1,25 +1,39 @@
|
||||
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::{fs::walk_dir, 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(())
|
||||
|
||||
// VPNs
|
||||
for vpn_conf in walk_dir("/bootstrap/vpns").await {
|
||||
if !vpn_conf.ends_with(".conf") {
|
||||
continue;
|
||||
}
|
||||
|
||||
retry_or_ignore(async || {
|
||||
info!("starting VPN from {vpn_conf}");
|
||||
try_exec("wg-quick", &["up", &vpn_conf]).await
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
// prepare system
|
||||
let boot_version = utils::param("version").unwrap_or("current");
|
||||
let base_dir = &format!("/bootstrap/{boot_version}");
|
||||
|
||||
@ -50,13 +64,11 @@ 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;
|
||||
|
||||
// TODO VPNs
|
||||
|
||||
mount_filesystems(&sys_cfg.mounts, "/system").await;
|
||||
|
||||
retry_or_ignore(async || {
|
||||
@ -84,42 +96,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 +151,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 +200,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 +240,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 +255,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"];
|
||||
|
||||
@ -8,9 +8,9 @@ use tokio::sync::Mutex;
|
||||
|
||||
use super::{retry_or_ignore, USED_DEVS};
|
||||
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() {
|
||||
@ -29,7 +29,7 @@ pub async fn setup(devs: &[CryptDev]) {
|
||||
let all_devs = walk_dir("/dev").await;
|
||||
|
||||
for dev in devs {
|
||||
let mut mappings = find_dev(dev, &all_devs);
|
||||
let mut mappings = find_dev(dev, &all_devs).await?;
|
||||
mappings.retain(|(_, dev_path)| !used_devs.contains(dev_path));
|
||||
|
||||
if mappings.is_empty() && !dev.optional() && !done.contains(&dev.name) {
|
||||
@ -56,27 +56,72 @@ pub async fn setup(devs: &[CryptDev]) {
|
||||
.await;
|
||||
}
|
||||
|
||||
static PREV_PW: Mutex<String> = Mutex::const_new(String::new());
|
||||
struct PrevPw {
|
||||
pw: String,
|
||||
reuse: bool,
|
||||
}
|
||||
|
||||
async fn crypt_open(crypt_dev: &str, dev_path: &str) -> Result<()> {
|
||||
'open_loop: loop {
|
||||
let mut prev_pw = PREV_PW.lock().await;
|
||||
let prompt = if prev_pw.is_empty() {
|
||||
format!("crypt password for {crypt_dev}? ")
|
||||
} else {
|
||||
format!("crypt password for {crypt_dev} (enter = reuse previous)? ")
|
||||
};
|
||||
|
||||
let mut pw = input::read_password(prompt).await;
|
||||
if pw.is_empty() {
|
||||
pw = prev_pw.clone();
|
||||
impl PrevPw {
|
||||
fn is_set(&self) -> bool {
|
||||
!self.pw.is_empty()
|
||||
}
|
||||
|
||||
fn can_reuse(&self) -> bool {
|
||||
self.reuse && self.is_set()
|
||||
}
|
||||
|
||||
fn invalidate(&mut self) {
|
||||
self.pw = String::new();
|
||||
self.reuse = false;
|
||||
}
|
||||
|
||||
async fn input(&mut self, prompt: impl std::fmt::Display) -> String {
|
||||
if self.can_reuse() {
|
||||
info!("reusing password");
|
||||
self.pw.clone()
|
||||
} else if self.is_set() {
|
||||
let pw =
|
||||
input::read_password(format!("{prompt} (\"\" reuse, \"*\" auto-reuse)? ")).await;
|
||||
|
||||
match pw.as_str() {
|
||||
"" => self.pw.clone(),
|
||||
"*" => {
|
||||
self.reuse = true;
|
||||
self.pw.clone()
|
||||
}
|
||||
_ => {
|
||||
self.pw = pw.clone();
|
||||
pw
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let pw = loop {
|
||||
let pw = input::read_password(format!("{prompt}? ")).await;
|
||||
if pw.is_empty() {
|
||||
error!("empty password provided!");
|
||||
continue;
|
||||
}
|
||||
break pw;
|
||||
};
|
||||
|
||||
*prev_pw = pw.clone();
|
||||
self.pw = pw.clone();
|
||||
pw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static PREV_PW: Mutex<PrevPw> = Mutex::const_new(PrevPw {
|
||||
pw: String::new(),
|
||||
reuse: false,
|
||||
});
|
||||
|
||||
async fn crypt_open(crypt_dev: &str, dev_path: &str) -> Result<()> {
|
||||
'open_loop: loop {
|
||||
let mut prev_pw = PREV_PW.lock().await;
|
||||
|
||||
let pw = prev_pw
|
||||
.input(format!("crypt password for {crypt_dev}"))
|
||||
.await;
|
||||
|
||||
if cryptsetup(&pw, ["open", dev_path, crypt_dev]).await? {
|
||||
return Ok(());
|
||||
@ -107,17 +152,19 @@ async fn crypt_open(crypt_dev: &str, dev_path: &str) -> Result<()> {
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
// device looks initialized, don't allow format
|
||||
warn!("{dev_path} looks initialized, formatting not allowed from init");
|
||||
|
||||
prev_pw.invalidate();
|
||||
|
||||
match input::read_choice(["[r]etry", "[i]gnore"]).await {
|
||||
'r' => continue 'open_loop,
|
||||
'i' => return Ok(()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cryptsetup<const N: usize>(pw: &str, args: [&str; N]) -> Result<bool> {
|
||||
@ -134,18 +181,39 @@ async fn cryptsetup<const N: usize>(pw: &str, args: [&str; N]) -> Result<bool> {
|
||||
Ok(child.wait().await?.success())
|
||||
}
|
||||
|
||||
fn find_dev(dev: &CryptDev, all_devs: &[String]) -> Vec<(String, String)> {
|
||||
async fn find_dev(dev: &CryptDev, all_devs: &[String]) -> Result<Vec<(String, String)>> {
|
||||
let dev_name = &dev.name;
|
||||
match dev.filter {
|
||||
DevFilter::Dev(ref path) => (all_devs.iter())
|
||||
Ok(match dev.filter() {
|
||||
DevFilter::None => vec![],
|
||||
DevFilter::Dev(path) => (all_devs.iter())
|
||||
.filter(|dev_path| dev_path == &path)
|
||||
.map(|dev_path| (dev.name.clone(), dev_path.clone()))
|
||||
.collect(),
|
||||
DevFilter::Prefix(ref prefix) => (all_devs.iter())
|
||||
DevFilter::Prefix(prefix) => (all_devs.iter())
|
||||
.filter_map(|path| {
|
||||
let suffix = path.strip_prefix(prefix)?;
|
||||
Some((format!("{dev_name}{suffix}"), path.clone()))
|
||||
})
|
||||
.collect(),
|
||||
DevFilter::Udev(filter) => {
|
||||
use crate::udev;
|
||||
let devs = udev::all().await?;
|
||||
|
||||
let filter: udev::Filter = filter.clone().into();
|
||||
|
||||
(devs.iter())
|
||||
.filter(|dev| dev.subsystem() == Some("block") && filter.matches(dev))
|
||||
.filter_map(|dev| {
|
||||
let path = dev.property("DEVNAME")?.to_string();
|
||||
|
||||
let mut name = dev_name.replace("${name}", dev.name()?);
|
||||
for (p, v) in dev.properties() {
|
||||
name = name.replace(&format!("${{{p}}}"), v);
|
||||
}
|
||||
|
||||
Some((name, path))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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::{format_err, retry_or_ignore, Result};
|
||||
use crate::{
|
||||
bootstrap::config,
|
||||
udev,
|
||||
utils::{select_n_by_regex, NameAliases},
|
||||
utils::{select_n_by_regex, select_n_by_udev, NameAliases},
|
||||
};
|
||||
use dkl::bootstrap::{Config, Network};
|
||||
|
||||
pub async fn setup(cfg: &Config) {
|
||||
if cfg.networks.is_empty() {
|
||||
@ -23,19 +23,16 @@ 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()?
|
||||
let netdevs = (get_interfaces().await?)
|
||||
.filter(|dev| !assigned.contains(dev.name()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for dev in &netdevs {
|
||||
info!(
|
||||
"- available network device: {}, aliases [{}]",
|
||||
dev.name(),
|
||||
dev.aliases().join(", ")
|
||||
);
|
||||
let names = [dev.name()].into_iter().chain(dev.aliases()).join(", ");
|
||||
info!("- available network device: {}", names);
|
||||
}
|
||||
|
||||
let mut cmd = Command::new("ash");
|
||||
@ -47,8 +44,19 @@ async fn setup_network(net: &config::Network, assigned: &mut Set<String>) -> Res
|
||||
for iface in &net.interfaces {
|
||||
let var = &iface.var;
|
||||
|
||||
let if_names = if let Some(ref udev_filter) = iface.udev {
|
||||
select_n_by_udev(
|
||||
iface.n,
|
||||
"net",
|
||||
"INTERFACE",
|
||||
&udev_filter.clone().into(),
|
||||
&assigned,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
let netdevs = netdevs.iter().filter(|na| !assigned.contains(na.name()));
|
||||
let if_names = select_n_by_regex(iface.n, &iface.regexps, netdevs);
|
||||
select_n_by_regex(iface.n, &iface.regexps, netdevs)
|
||||
};
|
||||
|
||||
if if_names.is_empty() {
|
||||
return Err(format_err!("- no interface match for {var:?}"));
|
||||
@ -71,24 +79,20 @@ async fn setup_network(net: &config::Network, assigned: &mut Set<String>) -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_interfaces() -> Result<impl Iterator<Item = NameAliases>> {
|
||||
Ok(udev::get_devices("net")?.into_iter().map(|dev| {
|
||||
let mut na = NameAliases::new(dev.sysname().to_string());
|
||||
async fn get_interfaces() -> Result<impl Iterator<Item = NameAliases>> {
|
||||
let nas: Vec<_> = (udev::all().await?.of_subsystem("net"))
|
||||
.filter_map(|dev| {
|
||||
let name = dev.property("INTERFACE")?;
|
||||
let mut na = NameAliases::new(name.to_string());
|
||||
|
||||
for (property, value) in dev.properties() {
|
||||
if [
|
||||
"INTERFACE",
|
||||
"ID_NET_NAME",
|
||||
"ID_NET_NAME_PATH",
|
||||
"ID_NET_NAME_MAC",
|
||||
"ID_NET_NAME_SLOT",
|
||||
]
|
||||
.contains(&property)
|
||||
{
|
||||
na.push(value.to_string());
|
||||
for (p, v) in dev.properties() {
|
||||
if p.starts_with("ID_NET_NAME") {
|
||||
na.push(v.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
na
|
||||
}))
|
||||
Some(na)
|
||||
})
|
||||
.collect();
|
||||
Ok(nas.into_iter())
|
||||
}
|
||||
|
||||
@ -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 || {
|
||||
|
||||
@ -2,6 +2,10 @@ use crate::input;
|
||||
|
||||
pub async fn run() {
|
||||
tokio::spawn(async {
|
||||
// give a bit of time for stdout
|
||||
use tokio::time::{sleep, Duration};
|
||||
sleep(Duration::from_millis(200)).await;
|
||||
|
||||
if let Err(e) = input::forward_requests_from_socket().await {
|
||||
eprintln!("failed to forwards requests from socket: {e}");
|
||||
std::process::exit(1);
|
||||
|
||||
61
src/dkl.rs
61
src/dkl.rs
@ -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),
|
||||
}
|
||||
@ -37,7 +37,7 @@ impl Log {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> LogWatch {
|
||||
pub fn subscribe(&self) -> LogWatch<'_> {
|
||||
LogWatch {
|
||||
log: self,
|
||||
pos: 0,
|
||||
|
||||
@ -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
|
||||
|
||||
10
src/lib.rs
10
src/lib.rs
@ -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;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use eyre::{format_err, Result};
|
||||
use eyre::{Result, format_err};
|
||||
use tokio::process::Command;
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
|
||||
145
src/udev.rs
145
src/udev.rs
@ -63,3 +63,148 @@ pub fn get_devices(class: &str) -> Result<Vec<Device>> {
|
||||
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
pub async fn all() -> Result<Devs> {
|
||||
let output = tokio::process::Command::new("udevadm")
|
||||
.args(["info", "-e"])
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(eyre::format_err!("udevadm failed: {}", output.status));
|
||||
}
|
||||
|
||||
Ok(Devs {
|
||||
data: unsafe { String::from_utf8_unchecked(output.stdout) },
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn by_path(path: &str) -> Result<Devs> {
|
||||
let output = tokio::process::Command::new("udevadm")
|
||||
.args(["info", path])
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(eyre::format_err!("udevadm failed: {}", output.status));
|
||||
}
|
||||
|
||||
Ok(Devs {
|
||||
data: unsafe { String::from_utf8_unchecked(output.stdout) },
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Devs {
|
||||
data: String,
|
||||
}
|
||||
|
||||
impl<'t> Devs {
|
||||
pub fn iter(&'t self) -> impl Iterator<Item = Dev<'t>> {
|
||||
self.data
|
||||
.split("\n\n")
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| Dev(s))
|
||||
}
|
||||
|
||||
pub fn of_subsystem(&'t self, subsystem: &str) -> impl Iterator<Item = Dev<'t>> {
|
||||
self.iter().filter(|dev| dev.subsystem() == Some(subsystem))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Dev<'t>(&'t str);
|
||||
|
||||
impl<'t> Dev<'t> {
|
||||
pub fn raw(&self) -> &str {
|
||||
self.0
|
||||
}
|
||||
|
||||
// alpine's udev prefixes we've seen:
|
||||
// - P: Device path in /sys/
|
||||
// - N: Kernel device node name
|
||||
// - S: Device node symlink
|
||||
// - L: Device node symlink priority [ignored]
|
||||
// - E: Device property
|
||||
|
||||
fn by_prefix(&self, prefix: &'static str) -> impl Iterator<Item = &str> {
|
||||
self.0.lines().filter_map(move |l| l.strip_prefix(prefix))
|
||||
}
|
||||
|
||||
/// Device path in /sys/
|
||||
pub fn path(&self) -> Option<&str> {
|
||||
self.by_prefix("P: ").next()
|
||||
}
|
||||
|
||||
/// Kernel device node name
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
self.by_prefix("N: ").next()
|
||||
}
|
||||
|
||||
/// Device node symlinks
|
||||
pub fn symlinks(&self) -> Vec<&str> {
|
||||
self.by_prefix("S: ").collect()
|
||||
}
|
||||
|
||||
/// Device properties
|
||||
pub fn properties(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||
self.by_prefix("E: ").filter_map(|s| s.split_once("="))
|
||||
}
|
||||
|
||||
/// Device property
|
||||
pub fn property(&self, name: &str) -> Option<&str> {
|
||||
self.properties()
|
||||
.filter_map(|(n, v)| (n == name).then_some(v))
|
||||
.next()
|
||||
}
|
||||
|
||||
/// Device subsystem
|
||||
pub fn subsystem(&self) -> Option<&str> {
|
||||
self.property("SUBSYSTEM")
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Filter {
|
||||
Has(String),
|
||||
Eq(String, String),
|
||||
Glob(String, glob::Pattern),
|
||||
And(Vec<Filter>),
|
||||
Or(Vec<Filter>),
|
||||
Not(Box<Filter>),
|
||||
False,
|
||||
}
|
||||
|
||||
impl<'t> Filter {
|
||||
pub fn matches(&self, dev: &Dev) -> bool {
|
||||
match self {
|
||||
Self::False => false,
|
||||
Self::Has(k) => dev.property(k).is_some(),
|
||||
Self::Eq(k, v) => dev.properties().any(|kv| kv == (k, v)),
|
||||
Self::Glob(k, pattern) => dev
|
||||
.properties()
|
||||
.any(|(pk, pv)| pk == k && pattern.matches(pv)),
|
||||
Self::And(ops) => ops.iter().all(|op| op.matches(dev)),
|
||||
Self::Or(ops) => ops.iter().any(|op| op.matches(dev)),
|
||||
Self::Not(op) => !op.matches(dev),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'t> Into<Filter> for dkl::bootstrap::UdevFilter {
|
||||
fn into(self) -> Filter {
|
||||
match self {
|
||||
Self::Has(p) => Filter::Has(p),
|
||||
Self::Eq(p, v) => Filter::Eq(p, v),
|
||||
Self::Glob(p, pattern) => match glob::Pattern::new(&pattern) {
|
||||
Ok(pattern) => Filter::Glob(p, pattern),
|
||||
Err(e) => {
|
||||
warn!("pattern {pattern:?} will never match: {e}");
|
||||
Filter::False
|
||||
}
|
||||
},
|
||||
Self::And(ops) => Filter::And(ops.into_iter().map(Self::into).collect()),
|
||||
Self::Or(ops) => Filter::Or(ops.into_iter().map(Self::into).collect()),
|
||||
Self::Not(op) => Filter::Not(Box::new((*op).into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
src/utils.rs
23
src/utils.rs
@ -2,6 +2,8 @@ use log::error;
|
||||
use std::collections::BTreeSet as Set;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::udev;
|
||||
|
||||
static CMDLINE: LazyLock<String> = LazyLock::new(|| {
|
||||
std::fs::read("/proc/cmdline")
|
||||
.inspect_err(|e| error!("failed to read kernel cmdline: {e}"))
|
||||
@ -88,3 +90,24 @@ pub fn select_n_by_regex<'t>(
|
||||
nas.take(n as usize).collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn select_n_by_udev<'t>(
|
||||
n: i16,
|
||||
subsystem: &str,
|
||||
result_property: &str,
|
||||
filter: &udev::Filter,
|
||||
in_use: &Set<String>,
|
||||
) -> eyre::Result<Vec<String>> {
|
||||
let devs = udev::all().await?;
|
||||
let nas = devs
|
||||
.of_subsystem(subsystem)
|
||||
.filter(|dev| filter.matches(dev))
|
||||
.filter_map(|dev| Some(dev.property(result_property)?.to_string()))
|
||||
.filter(|name| !in_use.contains(name));
|
||||
|
||||
Ok(if n == -1 {
|
||||
nas.collect()
|
||||
} else {
|
||||
nas.take(n as usize).collect()
|
||||
})
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ ssh:
|
||||
|
||||
networks:
|
||||
- name: loopback
|
||||
interfaces: [ { var: iface, n: 1, regexps: [ "^lo$" ] } ]
|
||||
interfaces: [ { var: iface, n: 1, udev: !eq [INTERFACE, lo] } ]
|
||||
script: |
|
||||
ip a add 127.0.0.1/8 dev lo
|
||||
ip a add ::1/128 dev lo
|
||||
@ -38,28 +38,22 @@ networks:
|
||||
interfaces:
|
||||
- var: iface
|
||||
n: 1
|
||||
regexps:
|
||||
- eth.*
|
||||
- veth.*
|
||||
- eno.*
|
||||
- enp.*
|
||||
udev: !has ID_NET_NAME_MAC
|
||||
script: |
|
||||
ip li set $iface up
|
||||
udhcpc -i $iface -b -t1 -T1 -A5 ||
|
||||
ip a add 2001:41d0:306:168f::1337:2eed/64 dev $iface
|
||||
|
||||
pre_lvm_crypt:
|
||||
- dev: /dev/vda
|
||||
name: sys0
|
||||
- dev: /dev/vdb
|
||||
name: sys1
|
||||
- name: sys-${name}
|
||||
udev: !glob [ DEVNAME, /dev/vd* ]
|
||||
|
||||
lvm:
|
||||
- vg: storage
|
||||
pvs:
|
||||
n: 2
|
||||
regexps:
|
||||
- /dev/mapper/sys[01]
|
||||
- ^/dev/mapper/sys-
|
||||
# to match full disks
|
||||
#- /dev/nvme[0-9]+n[0-9]+
|
||||
#- /dev/vd[a-z]+
|
||||
|
||||
Reference in New Issue
Block a user