use eyre::{format_err, Result}; use log::info; use std::path::Path; use tokio::{ fs, io::{AsyncBufReadExt, BufReader}, }; use super::{exec, mount, retry, retry_or_ignore, try_exec}; use crate::bootstrap::config::Config; use crate::{dkl, utils}; pub async fn bootstrap(cfg: Config) { let bs = cfg.bootstrap; retry_or_ignore(async || { fs::create_dir_all("/boostrap").await?; mount(Some(&bs.dev), "/bootstrap", Some("ext4"), None).await; Ok(()) }) .await; let boot_version = utils::param("version", "current"); let base_dir = &format!("/bootstrap/{boot_version}"); retry_or_ignore(async || { if !fs::try_exists(&base_dir).await? { info!("creating {base_dir}"); fs::create_dir_all(&base_dir).await? } Ok(()) }) .await; let sys_cfg: dkl::Config = retry(async || { let sys_cfg_bytes = seed_config(base_dir, &bs.seed).await?; Ok(serde_yaml::from_slice(&sys_cfg_bytes)?) }) .await; mount_system(&sys_cfg, base_dir).await; retry_or_ignore(async || { let path = "/etc/resolv.conf"; if fs::try_exists(path).await? { info!("cp /etc/resolv.conf"); fs::copy(path, &format!("/system{path}")).await?; } Ok(()) }) .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 || { info!("setting up root user"); setup_root_user(&sys_cfg.root_user, "/system").await }) .await; } async fn seed_config(base_dir: &str, seed_url: &Option) -> Result> { let cfg_path = &format!("{base_dir}/config.yaml"); if fs::try_exists(cfg_path).await? { return Ok(fs::read(cfg_path).await?); } let bs_tar = "/bootstrap.tar"; if !fs::try_exists(bs_tar).await? { if let Some(seed_url) = seed_url.as_ref() { fetch_bootstrap(seed_url, bs_tar).await?; } else { return Err(format_err!( "no {cfg_path}, no {bs_tar} and no seed, can't bootstrap" )); } } try_exec("tar", &["xf", bs_tar, "-C", base_dir]).await?; if !fs::try_exists(cfg_path).await? { return Err(format_err!("{cfg_path} does not exist after seeding")); } Ok(fs::read(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?; fs::rename(tmp_file, output_file) .await .map_err(|e| format_err!("seed rename failed: {e}"))?; Ok(()) } async fn mount_system(cfg: &dkl::Config, bs_dir: &str) { let mem_dir = "/mem"; mount(None, mem_dir, Some("tmpfs"), Some("size=512m")).await; let layers_dir = &format!("{mem_dir}/layers"); let mut lower_dir = String::new(); for layer in &cfg.layers { let src = if layer == "modules" { "/modules.sqfs" } else { &format!("{bs_dir}/{layer}.fs") }; let tgt = &format!("{mem_dir}/{layer}.fs"); retry(async || { info!("copying layer {layer} from {src}"); fs::copy(src, tgt).await?; Ok(()) }) .await; let layer_dir = &format!("{layers_dir}/{layer}"); mount(Some(tgt), layer_dir, Some("squashfs"), None).await; if !lower_dir.is_empty() { lower_dir.push(':'); } lower_dir.push_str(&layer_dir); } let upper_dir = &format!("{mem_dir}/upper"); let work_dir = &format!("{mem_dir}/work"); retry_or_ignore(async || { fs::create_dir_all(upper_dir).await?; fs::create_dir_all(work_dir).await?; Ok(()) }) .await; mount( None, "/system", Some("overlay"), Some(&format!( "lowerdir={lower_dir},upperdir={upper_dir},workdir={work_dir}" )), ) .await; // make root rshared (default in systemd, required by Kubernetes 1.10+) // equivalent to "mount --make-rshared /" // see kernel's Documentation/sharedsubtree.txt (search rshared) retry_or_ignore(async || { use nix::mount::MsFlags as M; const NONE: Option<&str> = None; nix::mount::mount(NONE, "/system", NONE, M::MS_SHARED | M::MS_REC, NONE)?; Ok(()) }) .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, 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(()) } async fn apply_groups(groups: &[dkl::Group], root: &str) { for group in groups { let mut args = vec![root, "groupadd", "-r"]; let gid = group.gid.map(|s| s.to_string()); if let Some(gid) = gid.as_ref() { args.extend(&["-g", gid]); } args.push(group.name.as_str()); exec("chroot", &args).await; } } async fn apply_users(users: &[dkl::User], root: &str) { for user in users { let mut args = vec![root, "useradd", "-r"]; let uid = user.uid.map(|s| s.to_string()); if let Some(uid) = uid.as_ref() { args.extend(&["-u", uid]); } let gid = user.gid.map(|s| s.to_string()); if let Some(gid) = gid.as_ref() { args.extend(&["-g", gid]); } args.push(user.name.as_str()); exec("chroot", &args).await; } } async fn mount_filesystems(mounts: &[dkl::Mount], root: &str) { for m in mounts { let path = chroot(root, &m.path); mount( Some(&m.dev), &path, m.r#type.as_ref().map(|v| v.as_str()), m.options .as_ref() .filter(|v| !v.is_empty()) .map(|s| s.as_str()), ) .await; } } async fn setup_root_user(user: &dkl::RootUser, root: &str) -> Result<()> { if let Some(pw_hash) = user.password_hash.as_ref().filter(|v| !v.is_empty()) { set_user_password("root", &pw_hash, root).await?; } let mut authorized_keys = Vec::new(); for ak in &user.authorized_keys { authorized_keys.extend(ak.as_bytes()); authorized_keys.push(b'\n'); } let ssh_dir = &chroot(root, "root/.ssh"); fs::create_dir_all(ssh_dir) .await .map_err(|e| format_err!("mkdir -p {ssh_dir} failed: {e}"))?; set_perms(ssh_dir, Some(0o700)) .await .map_err(|e| format_err!("chmod {ssh_dir} failed: {e}"))?; let ak_path = &format!("{ssh_dir}/authorized_keys"); fs::write(ak_path, authorized_keys) .await .map_err(|e| format_err!("write {ak_path} failed: {e}"))?; Ok(()) } async fn set_user_password(user: &str, password_hash: &str, root: &str) -> Result<()> { info!("setting password for {user}"); let user = user.as_bytes(); let password_hash = password_hash.as_bytes(); let mut buf = Vec::new(); let pw_file = &chroot(root, "etc/shadow"); let rd = fs::File::open(pw_file) .await .map_err(|e| format_err!("open {pw_file} failed: {e}"))?; let mut rd = BufReader::new(rd); let mut line = Vec::new(); while (rd.read_until(b'\n', &mut line).await) .map_err(|e| format_err!("read {pw_file} failed: {e}"))? != 0 { let mut split: Vec<_> = line.split(|c| *c == b':').collect(); if split.len() > 2 && split[0] == user { split[1] = password_hash; buf.extend(split.join(&b':')); } else { buf.extend(&line); } line.clear(); } fs::write(pw_file, buf).await?; Ok(()) }