diff --git a/Cargo.lock b/Cargo.lock index d617d2d..8aa22cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -267,6 +267,7 @@ dependencies = [ "event-listener", "getrandom", "libc", + "natord", "sd-notify", "serde", "serde_json", @@ -686,6 +687,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "natord" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" + [[package]] name = "nix" version = "0.30.1" @@ -878,18 +885,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 8bb62e8..59b1817 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ event-listener = "5.4.1" tokio = { version = "1.47.1", features = ["rt", "rt-multi-thread"] } sd-notify = { version = "0.4.5", optional = true } getrandom = "0.3.3" +natord = "1.0" [features] default = ["dbus", "hooks"] diff --git a/NEWS.md b/NEWS.md index 65ba937..7f49062 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,8 @@ * Mounting an already-mounted boot environment is now a no-op. +* An experimental `beadm load` subcommand enables fast rebooting via `kexec(8)`. + # beadm v0.2.1 * D-Bus errors are now more informative. diff --git a/doc/beadm.8.scd b/doc/beadm.8.scd index 8661310..252efef 100644 --- a/doc/beadm.8.scd +++ b/doc/beadm.8.scd @@ -19,6 +19,7 @@ beadm - Boot Environment Administration *beadm* *rename* _name_ _new-name_ ++ *beadm* *describe* { _name_ | _name@snapshot_ } _desc_ ++ *beadm* *rollback* _name_ _snapshot_ ++ +*beadm* *load* ++ *beadm* *init* _pool_ ++ *beadm* *daemon* @@ -189,6 +190,14 @@ same installation) on a single system. _name_ The boot environment to query. +*load* + + Load the activated boot environment's kernel and initramfs into *kexec(8)*, + enabling fast reboots on supported systems. + + The kernel, initramfs, and command line are determined automatically using the + same techniques used by *zfsbootmenu(7)*. + *init* _pool_ Create the ZFS dataset layout for boot environments. @@ -281,6 +290,18 @@ Set a description for a snapshot: Roll back to a previous snapshot: *beadm rollback current-be@yesterday* +Reboot the activated boot environment via the systemd *kexec(8)* integration: + + *beadm load* ++ +*systemctl kexec* + +Reboot into a temporarily activated boot environment via *kexec(8)* (on non-systemd +systems): + + *beadm activate -t alt* ++ +*beadm load* ++ +*kexec -e* + # EXIT STATUS *0* diff --git a/src/be/kexec.rs b/src/be/kexec.rs new file mode 100644 index 0000000..4bac060 --- /dev/null +++ b/src/be/kexec.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MPL-2.0 + +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::fs; +use std::process::Command; + +use crate::{Error, be::scan}; + +/// Check if `kexec(8)` can be used to load a kernel and initramfs pair. +pub fn has_kexec() -> Result<(), Error> { + if let Err(e) = Command::new("kexec").arg("--version").output() { + return Err(Error::KexecNotAvailable(e)); + } + + // Check if kexec has been disabled at the kernel level. + if let Ok(contents) = fs::read_to_string("/proc/sys/kernel/kexec_load_disabled") { + if contents.trim() != "0" { + return Err(Error::KexecNotAvailable(std::io::Error::other( + "kexec syscalls disabled for this kernel", + ))); + } + } + + Ok(()) +} + +/// Load a kernel and initramfs into kexec. +pub fn kexec_load(kernel: &scan::KernelPair, cmdline: &str) -> Result<(), Error> { + // TODO: Device tree support. + let mut cmd = Command::new("kexec"); + cmd.arg("-l") + .arg(kernel.path.as_os_str()) + .arg("--initrd") + .arg(kernel.initrd.as_os_str()) + .arg("--command-line") + .arg(cmdline); + + let output = cmd.output().map_err(|e| Error::KexecFailed(e))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(Error::KexecFailed(std::io::Error::other(stderr.trim()))); + } + + Ok(()) +} diff --git a/src/be/mock.rs b/src/be/mock.rs index 4e595bf..19b87d4 100644 --- a/src/be/mock.rs +++ b/src/be/mock.rs @@ -585,6 +585,22 @@ impl Client for EmulatorClient { fn active_root(&self) -> Option<&Root> { Some(&self.active_root) } + + fn load(&self, root: Option<&Root>) -> Result<(), Error> { + // Validate that we have a boot environment to load, but otherwise do + // nothing. + let root = self.effective_root(root); + match self + .bes + .read() + .unwrap() + .iter() + .find(|be| (be.next_boot || be.boot_once) && be.root == *root) + { + Some(_) => Ok(()), + None => Err(Error::NoActiveBootEnvironment), + } + } } fn sample_boot_environments() -> Vec { diff --git a/src/be/mod.rs b/src/be/mod.rs index d830c97..d230b0d 100644 --- a/src/be/mod.rs +++ b/src/be/mod.rs @@ -12,6 +12,7 @@ use thiserror::Error as ThisError; #[cfg(feature = "dbus")] use zvariant::{DeserializeDict, SerializeDict, Type}; +mod kexec; mod mock; pub(crate) mod scan; mod validation; @@ -58,6 +59,15 @@ pub enum Error { #[error("Invalid boot environment root: '{name}'")] InvalidBootEnvironmentRoot { name: String }, + #[error("No matching kernel and initramfs pair found in /boot")] + KernelNotFound, + + #[error("kexec command not found, disabled, or otherwise unavailable: {0}")] + KexecNotAvailable(std::io::Error), + + #[error("Failed to execute kexec: {0}")] + KexecFailed(std::io::Error), + #[error(transparent)] LibzfsError(#[from] zfs::LibzfsError), @@ -87,6 +97,9 @@ impl From for zbus::fdo::Error { Error::InvalidBootEnvironmentRoot { .. } => { zbus::fdo::Error::InvalidArgs(err.to_string()) } + Error::KernelNotFound => zbus::fdo::Error::NotSupported(err.to_string()), + Error::KexecNotAvailable(_) => zbus::fdo::Error::NotSupported(err.to_string()), + Error::KexecFailed(_) => zbus::fdo::Error::Failed(err.to_string()), Error::Io(err) => { let e: zbus::Error = From::from(err); e.into() @@ -395,6 +408,14 @@ pub trait Client: Send + Sync { /// Get the active boot environment root, if any. fn active_root(&self) -> Option<&Root>; + + /// Load the activated boot environment's kernel and initramfs into + /// `kexec(8)`. This enables faster reboots on some systems. + /// + /// After calling this method, the user can execute the loaded kernel by + /// running `kexec -e` or, ideally, `reboot` or `systemctl kexec` with + /// systemd. + fn load(&self, root: Option<&Root>) -> Result<(), Error>; } /// Generate a snapshot name based on the current time. diff --git a/src/be/scan.rs b/src/be/scan.rs index ddfc1b3..bb274c8 100644 --- a/src/be/scan.rs +++ b/src/be/scan.rs @@ -5,13 +5,18 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; + +use crate::Error; /// Relevant content from an `/etc/os-release` file. #[derive(Debug, Clone, PartialEq, Eq)] pub struct OsRelease { /// The `ID` parameter identifying the distribution. pub id: String, + /// The `ID_LIKE` parameter, a space-separated list of distribution IDs this + /// one is similar to (if any). + pub id_like: Vec, /// The "pretty" name of the distribution. pub pretty: String, } @@ -21,12 +26,27 @@ impl Default for OsRelease { // Match the defaults from os-release(5). Self { id: "linux".to_string(), + id_like: Vec::new(), pretty: "Linux".to_string(), } } } impl OsRelease { + /// Attempt to find and parse the `/etc/os-release` (or + /// `/usr/lib/os-release`) file from the root filesystem at `root_dir`. + pub fn scan>(root_dir: P) -> std::io::Result> { + let etc_path = root_dir.as_ref().join("etc/os-release"); + if etc_path.exists() { + return Self::from_path(&etc_path).map(Some); + } + let usr_path = root_dir.as_ref().join("usr/lib/os-release"); + if usr_path.exists() { + return Self::from_path(&usr_path).map(Some); + } + Ok(None) + } + /// Read and parse an `/etc/os-release` file from a path. pub fn from_path>(path: P) -> std::io::Result { fs::read_to_string(path).map(Self::parse) @@ -38,17 +58,133 @@ impl OsRelease { for line in contents.as_ref().lines() { if let Some(value) = line.strip_prefix("ID=") { out.id = value.trim_matches('"').to_string(); + } else if let Some(value) = line.strip_prefix("ID_LIKE=") { + out.id_like = value + .trim_matches('"') + .split_whitespace() + .map(|s| s.to_string()) + .collect(); } else if let Some(value) = line.strip_prefix("PRETTY_NAME=") { out.pretty = value.trim_matches('"').to_string(); } } out } + + /// Combined distribution identifiers. + pub fn ids(&self) -> Vec<&str> { + let mut ids = vec![self.id.as_str()]; + ids.extend(self.id_like.iter().map(|s| s.as_str())); + ids + } +} + +/// A matching kernel and initial RAM disk pair. +#[derive(Debug, PartialEq, Eq)] +pub struct KernelPair { + /// Path to the kernel image. + pub path: PathBuf, + /// Path to the initial RAM disk. + pub initrd: PathBuf, +} + +impl KernelPair { + /// Find the latest kernel and initramfs pair -- if any -- in the `/boot` + /// directory of the root filesystem at `root_dir`. + /// + /// This uses the same approach as ZFSBootMenu, and should be compatible. + pub fn scan>(root_dir: P) -> Result { + let boot_dir = root_dir.as_ref().join("boot"); + + // Match the kernel prefixes supported by ZFSBootMenu. + const KERNEL_PREFIXES: [&str; 5] = ["vmlinuz", "vmlinux", "linux", "linuz", "kernel"]; + + let mut kernel_candidates = Vec::new(); + for entry in boot_dir.read_dir()? { + let path = entry?.path(); + // Be slightly more picky than ZBM and ignore non-UTF8 file names. + let basename = match path.file_name().and_then(|name| name.to_str()) { + Some(name) => name.to_string(), + None => continue, + }; + + for prefix in KERNEL_PREFIXES { + if let Some(suffix) = basename.strip_prefix(prefix) { + let version = suffix.strip_prefix("-").unwrap_or(suffix).to_string(); + kernel_candidates.push((path, basename, version)); + break; // No boot directory should have multiple kernel prefixes. + } + } + } + + if kernel_candidates.is_empty() { + return Err(Error::KernelNotFound); + } + + // Sort kernels by version using "natural" ordering and pick the latest one. + kernel_candidates.sort_by(|a, b| natord::compare(&b.2, &a.2)); + let (kernel_path, kernel_name, kernel_version) = + kernel_candidates.into_iter().next().unwrap(); + + // We generally follow ZFSBootMenu here and support the following cases for + // matching an initramfs to a kernels: + // + // * initramfs-${label}${extension} + // * initramfs${extension}-${label} + // * initrd-${label}${extension} + // * initrd${extension}-${label} + // + // Where the "label" is the full kernel name or just the trailing version + // number or other identifier. + const INITRAMFS_PREFIXES: [&str; 2] = ["initramfs", "initrd"]; + const EXTENSIONS: [&str; 16] = [ + "", + ".img", + ".gz", + ".img.gz", + ".bz2", + ".img.bz2", + ".xz", + ".img.xz", + ".lzma", + ".img.lzma", + ".lz4", + ".img.lz4", + ".lzo", + ".img.lzo", + ".zstd", + ".img.zstd", + ]; + + for label in &[kernel_name, kernel_version] { + for prefix in INITRAMFS_PREFIXES { + for ext in EXTENSIONS { + let initrd = boot_dir.join(format!("{}-{}{}", prefix, label, ext)); + if initrd.exists() { + return Ok(Self::new(kernel_path.to_owned(), initrd)); + } + let initrd = boot_dir.join(format!("{}{}-{}", prefix, ext, label)); + if initrd.exists() { + return Ok(Self::new(kernel_path.to_owned(), initrd)); + } + } + } + } + + Err(Error::KernelNotFound) + } + + /// Create a new `KernelPair` from a matching kernel binary and initramfs. + fn new(path: PathBuf, initrd: PathBuf) -> Self { + KernelPair { path, initrd } + } } #[cfg(test)] mod tests { use super::*; + use std::fs; + use tempfile::TempDir; #[test] fn test_os_release_parsing() { @@ -71,6 +207,7 @@ LOGO=ubuntu-logo "#, OsRelease { id: "ubuntu".to_string(), + id_like: vec!["debian".to_string()], pretty: "Ubuntu 24.04 LTS".to_string(), }, ), @@ -142,6 +279,7 @@ LOGO=archlinux-logo "#, OsRelease { id: "arch".to_string(), + id_like: Vec::new(), pretty: "Arch Linux".to_string(), }, ), @@ -155,6 +293,7 @@ BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues" "#, OsRelease { id: "alpine".to_string(), + id_like: Vec::new(), pretty: "Alpine Linux v3.21".to_string(), }, ), @@ -169,6 +308,7 @@ BUG_REPORT_URL="https://bugs.gentoo.org/" "#, OsRelease { id: "gentoo".to_string(), + id_like: Vec::new(), pretty: "Gentoo/Linux".to_string(), }, ), @@ -183,6 +323,7 @@ ANSI_COLOR="0;38;2;214;79;93" "#, OsRelease { id: "chimera".to_string(), + id_like: Vec::new(), pretty: "Chimera Linux".to_string(), }, ), @@ -199,6 +340,7 @@ DISTRIB_ID="void" "#, OsRelease { id: "void".to_string(), + id_like: Vec::new(), pretty: "Void Linux".to_string(), }, ), @@ -210,4 +352,130 @@ DISTRIB_ID="void" assert_eq!(parsed, expected, "Failed for input: {:?}", input); } } + + #[test] + fn test_os_release_scan() { + let tmpdir = TempDir::new().unwrap(); + + // 1. No os-release(5) file. + let result = OsRelease::scan(tmpdir.path()).unwrap(); + assert!(result.is_none()); + + // 2. Found /usr/lib/os-release. + fs::create_dir_all(tmpdir.path().join("usr/lib")).unwrap(); + fs::write(tmpdir.path().join("usr/lib/os-release"), "ID=usrlib\n").unwrap(); + assert_eq!( + OsRelease::scan(tmpdir.path()).unwrap(), + Some(OsRelease { + id: "usrlib".to_string(), + ..Default::default() + }) + ); + + // 3. /etc/os-release takes precendence. + fs::create_dir(tmpdir.path().join("etc")).unwrap(); + fs::write(tmpdir.path().join("etc/os-release"), "ID=etc\n").unwrap(); + assert_eq!( + OsRelease::scan(tmpdir.path()).unwrap(), + Some(OsRelease { + id: "etc".to_string(), + ..Default::default() + }) + ); + } + + #[test] + fn test_find_kernel_and_initramfs_patterns() { + struct TestCase { + files: &'static [&'static str], + kernel: &'static str, + initramfs: &'static str, + } + + let cases = [ + TestCase { + // This is what I see in /boot on Pop OS, which is presumably + // similar to Ubuntu. + // + // This tests version selection and precedence. + files: &[ + "vmlinuz-6.16.3-76061603-generic", + "initrd.img-6.16.3-76061603-generic", + "vmlinuz-6.6.6-76060606-generic.dpkg-bak", + "initrd.img-6.6.6-76060606-generic.dpkg-bak", + "vmlinuz", + "initrd.img", + "vmlinuz.old", + "initrd.img.old", + ], + kernel: "vmlinuz-6.16.3-76061603-generic", + initramfs: "initrd.img-6.16.3-76061603-generic", + }, + TestCase { + // Arch Linux example. + files: &[ + "vmlinuz-linux", + "initramfs-linux.img", + "initramfs-linux-fallback.img", + ], + kernel: "vmlinuz-linux", + initramfs: "initramfs-linux.img", + }, + TestCase { + // Tests the 'vmlinux' kernel naming scheme and compression + // suffixes for the initramfs. + files: &["vmlinux-6.1.12", "initrd-6.1.12.gz"], + kernel: "vmlinux-6.1.12", + initramfs: "initrd-6.1.12.gz", + }, + TestCase { + // Tests the 'linux' kernel naming scheme, dual extension + // suffixes for the initramfs, and the version suffix format for + // the initramfs. + files: &["linux-lts", "initrd.img.bz2-lts"], + kernel: "linux-lts", + initramfs: "initrd.img.bz2-lts", + }, + TestCase { + // Tests the 'linuz' and 'initramfs' naming schemes. + files: &["linuz-4.19.0", "initramfs-4.19.0.img.xz"], + kernel: "linuz-4.19.0", + initramfs: "initramfs-4.19.0.img.xz", + }, + TestCase { + // Tests the 'kernel' kernel naming scheme. + files: &["kernel-5.4.0", "initrd.img-5.4.0"], + kernel: "kernel-5.4.0", + initramfs: "initrd.img-5.4.0", + }, + TestCase { + // No hyphens. + files: &["vmlinuz5.15.0", "initramfs-vmlinuz5.15.0"], + kernel: "vmlinuz5.15.0", + initramfs: "initramfs-vmlinuz5.15.0", + }, + TestCase { + // No version. + files: &["vmlinuz", "initramfs-vmlinuz.img"], + kernel: "vmlinuz", + initramfs: "initramfs-vmlinuz.img", + }, + ]; + + for (i, case) in cases.iter().enumerate() { + let tmpdir = TempDir::new().unwrap(); + let boot_dir = tmpdir.path().join("boot"); + fs::create_dir_all(&boot_dir).unwrap(); + for file in case.files { + fs::File::create(boot_dir.join(file)).unwrap(); + } + + let result = KernelPair::scan(tmpdir.path()).unwrap(); + let expected = KernelPair { + path: boot_dir.join(case.kernel), + initrd: boot_dir.join(case.initramfs), + }; + assert_eq!(result, expected, "case {}", i); + } + } } diff --git a/src/be/zfs.rs b/src/be/zfs.rs index 551236e..52fbca3 100644 --- a/src/be/zfs.rs +++ b/src/be/zfs.rs @@ -15,7 +15,7 @@ use std::sync::{LazyLock, Mutex, MutexGuard}; use super::validation::{validate_component, validate_dataset_name}; use super::{ BootEnvironment, Client, Error, Label, MountMode, Root, Snapshot, generate_snapshot_name, - generate_temp_mountpoint, is_temp_mountpoint, + generate_temp_mountpoint, is_temp_mountpoint, kexec, scan, }; const DESCRIPTION_PROP: &str = "ca.kamacite:description"; @@ -651,6 +651,54 @@ impl Client for LibZfsClient { fn active_root(&self) -> Option<&Root> { self.active_root.as_ref() } + + fn load(&self, root: Option<&Root>) -> Result<(), Error> { + kexec::has_kexec()?; + + let root_dataset = self.effective_root(root)?; + let bes = self.get_boot_environments(root)?; + let target = match bes + .iter() + // Prefer temporarily activated, then activated. + .find(|be| be.boot_once) + .or(bes.iter().find(|be| be.next_boot)) + { + Some(target) => target, + None => return Err(Error::NoActiveBootEnvironment), + }; + + let be_path = root_dataset.append(&target.name)?; + + // We need the boot environment to be mounted so that we can read from + // its filesystem. + // + // Note: we don't unmount here, because (a) the user may want to inspect + // the filesystem if this fails; and (b) if it succeeds, unmounting + // should be handled by the shutdown sequence rather than us. + let mountpoint = self.mount(&target.name, None, MountMode::ReadOnly, root)?; + + // Make sure we can find a kernel first. + let kernel = scan::KernelPair::scan(&mountpoint)?; + let os_release = scan::OsRelease::scan(&mountpoint)?; + + // Read default kernel command line parameters, if any, from the + // well-known ZBM property. + let lzh = LibHandle::get(); + let be_dataset = Dataset::filesystem(&lzh, &be_path)?; + let params = be_dataset.get_user_property("org.zfsbootmenu:commandline"); + + // Try to reboot with a matching hostid so that ZFS pool can be imported + // without issue. + let current_hostid = read_hostid(None::)?; + let cmdline = build_cmdline( + &be_path, + params.as_deref(), + current_hostid, + os_release.as_ref(), + ); + + kexec::kexec_load(&kernel, &cmdline) + } } /// Safe wrapper for various operations on an owned ZFS dataset handle. @@ -1634,6 +1682,42 @@ fn get_active_boot_environment_root() -> Result { Ok(Root::from(parent)) } +/// Construct the kernel command line for booting a ZFS boot environment, which +/// will vary based on the distribution. +fn build_cmdline( + dataset: &DatasetName, + params: Option<&str>, + hostid: Option, + os_release: Option<&scan::OsRelease>, +) -> String { + // The root-on-ZFS filesystem prefix varies by distribution. + let mut root_prefix = "root=zfs:"; // A semi-reasonable default. + for dist_id in os_release.map(|os| os.ids()).unwrap_or_default() { + // Handle known distributions directly. + match dist_id { + "ubuntu" | "debian" | "void" | "chimera" => { + root_prefix = "root=zfs:"; + break; + } + "arch" | "artix" => { + root_prefix = "zfs="; + break; + } + "gentoo" | "alpine" => { + root_prefix = "root=ZFS="; + break; + } + _ => continue, + } + } + let params = params.unwrap_or("quiet loglevel=4").trim(); + let mut cmdline = format!("{}{} {}", root_prefix, dataset.to_string(), params); + if let Some(id) = hostid { + cmdline.push_str(&format!(" spl_hostid=0x{:08x}", id)); + } + cmdline +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/dbus.rs b/src/dbus.rs index b23370b..be195a3 100644 --- a/src/dbus.rs +++ b/src/dbus.rs @@ -361,6 +361,18 @@ impl Client for ClientProxy { fn active_root(&self) -> Option<&Root> { self.active_root.as_ref() } + + fn load(&self, root: Option<&Root>) -> Result<(), Error> { + let beroot = root.map(|r| r.as_str()).unwrap_or_default(); + self.connection.call_method( + Some(SERVICE_NAME), + BOOT_ENV_PATH, + Some(MANAGER_INTERFACE), + "Load", + &beroot, + )?; + Ok(()) + } } // ============================================================================ @@ -1181,6 +1193,19 @@ impl BootEnvironmentManager { Ok(()) } + /// Load the activated boot environment's kernel into kexec(8). + async fn load( + &self, + beroot: &str, + #[zbus(header)] header: zbus::message::Header<'_>, + #[zbus(connection)] conn: &zbus::Connection, + ) -> zbus::fdo::Result<()> { + check_authorization(conn, &header, "ca.kamacite.BootEnvironments1.manage").await?; + self.client.load(root_from_arg(beroot)?.as_ref())?; + tracing::info!("Loaded the kernel of the activated boot environment into kexec"); + Ok(()) + } + /// The active boot environment root. #[zbus(property(emits_changed_signal = "const"))] fn active_root(&self) -> String { diff --git a/src/main.rs b/src/main.rs index 0be8e45..9aa6b02 100644 --- a/src/main.rs +++ b/src/main.rs @@ -234,6 +234,8 @@ enum Commands { /// The snapshot name. snapshot: String, }, + /// Load the activated boot environment's kernel into kexec(8) + Load, /// Get the host ID from a boot environment. #[command(hide = true)] Hostid { @@ -671,6 +673,13 @@ fn execute_command( println!("Rolled back to '{}'.", snapshot); Ok(()) } + Commands::Load => { + client + .load(root) + .context("Failed to load boot environment kernel")?; + println!("Kernel loaded. Ready to reboot via kexec."); + Ok(()) + } Commands::Hostid { be_name } => { match client .hostid(be_name, root) diff --git a/src/meson.build b/src/meson.build index 5fdd724..ed07faa 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,6 +1,7 @@ # -*- meson-indent-basic: 8 -*- sources = files( + 'be/kexec.rs', 'be/mock.rs', 'be/mod.rs', 'be/scan.rs',