From 19ebfe98f20ba9fa2a93f5b473a03458510ec615 Mon Sep 17 00:00:00 2001 From: Omer Tuchfeld Date: Sat, 25 Jan 2025 14:20:22 +0100 Subject: [PATCH] Add `bootc-reinstall` binary # Background The current usage instructions for bootc involve a long podman invocation. # Issue It's hard to remember and type the long podman invocation, making the usage of bootc difficult for users. See https://issues.redhat.com/browse/BIFROST-610 and https://issues.redhat.com/browse/BIFROST-611 (Epic https://issues.redhat.com/browse/BIFROST-594) # Solution We want to make the usage of bootc easier by providing a new Fedora/RHEL subpackage that includes a new binary `bootc-reinstall`. This binary will simplify the usage of bootc by providing a simple command line interface (configured either through CLI flags or a configuration file) with an interactive prompt that allows users to reinstall the current system using bootc. The commandline will handle helping the user choose SSH keys / users and ensure and warn the user about the destructive nature of the operation, and eventually issues they might run into in the various clouds (e.g. missing cloud agent on the target image) # Implementation Added new reinstall-cli crate that outputs the new bootc-reinstall binary. Modified the bootc.spec file to generate the new subpackage which includes this binary. This new crate depends on the existing utils crate. Refactored the tracing initialization from the bootc binary into the utils crate so that it can be reused by the new crate. The new CLI can either be configured through commandline flags or through a configuration file in a path set by the environment variable `BOOTC_REINSTALL_CONFIG`. The configuration file is a YAML file. # Limitations Only root SSH keys are supported. The multi user selection TUI is implemented, but if you choose anything other than root you will get an error. # Try Try out instructions: ```bash # Make srpm cargo xtask package-srpm # Mock group sudo usermod -a -G mock $(whoami) newgrp mock # Build RPM for RHEL mock --rebuild -r rhel+epel-9-x86_64 --rebuild target/bootc-*.src.rpm ``` Then install the RPM (`/var/lib/mock/rhel+epel-9-x86_64/result/bootc-reinstall-2*.el9.x86_64.rpm`) on [a rhel9 gcp vm](https://console.cloud.google.com/compute/instanceTemplates/details/rhel9-dev-1?project=bifrost-devel&authuser=1&inv=1&invt=Abn-jg) instance template # TODO Missing docs, missing functionality. Everything is in alpha stage. User choice / SSH keys / prompt disabling should also eventually be supported to be configured through commandline arguments or the configuration file. --- Cargo.lock | 78 +++++++++++++++++++++-- Cargo.toml | 2 + Makefile | 1 + cli/Cargo.toml | 9 +-- cli/src/main.rs | 19 ++---- contrib/packaging/bootc.spec | 16 +++++ lib/src/cli.rs | 1 + reinstall-cli/Cargo.toml | 31 +++++++++ reinstall-cli/sample_config.yaml | 2 + reinstall-cli/src/config/cli.rs | 7 ++ reinstall-cli/src/config/mod.rs | 72 +++++++++++++++++++++ reinstall-cli/src/main.rs | 36 +++++++++++ reinstall-cli/src/podman.rs | 59 +++++++++++++++++ reinstall-cli/src/prompt.rs | 91 ++++++++++++++++++++++++++ reinstall-cli/src/users.rs | 106 +++++++++++++++++++++++++++++++ utils/Cargo.toml | 1 + utils/src/lib.rs | 2 + utils/src/tracing_util.rs | 18 ++++++ 18 files changed, 525 insertions(+), 26 deletions(-) create mode 100644 reinstall-cli/Cargo.toml create mode 100644 reinstall-cli/sample_config.yaml create mode 100644 reinstall-cli/src/config/cli.rs create mode 100644 reinstall-cli/src/config/mod.rs create mode 100644 reinstall-cli/src/main.rs create mode 100644 reinstall-cli/src/podman.rs create mode 100644 reinstall-cli/src/prompt.rs create mode 100644 reinstall-cli/src/users.rs create mode 100644 utils/src/tracing_util.rs diff --git a/Cargo.lock b/Cargo.lock index a9cc578e7..a711acf6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,11 +156,10 @@ version = "0.1.9" dependencies = [ "anyhow", "bootc-lib", - "clap", + "bootc-utils", "log", "tokio", "tracing", - "tracing-subscriber", ] [[package]] @@ -222,6 +221,23 @@ dependencies = [ "xshell", ] +[[package]] +name = "bootc-reinstall" +version = "0.1.9" +dependencies = [ + "anyhow", + "bootc-utils", + "clap", + "dialoguer", + "itertools", + "log", + "rustix", + "serde_json", + "serde_yaml", + "tracing", + "uzers", +] + [[package]] name = "bootc-utils" version = "0.0.0" @@ -235,6 +251,7 @@ dependencies = [ "tempfile", "tokio", "tracing", + "tracing-subscriber", ] [[package]] @@ -600,6 +617,19 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror 1.0.69", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -617,6 +647,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -646,7 +682,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1080,6 +1116,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.14" @@ -1802,7 +1847,7 @@ dependencies = [ "libc", "linux-raw-sys", "once_cell", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1900,6 +1945,7 @@ version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" dependencies = [ + "indexmap", "itoa", "memchr", "ryu", @@ -1948,6 +1994,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -2107,7 +2159,7 @@ dependencies = [ "fastrand", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2426,6 +2478,16 @@ dependencies = [ "serde", ] +[[package]] +name = "uzers" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df81ff504e7d82ad53e95ed1ad5b72103c11253f39238bcc0235b90768a97dd" +dependencies = [ + "libc", + "log", +] + [[package]] name = "valuable" version = "0.1.0" @@ -2722,6 +2784,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zstd" version = "0.13.2" diff --git a/Cargo.toml b/Cargo.toml index b0d965d69..9d557b3c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "cli", + "reinstall-cli", "lib", "ostree-ext", "utils", @@ -60,6 +61,7 @@ tempfile = "3.10.1" tracing = "0.1.40" tokio = ">= 1.37.0" tokio-util = { features = ["io-util"], version = "0.7.10" } +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } # See https://github.com/coreos/cargo-vendor-filterer [workspace.metadata.vendor-filter] diff --git a/Makefile b/Makefile index 7a940cac7..70bcf481f 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ all: install: install -D -m 0755 -t $(DESTDIR)$(prefix)/bin target/release/bootc + install -D -m 0755 -t $(DESTDIR)$(prefix)/bin target/release/bootc-reinstall install -d -m 0755 $(DESTDIR)$(prefix)/lib/bootc/bound-images.d install -d -m 0755 $(DESTDIR)$(prefix)/lib/bootc/kargs.d ln -s /sysroot/ostree/bootc/storage $(DESTDIR)$(prefix)/lib/bootc/storage diff --git a/cli/Cargo.toml b/cli/Cargo.toml index d9ae82b93..74cc9acca 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -12,19 +12,16 @@ default-run = "bootc" # See https://github.com/coreos/cargo-vendor-filterer [package.metadata.vendor-filter] -# This list of platforms is not intended to be exclusive, feel free -# to extend it. But missing a platform will only matter for the case where -# a dependent crate is *only* built on that platform. -platforms = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", "powerpc64le-unknown-linux-gnu", "s390x-unknown-linux-gnu", "riscv64gc-unknown-linux-gnu"] +# For now we only care about tier 1+2 Linux. (In practice, it's unlikely there is a tier3-only Linux dependency) +platforms = ["*-unknown-linux-gnu"] [dependencies] anyhow = { workspace = true } bootc-lib = { version = "1.0", path = "../lib" } -clap = { workspace = true } +bootc-utils = { path = "../utils" } tokio = { workspace = true, features = ["macros"] } log = "0.4.21" tracing = { workspace = true } -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } [lints] workspace = true diff --git a/cli/src/main.rs b/cli/src/main.rs index 8407c41c4..d9429aa33 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,25 +1,14 @@ //! The main entrypoint for bootc, which just performs global initialization, and then //! calls out into the library. - use anyhow::Result; /// The code called after we've done process global init and created /// an async runtime. async fn async_main() -> Result<()> { - // Don't include timestamps and such because they're not really useful and - // too verbose, and plus several log targets such as journald will already - // include timestamps. - let format = tracing_subscriber::fmt::format() - .without_time() - .with_target(false) - .compact(); - // Log to stderr by default - tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .event_format(format) - .with_writer(std::io::stderr) - .init(); - tracing::trace!("starting"); + bootc_utils::initialize_tracing(); + + tracing::trace!("starting bootc"); + // As you can see, the role of this file is mostly to just be a shim // to call into the code that lives in the internal shared library. bootc_lib::cli::run_from_iter(std::env::args()).await diff --git a/contrib/packaging/bootc.spec b/contrib/packaging/bootc.spec index 412370bd4..2946a17c4 100644 --- a/contrib/packaging/bootc.spec +++ b/contrib/packaging/bootc.spec @@ -62,17 +62,30 @@ Provides: ostree-cli(ostree-container) %description %{summary} +%package reinstall +Summary: Utility to reinstall the current system using bootc +Requires: podman +# The reinstall subpackage intentionally does not require bootc, as it pulls in many unnecessary dependencies + +%description reinstall +This package provides a utility to simplify reinstalling the current system to a given bootc image. + %prep %autosetup -p1 -a1 %cargo_prep -v vendor %build +# Build the main bootc binary %if 0%{?fedora} || 0%{?rhel} >= 10 %cargo_build %{?with_rhsm:-f rhsm} %else %cargo_build %{?with_rhsm:--features rhsm} %endif +# Build the bootc-reinstall binary +%global cargo_args -p bootc-reinstall +%cargo_build + %cargo_vendor_manifest %cargo_license_summary %{cargo_license} > LICENSE.dependencies @@ -104,5 +117,8 @@ make install-ostree-hooks DESTDIR=%{?buildroot} %{_docdir}/bootc/* %{_mandir}/man*/bootc* +%files reinstall +%{_bindir}/bootc-reinstall + %changelog %autochangelog diff --git a/lib/src/cli.rs b/lib/src/cli.rs index 70b6d91b9..6e9775a30 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -616,6 +616,7 @@ pub(crate) async fn get_storage() -> Result { crate::store::Storage::new(sysroot, &global_run) } +/// Ensure that the current process is running as root and has CAP_SYS_ADMIN. #[context("Querying root privilege")] pub(crate) fn require_root(is_container: bool) -> Result<()> { ensure!( diff --git a/reinstall-cli/Cargo.toml b/reinstall-cli/Cargo.toml new file mode 100644 index 000000000..3888b89a8 --- /dev/null +++ b/reinstall-cli/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "bootc-reinstall" +version = "0.1.9" +edition = "2021" +license = "MIT OR Apache-2.0" +repository = "https://github.com/containers/bootc" +readme = "README.md" +publish = false +# For now don't bump this above what is currently shipped in RHEL9. +rust-version = "1.75.0" + +# See https://github.com/coreos/cargo-vendor-filterer +[package.metadata.vendor-filter] +# For now we only care about tier 1+2 Linux. (In practice, it's unlikely there is a tier3-only Linux dependency) +platforms = ["*-unknown-linux-gnu"] + +[dependencies] +anyhow = { workspace = true } +bootc-utils = { path = "../utils" } +clap = { workspace = true, features = ["derive"] } +dialoguer = "0.11.0" +log = "0.4.21" +rustix = { workspace = true } +serde_json = { version = "1.0.93", features = ["preserve_order"] } +serde_yaml = "0.9.22" +tracing = { workspace = true } +uzers = "0.12.1" +itertools = "0.14.0" + +[lints] +workspace = true diff --git a/reinstall-cli/sample_config.yaml b/reinstall-cli/sample_config.yaml new file mode 100644 index 000000000..1d40e88ff --- /dev/null +++ b/reinstall-cli/sample_config.yaml @@ -0,0 +1,2 @@ +# The bootc container image to install +bootc_image: quay.io/fedora/fedora-bootc:41 diff --git a/reinstall-cli/src/config/cli.rs b/reinstall-cli/src/config/cli.rs new file mode 100644 index 000000000..7d7524881 --- /dev/null +++ b/reinstall-cli/src/config/cli.rs @@ -0,0 +1,7 @@ +use clap::Parser; + +#[derive(Parser)] +pub(crate) struct Cli { + /// The bootc container image to install, e.g. quay.io/fedora/fedora-bootc:41 + pub(crate) bootc_image: String, +} diff --git a/reinstall-cli/src/config/mod.rs b/reinstall-cli/src/config/mod.rs new file mode 100644 index 000000000..9982eca8c --- /dev/null +++ b/reinstall-cli/src/config/mod.rs @@ -0,0 +1,72 @@ +use anyhow::{bail, ensure, Context, Result}; +use clap::Parser; +use itertools::Itertools; +use serde_json::Value; + +mod cli; + +#[derive(Debug)] +pub(crate) struct ReinstallConfig { + pub(crate) bootc_image: String, +} + +impl ReinstallConfig { + pub fn parse_from_cli(cli: cli::Cli) -> Result { + Ok(Self { + bootc_image: cli.bootc_image, + }) + } + + pub fn parse_from_config_file(config_bytes: &[u8]) -> Result { + let value: Value = serde_yaml::from_slice(config_bytes)?; + + let mut value = value + .as_object() + .context("config file must be a YAML object")? + .clone(); + + let bootc_image = match value.remove("bootc_image") { + Some(value) => value + .as_str() + .context("bootc_image must be a string")? + .to_string(), + None => bail!("bootc_image is required"), + }; + + ensure!( + value.is_empty(), + "unknown keys {:?} in config file", + value.keys().map(|key| key.to_string()).join(", ") + ); + + let reinstall_config = Self { bootc_image }; + + Ok(reinstall_config) + } + + pub fn load() -> Result { + Ok(match std::env::var("BOOTC_REINSTALL_CONFIG") { + Ok(var) => { + ensure_no_cli_args()?; + + ReinstallConfig::parse_from_config_file( + &std::fs::read(&var) + .context(format!("reading BOOTC_REINSTALL_CONFIG file {}", var))?, + ) + .context(format!("parsing BOOTC_REINSTALL_CONFIG file {}", var))? + } + Err(_) => ReinstallConfig::parse_from_cli(cli::Cli::parse()).context("CLI parsing")?, + }) + } +} + +fn ensure_no_cli_args() -> Result<()> { + let num_args = std::env::args().len(); + + ensure!( + num_args == 1, + "BOOTC_REINSTALL_CONFIG is set, but there are {num_args} CLI arguments. BOOTC_REINSTALL_CONFIG is meant to be used with no arguments." + ); + + Ok(()) +} diff --git a/reinstall-cli/src/main.rs b/reinstall-cli/src/main.rs new file mode 100644 index 000000000..cd229efc7 --- /dev/null +++ b/reinstall-cli/src/main.rs @@ -0,0 +1,36 @@ +//! The main entrypoint for bootc-reinstall + +use anyhow::{ensure, Context, Result}; +use rustix::process::getuid; + +mod config; +mod podman; +mod prompt; +pub(crate) mod users; + +const ROOT_KEY_MOUNT_POINT: &str = "/bootc_authorized_ssh_keys/root"; + +fn run() -> Result<()> { + bootc_utils::initialize_tracing(); + + let config = config::ReinstallConfig::load().context("loading config")?; + + tracing::trace!("starting bootc-reinstall"); + // Rootless podman is not supported by bootc + ensure!(getuid().is_root(), "Must run as the root user"); + let all_args = podman::command(&config.bootc_image, prompt::get_root_key()?); + println!("Going to run command: {}", all_args.join(" ")); + prompt::temporary_developer_protection_prompt()?; + podman::run_podman(all_args)?; + Ok(()) +} + +fn main() { + // In order to print the error in a custom format (with :#) our + // main simply invokes a run() where all the work is done. + // This code just captures any errors. + if let Err(e) = run() { + tracing::error!("{:#}", e); + std::process::exit(1); + } +} diff --git a/reinstall-cli/src/podman.rs b/reinstall-cli/src/podman.rs new file mode 100644 index 000000000..1de5541cc --- /dev/null +++ b/reinstall-cli/src/podman.rs @@ -0,0 +1,59 @@ +use super::ROOT_KEY_MOUNT_POINT; +use crate::users::UserKeys; +use anyhow::Context; +use std::process::Command; + +pub(crate) fn command(image: &str, root_key: Option) -> Vec { + let mut podman_command_and_args = vec![ + // We use podman to run the bootc container. This might change in the future to remove the + // podman dependency. + "podman", + "run", + // The container needs to be privileged, as it heavily modifies the host + "--privileged", + // The container needs to access the host's PID namespace to mount host directories + "--pid=host", + // Since https://github.com/containers/bootc/pull/919 this mount should not be needed, but + // some reason with e.g. quay.io/fedora/fedora-bootc:41 it is still needed. + "-v", + "/var/lib/containers:/var/lib/containers", + ]; + + let mut bootc_command_and_args = vec![ + "bootc", + "install", + // We're replacing the current root + "to-existing-root", + // The user already knows they're reinstalling their machine, that's the entire purpose of + // this binary. Since this is no longer an "arcane" bootc command, we can safely avoid this + // timed warning prompt. TODO: Discuss in https://github.com/containers/bootc/discussions/1060 + "--acknowledge-destructive", + ]; + + if let Some(root_key) = root_key.as_ref() { + podman_command_and_args.push("-v"); + podman_command_and_args.push(&root_key.authorized_keys_path); + podman_command_and_args.push(ROOT_KEY_MOUNT_POINT); + + bootc_command_and_args.push("--root-ssh-authorized-keys"); + bootc_command_and_args.push(&ROOT_KEY_MOUNT_POINT); + } + + podman_command_and_args + .iter() + .chain([image].iter()) + .chain(bootc_command_and_args.iter()) + .map(|x| x.to_string()) + .collect::>() +} + +pub(crate) fn run_podman(all_args: Vec) -> Result<(), anyhow::Error> { + Command::new(all_args[0].clone()) + .args(all_args[1..].iter()) + .status() + .context(format!( + "Failed to run the command \"{}\"", + all_args.join(" ") + ))?; + Ok(()) +} diff --git a/reinstall-cli/src/prompt.rs b/reinstall-cli/src/prompt.rs new file mode 100644 index 000000000..301f696a0 --- /dev/null +++ b/reinstall-cli/src/prompt.rs @@ -0,0 +1,91 @@ +use std::io::Write; + +use anyhow::{ensure, Result}; + +use crate::users::{get_all_users_keys, UserKeys}; + +fn prompt_single_user(user: &crate::users::UserKeys) -> Result> { + let prompt = format!( + "Found only one user ({}) with {} SSH authorized keys. Would you like to install this user in the system? (y/n)", + user.user, + user.num_keys(), + ); + let answer = ask_yes_no(&prompt)?; + Ok(if answer { vec![&user] } else { vec![] }) +} + +fn prompt_user_selection( + all_users: &[crate::users::UserKeys], +) -> Result> { + let keys: Vec = all_users.iter().map(|x| x.user.clone()).collect(); + + // TODO: Handle https://github.com/console-rs/dialoguer/issues/77 + let selected_user_indices: Vec = dialoguer::MultiSelect::new() + .with_prompt("Select the users you want to install in the system (along with their authorized SSH keys)") + .items(&keys) + .interact()?; + + Ok(selected_user_indices + .iter() + // Safe unwrap because we know the index is valid + .map(|x| all_users.get(*x).unwrap()) + .collect()) +} + +/// Temporary safety mechanism to stop devs from running it on their dev machine. TODO: Discuss +/// final prompting UX in https://github.com/containers/bootc/discussions/1060 +pub(crate) fn temporary_developer_protection_prompt() -> Result<()> { + let prompt = "This will reinstall your system. Are you sure you want to continue? (y/n) "; + let answer = ask_yes_no(prompt)?; + + if !answer { + println!("Exiting without reinstalling the system."); + std::process::exit(0); + } + + Ok(()) +} + +pub(crate) fn ask_yes_no(prompt: &str) -> Result { + let mut user_input = String::new(); + Ok(loop { + print!("{}", prompt); + std::io::stdout().flush()?; + std::io::stdin().read_line(&mut user_input)?; + match user_input.trim() { + "y" => break true, + "n" => { + break false; + } + _ => { + println!("Unrecognized input. enter 'y' or 'n'."); + user_input.clear(); + } + } + }) +} + +/// For now we only support the root user. This function returns the root user's SSH +/// authorized_keys. In the future, when bootc supports multiple users, this function will need to +/// be updated to return the SSH authorized_keys for all the users selected by the user. +pub(crate) fn get_root_key() -> Result> { + let users = get_all_users_keys()?; + if users.is_empty() { + return Ok(None); + } + + let selected_users = if users.len() == 1 { + prompt_single_user(&users[0])? + } else { + prompt_user_selection(&users)? + }; + + ensure!( + selected_users.iter().all(|x| x.user == "root"), + "Only root user is supported" + ); + + let root_key = selected_users.into_iter().find(|x| x.user == "root"); + + Ok(root_key.cloned()) +} diff --git a/reinstall-cli/src/users.rs b/reinstall-cli/src/users.rs new file mode 100644 index 000000000..e06e17dc5 --- /dev/null +++ b/reinstall-cli/src/users.rs @@ -0,0 +1,106 @@ +use anyhow::{Context, Result}; +use std::fmt::Display; +use std::fmt::Formatter; +use uzers::os::unix::UserExt; + +#[derive(Clone, Debug)] +pub(crate) struct UserKeys { + pub(crate) user: String, + pub(crate) authorized_keys: String, + pub(crate) authorized_keys_path: String, +} + +impl UserKeys { + pub(crate) fn num_keys(&self) -> usize { + self.authorized_keys.lines().count() + } +} + +impl Display for UserKeys { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "User {} ({} authorized keys)", + self.user, + self.num_keys() + ) + } +} + +/// # Safety +/// See safety information in [`uzers::all_users`]. We are not impacted because we're not +/// multi-threaded. +pub(crate) fn get_all_users_keys() -> Result> { + #[allow(unsafe_code)] + let iter = unsafe { uzers::all_users() }; + + let mut result = Vec::new(); + + for user_info in iter { + if user_info.uid() < 1000 && user_info.uid() != 0 { + tracing::debug!( + "Skipping user {} with uid {} because it's a system user (uid < 1000)", + user_info.name().to_string_lossy(), + user_info.uid() + ); + continue; + } + + if ["/bin/false", "/bin/true", "nologin"] + .into_iter() + .any(|x| user_info.shell().to_string_lossy().contains(x)) + { + tracing::debug!( + "Skipping user {} with shell {} because it can't login", + user_info.name().to_string_lossy(), + user_info.shell().to_string_lossy() + ); + continue; + } + + let home_dir = user_info.home_dir(); + let user_authorized_keys_path = home_dir.join(".ssh/authorized_keys"); + if !user_authorized_keys_path.exists() { + tracing::debug!( + "Skipping user {} because it doesn't have an SSH authorized_keys file", + user_info.name().to_string_lossy() + ); + continue; + } + + let user_name = user_info + .name() + .to_str() + .context("user name is not valid utf-8")?; + + let user_authorized_keys = std::fs::read_to_string(&user_authorized_keys_path) + .context("Failed to read user's authorized keys")?; + + if user_authorized_keys.trim().is_empty() { + tracing::debug!( + "Skipping user {} because it has an empty SSH authorized_keys file", + user_info.name().to_string_lossy() + ); + continue; + } + + let user_keys = UserKeys { + user: user_name.to_string(), + authorized_keys: user_authorized_keys, + authorized_keys_path: user_authorized_keys_path + .to_str() + .context("user's authorized_keys path is not valid utf-8")? + .to_string(), + }; + + tracing::debug!( + "Found user {} with {} SSH authorized_keys", + user_keys.user, + user_keys.num_keys() + ); + + result.push(user_keys); + } + + Ok(result) +} diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 6aeda593d..e70b13fb1 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -14,6 +14,7 @@ serde_json = { workspace = true } tempfile = { workspace = true } tracing = { workspace = true } tokio = { workspace = true, features = ["process"] } +tracing-subscriber = { workspace = true, features = ["env-filter"] } [dev-dependencies] similar-asserts = { workspace = true } diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 6976ce24d..5d4e4da11 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -3,4 +3,6 @@ //! "core" crates. //! mod command; +mod tracing_util; pub use command::*; +pub use tracing_util::*; diff --git a/utils/src/tracing_util.rs b/utils/src/tracing_util.rs new file mode 100644 index 000000000..c6a7cf94e --- /dev/null +++ b/utils/src/tracing_util.rs @@ -0,0 +1,18 @@ +//! Helpers related to tracing, used by main entrypoints + +/// Initialize tracing with the default configuration. +pub fn initialize_tracing() { + // Don't include timestamps and such because they're not really useful and + // too verbose, and plus several log targets such as journald will already + // include timestamps. + let format = tracing_subscriber::fmt::format() + .without_time() + .with_target(false) + .compact(); + // Log to stderr by default + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .event_format(format) + .with_writer(std::io::stderr) + .init(); +}