bootc_lib/
utils.rs

1use std::future::Future;
2use std::io::Write;
3use std::os::fd::BorrowedFd;
4use std::path::{Component, Path, PathBuf};
5use std::process::Command;
6use std::time::Duration;
7
8use anyhow::{Context, Result};
9use bootc_utils::CommandRunExt;
10use camino::Utf8Path;
11use cap_std_ext::cap_std::fs::Dir;
12use cap_std_ext::dirext::CapStdExtDirExt;
13use cap_std_ext::prelude::CapStdExtCommandExt;
14use fn_error_context::context;
15use indicatif::HumanDuration;
16use libsystemd::logging::journal_print;
17use ostree::glib;
18use ostree_ext::container::SignatureSource;
19use ostree_ext::ostree;
20
21/// Try to look for keys injected by e.g. rpm-ostree requesting machine-local
22/// changes; if any are present, return `true`.
23pub(crate) fn origin_has_rpmostree_stuff(kf: &glib::KeyFile) -> bool {
24    // These are groups set in https://github.com/coreos/rpm-ostree/blob/27f72dce4f9b5c176ad030911c12354e2498c07d/rust/src/origin.rs#L23
25    // TODO: Add some notion of "owner" into origin files
26    for group in ["rpmostree", "packages", "overrides", "modules"] {
27        if kf.has_group(group) {
28            return true;
29        }
30    }
31    false
32}
33
34// Access the file descriptor for a sysroot
35#[allow(unsafe_code)]
36pub(crate) fn sysroot_fd(sysroot: &ostree::Sysroot) -> BorrowedFd<'_> {
37    unsafe { BorrowedFd::borrow_raw(sysroot.fd()) }
38}
39
40// Return a cap-std `Dir` type for a sysroot
41pub(crate) fn sysroot_dir(sysroot: &ostree::Sysroot) -> Result<Dir> {
42    Dir::reopen_dir(&sysroot_fd(sysroot)).map_err(Into::into)
43}
44
45// Return a cap-std `Dir` type for a deployment.
46// TODO: in the future this should perhaps actually mount via composefs
47pub(crate) fn deployment_fd(
48    sysroot: &ostree::Sysroot,
49    deployment: &ostree::Deployment,
50) -> Result<Dir> {
51    let sysroot_dir = &Dir::reopen_dir(&sysroot_fd(sysroot))?;
52    let dirpath = sysroot.deployment_dirpath(deployment);
53    sysroot_dir.open_dir(&dirpath).map_err(Into::into)
54}
55
56/// Given an mount option string list like foo,bar=baz,something=else,ro parse it and find
57/// the first entry like $optname=
58/// This will not match a bare `optname` without an equals.
59pub(crate) fn find_mount_option<'a>(
60    option_string_list: &'a str,
61    optname: &'_ str,
62) -> Option<&'a str> {
63    option_string_list
64        .split(',')
65        .filter_map(|k| k.split_once('='))
66        .filter_map(|(k, v)| (k == optname).then_some(v))
67        .next()
68}
69
70#[allow(dead_code)]
71pub fn have_executable(name: &str) -> Result<bool> {
72    let Some(path) = std::env::var_os("PATH") else {
73        return Ok(false);
74    };
75    for mut elt in std::env::split_paths(&path) {
76        elt.push(name);
77        if elt.try_exists()? {
78            return Ok(true);
79        }
80    }
81    Ok(false)
82}
83
84/// Given a target directory, if it's a read-only mount, then remount it writable
85#[context("Opening {target} with writable mount")]
86pub(crate) fn open_dir_remount_rw(root: &Dir, target: &Utf8Path) -> Result<Dir> {
87    if matches!(root.is_mountpoint(target), Ok(Some(true))) {
88        tracing::debug!("Target {target} is a mountpoint, remounting rw");
89        let st = Command::new("mount")
90            .args(["-o", "remount,rw", target.as_str()])
91            .cwd_dir(root.try_clone()?)
92            .status()?;
93
94        anyhow::ensure!(st.success(), "Failed to remount: {st:?}");
95    }
96    root.open_dir(target).map_err(anyhow::Error::new)
97}
98
99/// Given a target path, remove its immutability if present
100#[context("Removing immutable flag from {target}")]
101pub(crate) fn remove_immutability(root: &Dir, target: &Utf8Path) -> Result<()> {
102    use anyhow::ensure;
103
104    tracing::debug!("Target {target} is a mountpoint, remounting rw");
105    let st = Command::new("chattr")
106        .args(["-i", target.as_str()])
107        .cwd_dir(root.try_clone()?)
108        .status()?;
109
110    ensure!(st.success(), "Failed to remove immutability: {st:?}");
111
112    Ok(())
113}
114
115pub(crate) fn spawn_editor(tmpf: &tempfile::NamedTempFile) -> Result<()> {
116    let editor_variables = ["EDITOR"];
117    // These roughly match https://github.com/systemd/systemd/blob/769ca9ab557b19ee9fb5c5106995506cace4c68f/src/shared/edit-util.c#L275
118    let backup_editors = ["nano", "vim", "vi"];
119    let editor = editor_variables.into_iter().find_map(std::env::var_os);
120    let editor = if let Some(e) = editor.as_ref() {
121        e.to_str()
122    } else {
123        backup_editors
124            .into_iter()
125            .find(|v| std::path::Path::new("/usr/bin").join(v).exists())
126    };
127    let editor =
128        editor.ok_or_else(|| anyhow::anyhow!("$EDITOR is unset, and no backup editor found"))?;
129    let mut editor_args = editor.split_ascii_whitespace();
130    let argv0 = editor_args
131        .next()
132        .ok_or_else(|| anyhow::anyhow!("Invalid editor: {editor}"))?;
133    Command::new(argv0)
134        .args(editor_args)
135        .arg(tmpf.path())
136        .lifecycle_bind()
137        .run_inherited()
138        .with_context(|| format!("Invoking editor {editor} failed"))
139}
140
141/// Convert a combination of values (likely from CLI parsing) into a signature source
142pub(crate) fn sigpolicy_from_opt(enforce_container_verification: bool) -> SignatureSource {
143    match enforce_container_verification {
144        true => SignatureSource::ContainerPolicy,
145        false => SignatureSource::ContainerPolicyAllowInsecure,
146    }
147}
148
149/// Output a warning message that we want to be quite visible.
150/// The process (thread) execution will be delayed for a short time.
151pub(crate) fn medium_visibility_warning(s: &str) {
152    anstream::eprintln!(
153        "{}{s}{}",
154        anstyle::AnsiColor::Red.render_fg(),
155        anstyle::Reset.render()
156    );
157    // When warning, add a sleep to ensure it's seen
158    std::thread::sleep(std::time::Duration::from_secs(1));
159}
160
161/// Call an async task function, and write a message to stderr
162/// with an automatic spinner to show that we're not blocked.
163/// Note that generally the called function should not output
164/// anything to stderr as this will interfere with the spinner.
165pub(crate) async fn async_task_with_spinner<F, T>(msg: &str, f: F) -> T
166where
167    F: Future<Output = T>,
168{
169    let start_time = std::time::Instant::now();
170    let pb = indicatif::ProgressBar::new_spinner();
171    let style = indicatif::ProgressStyle::default_bar();
172    pb.set_style(style.template("{spinner} {msg}").unwrap());
173    pb.set_message(msg.to_string());
174    pb.enable_steady_tick(Duration::from_millis(150));
175    // We need to handle the case where we aren't connected to
176    // a tty, so indicatif would show nothing by default.
177    if pb.is_hidden() {
178        eprint!("{msg}...");
179        std::io::stderr().flush().unwrap();
180    }
181    let r = f.await;
182    let elapsed = HumanDuration(start_time.elapsed());
183    let _ = journal_print(
184        libsystemd::logging::Priority::Info,
185        &format!("completed task in {elapsed}: {msg}"),
186    );
187    if pb.is_hidden() {
188        eprintln!("done ({elapsed})");
189    } else {
190        pb.finish_with_message(format!("{msg}: done ({elapsed})"));
191    }
192    r
193}
194
195/// Given a possibly tagged image like quay.io/foo/bar:latest and a digest 0ab32..., return
196/// the digested form quay.io/foo/bar:latest@sha256:0ab32...
197/// If the image already has a digest, it will be replaced.
198#[allow(dead_code)]
199pub(crate) fn digested_pullspec(image: &str, digest: &str) -> String {
200    let image = image.rsplit_once('@').map(|v| v.0).unwrap_or(image);
201    format!("{image}@{digest}")
202}
203
204#[derive(Debug)]
205pub enum EfiError {
206    SystemNotUEFI,
207    MissingVar,
208    #[allow(dead_code)]
209    InvalidData(&'static str),
210    #[allow(dead_code)]
211    Io(std::io::Error),
212}
213
214impl From<std::io::Error> for EfiError {
215    fn from(e: std::io::Error) -> Self {
216        EfiError::Io(e)
217    }
218}
219
220pub fn read_uefi_var(var_name: &str) -> Result<String, EfiError> {
221    use crate::install::EFIVARFS;
222    use cap_std_ext::cap_std::ambient_authority;
223
224    let efivarfs = match Dir::open_ambient_dir(EFIVARFS, ambient_authority()) {
225        Ok(dir) => dir,
226        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Err(EfiError::SystemNotUEFI),
227        Err(e) => Err(e)?,
228    };
229
230    match efivarfs.read(var_name) {
231        Ok(loader_bytes) => {
232            if loader_bytes.len() % 2 != 0 {
233                return Err(EfiError::InvalidData(
234                    "EFI var length is not valid UTF-16 LE",
235                ));
236            }
237
238            // EFI vars are UTF-16 LE
239            let loader_u16_bytes: Vec<u16> = loader_bytes
240                .chunks_exact(2)
241                .map(|x| u16::from_le_bytes([x[0], x[1]]))
242                .collect();
243
244            let loader = String::from_utf16(&loader_u16_bytes)
245                .map_err(|_| EfiError::InvalidData("EFI var is not UTF-16"))?;
246
247            return Ok(loader);
248        }
249
250        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
251            return Err(EfiError::MissingVar);
252        }
253
254        Err(e) => Err(e)?,
255    }
256}
257
258/// Computes a relative path from `from` to `to`.
259///
260/// Both `from` and `to` must be absolute paths.
261pub(crate) fn path_relative_to(from: &Path, to: &Path) -> Result<PathBuf> {
262    if !from.is_absolute() || !to.is_absolute() {
263        anyhow::bail!("Paths must be absolute");
264    }
265
266    let from = from.components().collect::<Vec<_>>();
267    let to = to.components().collect::<Vec<_>>();
268
269    let common = from.iter().zip(&to).take_while(|(a, b)| a == b).count();
270
271    let up = std::iter::repeat(Component::ParentDir).take(from.len() - common);
272
273    let mut final_path = PathBuf::new();
274    final_path.extend(up);
275    final_path.extend(&to[common..]);
276
277    return Ok(final_path);
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_digested_pullspec() {
286        let digest = "ebe3bdccc041864e5a485f1e755e242535c3b83d110c0357fe57f110b73b143e";
287        assert_eq!(
288            digested_pullspec("quay.io/example/foo:bar", digest),
289            format!("quay.io/example/foo:bar@{digest}")
290        );
291        assert_eq!(
292            digested_pullspec("quay.io/example/foo@sha256:otherdigest", digest),
293            format!("quay.io/example/foo@{digest}")
294        );
295        assert_eq!(
296            digested_pullspec("quay.io/example/foo", digest),
297            format!("quay.io/example/foo@{digest}")
298        );
299    }
300
301    #[test]
302    fn test_find_mount_option() {
303        const V1: &str = "rw,relatime,compress=foo,subvol=blah,fast";
304        assert_eq!(find_mount_option(V1, "subvol").unwrap(), "blah");
305        assert_eq!(find_mount_option(V1, "rw"), None);
306        assert_eq!(find_mount_option(V1, "somethingelse"), None);
307    }
308
309    #[test]
310    fn test_sigpolicy_from_opts() {
311        assert_eq!(sigpolicy_from_opt(true), SignatureSource::ContainerPolicy);
312        assert_eq!(
313            sigpolicy_from_opt(false),
314            SignatureSource::ContainerPolicyAllowInsecure
315        );
316    }
317
318    #[test]
319    fn test_relative_path() {
320        let from = Path::new("/sysroot/state/deploy/image_id");
321        let to = Path::new("/sysroot/state/os/default/var");
322
323        assert_eq!(
324            path_relative_to(from, to).unwrap(),
325            PathBuf::from("../../os/default/var")
326        );
327        assert_eq!(
328            path_relative_to(&Path::new("state/deploy"), to)
329                .unwrap_err()
330                .to_string(),
331            "Paths must be absolute"
332        );
333    }
334
335    #[test]
336    fn test_have_executable() {
337        assert!(have_executable("true").unwrap());
338        assert!(!have_executable("someexethatdoesnotexist").unwrap());
339    }
340}