bootc_lib/bootc_composefs/
soft_reboot.rs

1use crate::{
2    bootc_composefs::{
3        service::start_finalize_stated_svc, status::composefs_deployment_status_from,
4    },
5    cli::SoftRebootMode,
6    composefs_consts::COMPOSEFS_CMDLINE,
7    store::{BootedComposefs, Storage},
8};
9use anyhow::{Context, Result};
10use bootc_initramfs_setup::setup_root;
11use bootc_kernel_cmdline::utf8::Cmdline;
12use bootc_mount::{PID1, bind_mount_from_pidns};
13use camino::Utf8Path;
14use cap_std_ext::cap_std::ambient_authority;
15use cap_std_ext::cap_std::fs::Dir;
16use cap_std_ext::dirext::CapStdExtDirExt;
17use fn_error_context::context;
18use ostree_ext::systemd_has_soft_reboot;
19use rustix::mount::{UnmountFlags, unmount};
20use std::{fs::create_dir_all, os::unix::process::CommandExt, path::PathBuf, process::Command};
21
22const NEXTROOT: &str = "/run/nextroot";
23
24#[context("Resetting soft reboot state")]
25pub(crate) fn reset_soft_reboot() -> Result<()> {
26    // NOTE: By default bootc runs in an unshared mount namespace;
27    // this sets up our /runto actually be the same as the host/run
28    // so the umount (at the end of this function) actually affects the host
29    //
30    // Similar operation is performed in `prepare_soft_reboot_composefs`
31    let run = Utf8Path::new("/run");
32    bind_mount_from_pidns(PID1, &run, &run, true).context("Bind mounting /run")?;
33
34    let run_dir = Dir::open_ambient_dir("/run", ambient_authority()).context("Opening run")?;
35
36    let nextroot = run_dir
37        .open_dir_optional("nextroot")
38        .context("Opening nextroot")?;
39
40    let Some(nextroot) = nextroot else {
41        tracing::debug!("Nextroot does not exist");
42        println!("No deployment staged for soft rebooting");
43        return Ok(());
44    };
45
46    let nextroot_mounted = nextroot
47        .is_mountpoint(".")?
48        .ok_or_else(|| anyhow::anyhow!("Failed to get mount info"))?;
49
50    if !nextroot_mounted {
51        tracing::debug!("Nextroot is not a mountpoint");
52        println!("No deployment staged for soft rebooting");
53        return Ok(());
54    }
55
56    unmount(NEXTROOT, UnmountFlags::DETACH).context("Unmounting nextroot")?;
57
58    println!("Cleared soft reboot queued state");
59
60    Ok(())
61}
62
63/// Checks if the provided deployment is soft reboot capable, and soft reboots the system if
64/// argument `reboot` is true
65#[context("Soft rebooting")]
66pub(crate) async fn prepare_soft_reboot_composefs(
67    storage: &Storage,
68    booted_cfs: &BootedComposefs,
69    deployment_id: Option<&str>,
70    soft_reboot_mode: SoftRebootMode,
71    reboot: bool,
72) -> Result<()> {
73    if !systemd_has_soft_reboot() {
74        anyhow::bail!("System does not support soft reboots")
75    }
76
77    let deployment_id = deployment_id.ok_or_else(|| anyhow::anyhow!("Expected deployment id"))?;
78
79    if *deployment_id == *booted_cfs.cmdline.digest {
80        anyhow::bail!("Cannot soft-reboot to currently booted deployment");
81    }
82
83    // We definitely need to re-query the state as some deployment might've been staged
84    let host = composefs_deployment_status_from(storage, booted_cfs.cmdline).await?;
85
86    let all_deployments = host.all_composefs_deployments()?;
87
88    let requred_deployment = all_deployments
89        .iter()
90        .find(|entry| entry.deployment.verity == *deployment_id)
91        .ok_or_else(|| anyhow::anyhow!("Deployment '{deployment_id}' not found"))?;
92
93    if !requred_deployment.soft_reboot_capable {
94        match soft_reboot_mode {
95            SoftRebootMode::Required => {
96                anyhow::bail!("Cannot soft-reboot to deployment with a different kernel state")
97            }
98
99            SoftRebootMode::Auto => return Ok(()),
100        }
101    }
102
103    start_finalize_stated_svc()?;
104
105    // escape to global mnt namespace
106    let run = Utf8Path::new("/run");
107    bind_mount_from_pidns(PID1, &run, &run, false).context("Bind mounting /run")?;
108
109    create_dir_all(NEXTROOT).context("Creating nextroot")?;
110
111    let cmdline = Cmdline::from(format!("{COMPOSEFS_CMDLINE}={deployment_id}"));
112
113    let args = bootc_initramfs_setup::Args {
114        cmd: vec![],
115        sysroot: PathBuf::from("/sysroot"),
116        config: Default::default(),
117        root_fs: None,
118        cmdline: Some(cmdline),
119        target: Some(NEXTROOT.into()),
120    };
121
122    setup_root(args)?;
123
124    println!("Soft reboot setup complete");
125
126    if reboot {
127        // Replacing the current process should be fine as we restart userspace anyway
128        let err = Command::new("systemctl").arg("soft-reboot").exec();
129        return Err(anyhow::Error::from(err).context("Failed to exec 'systemctl soft-reboot'"));
130    }
131
132    Ok(())
133}