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