From a10f3a7adff8668c641ef54c61e615624965a104 Mon Sep 17 00:00:00 2001 From: Aaron Jacobs Date: Tue, 4 Nov 2025 16:05:20 -0500 Subject: [PATCH] Add an experimental 'beadm load' command for rebooting via kexec(8). This commit adds direct integration with `kexec(8)`, which allows rebooting the kernel without re-initialising firmware. It largely follows the documented behaviour of ZFSBootMenu for detecting matching kernal and initramfs pairs. Unfortunately this approach relies heavily on the hardware in use to actually reboot successfully, so it remains experimental for now. --- Cargo.lock | 25 ++++- Cargo.toml | 1 + NEWS.md | 2 + doc/beadm.8.scd | 21 ++++ src/be/kexec.rs | 48 +++++++++ src/be/mock.rs | 16 +++ src/be/mod.rs | 21 ++++ src/be/scan.rs | 270 +++++++++++++++++++++++++++++++++++++++++++++++- src/be/zfs.rs | 86 ++++++++++++++- src/dbus.rs | 25 +++++ src/main.rs | 9 ++ src/meson.build | 1 + 12 files changed, 519 insertions(+), 6 deletions(-) create mode 100644 src/be/kexec.rs 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',