From a76ddf0cef0d22367791975ac6ddca6313b9dade Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Sat, 24 Oct 2020 17:31:39 +0000 Subject: [PATCH] Rewrite livefs Now always based on an overlayfs: https://github.com/ostreedev/ostree/pull/2103/commits/f2773c1b55cdcc7eea0558e4f2505d4ecbd53d62 This fixes a whole swath of problems with the previous design, including the danger in replacing `/usr/lib/ostree-boot` which broke booting for some people. Further, we don't need to push a rollback deployment; the livefs changes are always transient. So now we store livefs state in `/run` instead of in the origin file. Since we're doing a rewrite, it's now in Rust for much more safety. We also always work in terms of incremental diffs between commits; the previous huge hammer of swapping `/usr` was way too dangerous. --- rust/Cargo.toml | 1 - rust/src/includes.rs | 10 - rust/src/lib.rs | 6 +- rust/src/livefs.rs | 499 ++++++++++++ rust/src/ostree_diff.rs | 170 +++++ rust/src/ostree_utils.rs | 20 + rust/src/syscore.rs | 73 -- src/app/rpmostree-builtin-livefs.c | 17 +- src/daemon/rpmostree-sysroot-core.c | 64 +- src/daemon/rpmostree-sysroot-core.h | 12 +- src/daemon/rpmostree-sysroot-upgrader.c | 2 +- src/daemon/rpmostreed-deployment-utils.c | 25 +- src/daemon/rpmostreed-os-experimental.c | 19 +- src/daemon/rpmostreed-transaction-livefs.c | 847 +-------------------- src/daemon/rpmostreed-transaction-types.c | 2 +- src/daemon/rpmostreed-transaction-types.h | 7 +- src/libpriv/rpmostree-origin.c | 41 - src/libpriv/rpmostree-origin.h | 12 - src/libpriv/rpmostree-rust-prelude.h | 2 + tests/vmcheck/test-layering-scripts.sh | 8 +- tests/vmcheck/test-livefs.sh | 126 +-- 21 files changed, 792 insertions(+), 1171 deletions(-) create mode 100644 rust/src/livefs.rs create mode 100644 rust/src/ostree_diff.rs create mode 100644 rust/src/ostree_utils.rs delete mode 100644 rust/src/syscore.rs diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 4e630b1d..0abbdbd2 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -35,7 +35,6 @@ subprocess = "0.2.6" chrono = { version = "0.4.19", features = ["serde"] } libdnf-sys = { path = "libdnf-sys", version = "0.1.0" } - [lib] name = "rpmostree_rust" path = "src/lib.rs" diff --git a/rust/src/includes.rs b/rust/src/includes.rs index a96c87c5..a52f5ead 100644 --- a/rust/src/includes.rs +++ b/rust/src/includes.rs @@ -8,7 +8,6 @@ NOTICE: The C header definitions are canonical, please update those first then synchronize the entries here. !*/ -use crate::syscore::ffi::RpmOstreeOrigin; use libdnf_sys::DnfPackage; // From `libpriv/rpmostree-rpm-util.h`. @@ -19,12 +18,3 @@ extern "C" { gerror: *mut *mut glib_sys::GError, ) -> libc::c_int; } - -// From `libpriv/rpmostree-origin.h`. -extern "C" { - pub(crate) fn rpmostree_origin_get_live_state( - origin: *mut RpmOstreeOrigin, - out_inprogress: *mut *mut libc::c_char, - out_livereplaced: *mut *mut libc::c_char, - ); -} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 3a8186c2..5c65237e 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -20,10 +20,12 @@ mod initramfs; pub use self::initramfs::ffi::*; mod lockfile; pub use self::lockfile::*; +mod livefs; +pub use self::livefs::*; +mod ostree_diff; +mod ostree_utils; mod progress; pub use self::progress::*; -mod syscore; -pub use self::syscore::ffi::*; mod testutils; pub use self::testutils::*; mod treefile; diff --git a/rust/src/livefs.rs b/rust/src/livefs.rs new file mode 100644 index 00000000..25eab456 --- /dev/null +++ b/rust/src/livefs.rs @@ -0,0 +1,499 @@ +//! Core implementation logic for "livefs" which applies +//! changes to an overlayfs on top of `/usr` in the booted +//! deployment. +/* + * Copyright (C) 2020 Red Hat, Inc. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use anyhow::{bail, Context, Result}; +use nix::sys::statvfs; +use openat_ext::OpenatDirExt; +use ostree::DeploymentUnlockedState; +use rayon::prelude::*; +use serde_derive::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::os::unix::io::AsRawFd; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// The directory where ostree stores transient per-deployment state. +/// This is currently semi-private to ostree; we should add an API to +/// access it. +const OSTREE_RUNSTATE_DIR: &str = "/run/ostree/deployment-state"; +/// Filename we use for serialized state, stored in the above directory. +const LIVEFS_STATE_NAME: &str = "rpmostree-livefs-state.json"; + +/// The model for livefs state. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +struct LiveFsState { + /// The OSTree commit that the running root filesystem is using, + /// as distinct from the one it was booted with. + commit: Option, + /// Set when a livefs operation is in progress; if the process + /// is interrupted, some files from this commit may exist + /// on disk but in an incomplete state. + inprogress_commit: Option, +} + +/// Get the transient state directory for a deployment; TODO +/// upstream this into libostree. +fn get_runstate_dir(deploy: &ostree::Deployment) -> PathBuf { + format!( + "{}/{}.{}", + OSTREE_RUNSTATE_DIR, + deploy.get_csum().expect("csum"), + deploy.get_deployserial() + ) + .into() +} + +/// Get the livefs state +fn get_livefs_state(deploy: &ostree::Deployment) -> Result> { + let root = openat::Dir::open("/")?; + if let Some(f) = root.open_file_optional(&get_runstate_dir(deploy).join(LIVEFS_STATE_NAME))? { + let s: LiveFsState = serde_json::from_reader(std::io::BufReader::new(f))?; + Ok(Some(s)) + } else { + Ok(None) + } +} + +/// Write new livefs state +fn write_livefs_state(deploy: &ostree::Deployment, state: &LiveFsState) -> Result<()> { + let rundir = get_runstate_dir(deploy); + let rundir = openat::Dir::open(&rundir)?; + rundir.write_file_with(LIVEFS_STATE_NAME, 0o644, |w| -> Result<_> { + Ok(serde_json::to_writer(w, state)?) + })?; + Ok(()) +} + +/// Get the relative parent directory of a path +fn relpath_dir(p: &Path) -> Result<&Path> { + Ok(p.strip_prefix("/")?.parent().expect("parent")) +} + +/// Return a path buffer we can provide to libostree for checkout +fn subpath(diff: &crate::ostree_diff::FileTreeDiff, p: &Path) -> Option { + if let Some(ref d) = diff.subdir { + let p = p.strip_prefix("/").expect("prefix"); + Some(Path::new(d).join(p)) + } else { + Some(p.to_path_buf()) + } +} + +/// Given a diff, apply it to the target directory, which should be a checkout of the source commit. +fn apply_diff( + repo: &ostree::Repo, + diff: &crate::ostree_diff::FileTreeDiff, + commit: &str, + destdir: &openat::Dir, +) -> Result<()> { + if !diff.changed_dirs.is_empty() { + anyhow::bail!("Changed directories are not supported yet"); + } + let cancellable = gio::NONE_CANCELLABLE; + // This applies to all added/changed content, we just + // overwrite `subpath` in each run. + let mut opts = ostree::RepoCheckoutAtOptions { + overwrite_mode: ostree::RepoCheckoutOverwriteMode::UnionFiles, + force_copy: true, + ..Default::default() + }; + // Check out new directories and files + for d in diff.added_dirs.iter().map(Path::new) { + opts.subpath = subpath(diff, &d); + let t = d.strip_prefix("/")?; + repo.checkout_at(Some(&opts), destdir.as_raw_fd(), t, commit, cancellable) + .with_context(|| format!("Checking out added dir {:?}", d))?; + } + for d in diff.added_files.iter().map(Path::new) { + opts.subpath = subpath(diff, &d); + repo.checkout_at( + Some(&opts), + destdir.as_raw_fd(), + relpath_dir(d)?, + commit, + cancellable, + ) + .with_context(|| format!("Checking out added file {:?}", d))?; + } + // Changed files in existing directories + for d in diff.changed_files.iter().map(Path::new) { + opts.subpath = subpath(diff, &d); + repo.checkout_at( + Some(&opts), + destdir.as_raw_fd(), + relpath_dir(d)?, + commit, + cancellable, + ) + .with_context(|| format!("Checking out changed file {:?}", d))?; + } + assert!(diff.changed_dirs.is_empty()); + + // Finally clean up removed directories and files together. We use + // rayon here just because we can. + diff.removed_files + .par_iter() + .chain(diff.removed_dirs.par_iter()) + .try_for_each(|d| -> Result<()> { + let d = d.strip_prefix("/").expect("prefix"); + destdir + .remove_all(d) + .with_context(|| format!("Failed to remove {:?}", d))?; + Ok(()) + })?; + + Ok(()) +} + +/// Special handling for `/etc` - we currently just add new default files/directories. +/// We don't try to delete anything yet, because doing so could mess up the actual +/// `/etc` merge on reboot between the real deployment. Much of the logic here +/// is similar to what libostree core does for `/etc` on upgrades. If we ever +/// push livefs down into libostree, this logic could be shared. +fn update_etc( + repo: &ostree::Repo, + diff: &crate::ostree_diff::FileTreeDiff, + sepolicy: &ostree::SePolicy, + commit: &str, + destdir: &openat::Dir, +) -> Result<()> { + let expected_subpath = "/usr"; + // We stripped both /usr and /etc, we need to readd them both + // for the checkout. + fn filtermap_paths(s: &String) -> Option<(Option, &Path)> { + s.strip_prefix("/etc").map(|p| { + let p = Path::new(p).strip_prefix("/").expect("prefix"); + (Some(Path::new("/usr/etc").join(p)), p) + }) + } + // For some reason in Rust the `parent()` of `foo` is just the empty string `""`; we + // need it to be the self-link `.` path. + fn canonicalized_parent(p: &Path) -> &Path { + match p.parent() { + Some(p) if p.as_os_str().len() == 0 => Path::new("."), + Some(p) => p, + None => Path::new("."), + } + } + + // The generic apply_diff() above in theory could work anywhere. + // But this code is only designed for /etc. + assert_eq!(diff.subdir.as_ref().expect("subpath"), expected_subpath); + if !diff.changed_dirs.is_empty() { + anyhow::bail!("Changed directories are not supported yet"); + } + + let cancellable = gio::NONE_CANCELLABLE; + // This applies to all added/changed content, we just + // overwrite `subpath` in each run. + let mut opts = ostree::RepoCheckoutAtOptions { + overwrite_mode: ostree::RepoCheckoutOverwriteMode::UnionFiles, + force_copy: true, + ..Default::default() + }; + // The labels for /etc and /usr/etc may differ; ensure that we label + // the files with the /etc target, even though we're checking out + // from /usr/etc. This is the same as what libostree does. + if sepolicy.get_name().is_some() { + opts.sepolicy = Some(sepolicy.clone()); + } + // Added directories and files + for (subpath, target) in diff.added_dirs.iter().filter_map(filtermap_paths) { + opts.subpath = subpath; + repo.checkout_at( + Some(&opts), + destdir.as_raw_fd(), + target, + commit, + cancellable, + ) + .with_context(|| format!("Checking out added /etc dir {:?}", (&opts.subpath, target)))?; + } + for (subpath, target) in diff.added_files.iter().filter_map(filtermap_paths) { + opts.subpath = subpath; + repo.checkout_at( + Some(&opts), + destdir.as_raw_fd(), + canonicalized_parent(target), + commit, + cancellable, + ) + .with_context(|| format!("Checking out added /etc file {:?}", (&opts.subpath, target)))?; + } + // Now changed files + for (subpath, target) in diff.changed_files.iter().filter_map(filtermap_paths) { + opts.subpath = subpath; + repo.checkout_at( + Some(&opts), + destdir.as_raw_fd(), + canonicalized_parent(target), + commit, + cancellable, + ) + .with_context(|| { + format!( + "Checking out changed /etc file {:?}", + (&opts.subpath, target) + ) + })?; + } + assert!(diff.changed_dirs.is_empty()); + + // And finally clean up removed files and directories. + diff.removed_files + .par_iter() + .chain(diff.removed_dirs.par_iter()) + .filter_map(filtermap_paths) + .try_for_each(|(_, target)| -> Result<()> { + destdir + .remove_all(target) + .with_context(|| format!("Failed to remove {:?}", target))?; + Ok(()) + })?; + + Ok(()) +} + +// Our main process uses MountFlags=slave set up by systemd; +// this is what allows us to e.g. remount /sysroot writable +// just inside our mount namespace. However, in this case +// we actually need to escape our mount namespace and affect +// the "main" mount namespace so that other processes will +// see the overlayfs. +fn unlock_transient(sysroot: &ostree::Sysroot) -> Result<()> { + // Temporarily drop the lock + sysroot.unlock(); + let status = Command::new("systemd-run") + .args(&[ + "-u", + "rpm-ostree-unlock", + "--wait", + "--", + "ostree", + "admin", + "unlock", + "--transient", + ]) + .status(); + sysroot.lock()?; + let status = status?; + if !status.success() { + bail!("Failed to unlock --transient"); + } + Ok(()) +} + +/// Run `systemd-tmpfiles` via `systemd-run` so we escape our mount namespace. +/// This allows our `ProtectHome=` in the unit file to work. +fn rerun_tmpfiles() -> Result<()> { + for prefix in &["/run", "/var"] { + let status = Command::new("systemd-run") + .args(&[ + "-u", + "rpm-ostree-tmpfiles", + "--wait", + "--", + "systemd-tmpfiles", + "--create", + "--prefix", + prefix, + ]) + .status()?; + if !status.success() { + bail!("Failed to invoke systemd-tmpfiles"); + } + } + Ok(()) +} + +/// Implementation of `rpm-ostree ex livefs`. +fn livefs(sysroot: &ostree::Sysroot, target: Option<&str>) -> Result<()> { + let repo = sysroot.repo().expect("repo"); + let repo = &repo; + + let booted = if let Some(b) = sysroot.get_booted_deployment() { + b + } else { + bail!("Not booted into an OSTree system") + }; + let osname = booted.get_osname().expect("osname"); + let booted_commit = booted.get_csum().expect("csum"); + let booted_commit = booted_commit.as_str(); + + let target_commit = if let Some(t) = target { + Cow::Borrowed(t) + } else { + match crate::ostree_utils::sysroot_query_deployments_for(sysroot, osname.as_str()) { + (Some(pending), _) => { + let pending_commit = pending.get_csum().expect("csum"); + let pending_commit = pending_commit.as_str(); + Cow::Owned(pending_commit.to_string()) + } + (None, _) => { + anyhow::bail!("No target commit specified and no pending deployment"); + } + } + }; + + let state = get_livefs_state(&booted)?; + if state.is_none() { + match booted.get_unlocked() { + DeploymentUnlockedState::None => { + unlock_transient(sysroot)?; + } + DeploymentUnlockedState::Transient | DeploymentUnlockedState::Development => {} + s => { + bail!("livefs is incompatible with unlock state: {}", s); + } + }; + } else { + match booted.get_unlocked() { + DeploymentUnlockedState::Transient | DeploymentUnlockedState::Development => {} + s => { + bail!("deployment not unlocked, is in state: {}", s); + } + }; + } + // In the transient mode, remount writable - this affects just the rpm-ostreed + // mount namespace. In the future it'd be nicer to run transactions as subprocesses + // so we don't lift the writable protection for the main rpm-ostree process. + if statvfs::statvfs("/usr")? + .flags() + .contains(statvfs::FsFlags::ST_RDONLY) + { + use nix::mount::MsFlags; + let none: Option<&str> = None; + nix::mount::mount( + none, + "/usr", + none, + MsFlags::MS_REMOUNT | MsFlags::MS_SILENT, + none, + )?; + } + + if let Some(ref state) = state { + if let Some(ref inprogress) = state.inprogress_commit { + if inprogress.as_str() != target_commit { + bail!( + "Previously interrupted while targeting commit {}, cannot change target to {}", + inprogress, + target_commit + ) + } + } + } + + let source_commit = state + .as_ref() + .map(|s| s.commit.as_ref().map(|s| s.as_str())) + .flatten() + .unwrap_or(booted_commit); + let diff = crate::ostree_diff::diff(repo, source_commit, &target_commit, Some("/usr")) + .context("Failed computing diff")?; + + let mut state = state.unwrap_or_default(); + + let rootfs_dfd = openat::Dir::open("/")?; + let sepolicy = ostree::SePolicy::new_at(rootfs_dfd.as_raw_fd(), gio::NONE_CANCELLABLE)?; + + // Record that we're targeting this commit + state.inprogress_commit = Some(target_commit.to_string()); + write_livefs_state(&booted, &state)?; + + // The heart of things: updating the overlayfs on /usr + apply_diff(repo, &diff, &target_commit, &openat::Dir::open("/usr")?)?; + + // The other important bits are /etc and /var + update_etc( + repo, + &diff, + &sepolicy, + &target_commit, + &openat::Dir::open("/etc")?, + )?; + rerun_tmpfiles()?; + + // Success! Update the recorded state. + state.commit = Some(target_commit.to_string()); + state.inprogress_commit = None; + write_livefs_state(&booted, &state)?; + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_subpath() { + let d = crate::ostree_diff::FileTreeDiff { + subdir: Some("/usr".to_string()), + ..Default::default() + }; + let s = subpath(&d, Path::new("/foo")); + assert_eq!(s.as_ref().map(|s| s.as_path()), Some(Path::new("/usr/foo"))); + } +} + +mod ffi { + use super::*; + use glib; + use glib::translate::*; + use glib::GString; + use glib_sys; + use libc; + + use crate::ffiutil::*; + + #[no_mangle] + pub extern "C" fn ror_livefs_get_state( + sysroot: *mut ostree_sys::OstreeSysroot, + deployment: *mut ostree_sys::OstreeDeployment, + out_inprogress: *mut *mut libc::c_char, + out_replaced: *mut *mut libc::c_char, + gerror: *mut *mut glib_sys::GError, + ) -> libc::c_int { + let _sysroot: ostree::Sysroot = unsafe { from_glib_none(sysroot) }; + let deployment: ostree::Deployment = unsafe { from_glib_none(deployment) }; + match get_livefs_state(&deployment) { + Ok(Some(state)) => { + unsafe { + if let Some(c) = state.inprogress_commit { + *out_inprogress = c.to_glib_full(); + } + if let Some(c) = state.commit { + *out_replaced = c.to_glib_full(); + } + } + 1 + } + Ok(None) => 1, + Err(ref e) => { + error_to_glib(e, gerror); + 0 + } + } + } + + #[no_mangle] + pub extern "C" fn ror_transaction_livefs( + sysroot: *mut ostree_sys::OstreeSysroot, + target: *const libc::c_char, + gerror: *mut *mut glib_sys::GError, + ) -> libc::c_int { + let sysroot: ostree::Sysroot = unsafe { from_glib_none(sysroot) }; + let target: Borrowed> = unsafe { from_glib_borrow(target) }; + // The reference hole goes deep + let target = target.as_ref().as_ref().map(|s| s.as_str()); + int_glib_error(livefs(&sysroot, target), gerror) + } +} +pub use self::ffi::*; diff --git a/rust/src/ostree_diff.rs b/rust/src/ostree_diff.rs new file mode 100644 index 00000000..5245cde9 --- /dev/null +++ b/rust/src/ostree_diff.rs @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2020 Red Hat, Inc. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + */ + +use anyhow::{Context, Result}; +use gio::prelude::*; +use ostree::RepoFileExt; +use serde_derive::{Deserialize, Serialize}; +use std::collections::BTreeSet; + +/// Like `g_file_query_info()`, but return None if the target doesn't exist. +fn query_info_optional( + f: &gio::File, + queryattrs: &str, + queryflags: gio::FileQueryInfoFlags, +) -> Result> { + let cancellable = gio::NONE_CANCELLABLE; + match f.query_info(queryattrs, queryflags, cancellable) { + Ok(i) => Ok(Some(i)), + Err(e) => { + if let Some(ref e2) = e.kind::() { + match e2 { + gio::IOErrorEnum::NotFound => Ok(None), + _ => return Err(e.into()), + } + } else { + return Err(e.into()); + } + } + } +} + +pub(crate) type FileSet = BTreeSet; + +/// Diff between two ostree commits. +#[derive(Debug, Default, Serialize, Deserialize)] +pub(crate) struct FileTreeDiff { + /// The prefix passed for diffing, e.g. /usr + pub(crate) subdir: Option, + /// Files that are new in an existing directory + pub(crate) added_files: FileSet, + /// New directories + pub(crate) added_dirs: FileSet, + /// Files removed + pub(crate) removed_files: FileSet, + /// Directories removed (recursively) + pub(crate) removed_dirs: FileSet, + /// Files that changed (in any way, metadata or content) + pub(crate) changed_files: FileSet, + /// Directories that changed mode/permissions + pub(crate) changed_dirs: FileSet, +} + +fn diff_recurse( + prefix: &str, + diff: &mut FileTreeDiff, + from: &ostree::RepoFile, + to: &ostree::RepoFile, +) -> Result<()> { + let cancellable = gio::NONE_CANCELLABLE; + let queryattrs = "standard::name,standard::type"; + let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS; + let from_iter = from.enumerate_children(queryattrs, queryflags, cancellable)?; + + // Iterate over the source (from) directory, and compare with the + // target (to) directory. This generates removals and changes. + while let Some(from_info) = from_iter.next_file(cancellable)? { + let from_child = from_iter.get_child(&from_info).expect("file"); + let name = from_info.get_name().expect("name"); + let name = name.to_str().expect("UTF-8 ostree name"); + let path = format!("{}{}", prefix, name); + let to_child = to.get_child(&name).expect("child"); + let to_info = query_info_optional(&to_child, queryattrs, queryflags) + .context("querying optional to")?; + let is_dir = match from_info.get_file_type() { + gio::FileType::Directory => true, + _ => false, + }; + if to_info.is_some() { + let to_child = to_child.downcast::().expect("downcast"); + to_child.ensure_resolved()?; + let from_child = from_child.downcast::().expect("downcast"); + from_child.ensure_resolved()?; + + if is_dir { + let from_contents_checksum = + from_child.tree_get_contents_checksum().expect("checksum"); + let to_contents_checksum = to_child.tree_get_contents_checksum().expect("checksum"); + if from_contents_checksum != to_contents_checksum { + let subpath = format!("{}/", path); + diff_recurse(&subpath, diff, &from_child, &to_child)?; + } + let from_meta_checksum = from_child.tree_get_metadata_checksum().expect("checksum"); + let to_meta_checksum = to_child.tree_get_metadata_checksum().expect("checksum"); + if from_meta_checksum != to_meta_checksum { + diff.changed_dirs.insert(path); + } + } else { + let from_checksum = from_child.get_checksum().expect("checksum"); + let to_checksum = to_child.get_checksum().expect("checksum"); + if from_checksum != to_checksum { + diff.changed_files.insert(path); + } + } + } else { + if is_dir { + diff.removed_dirs.insert(path); + } else { + diff.removed_files.insert(path); + } + } + } + // Iterate over the target (to) directory, and find any + // files/directories which were not present in the source. + let to_iter = to.enumerate_children(queryattrs, queryflags, cancellable)?; + while let Some(to_info) = to_iter.next_file(cancellable)? { + let name = to_info.get_name().expect("name"); + let name = name.to_str().expect("UTF-8 ostree name"); + let path = format!("{}{}", prefix, name); + let from_child = from.get_child(name).expect("child"); + let from_info = query_info_optional(&from_child, queryattrs, queryflags) + .context("querying optional from")?; + if from_info.is_some() { + continue; + } + let is_dir = match to_info.get_file_type() { + gio::FileType::Directory => true, + _ => false, + }; + if is_dir { + diff.added_dirs.insert(path); + } else { + diff.added_files.insert(path); + } + } + Ok(()) +} + +/// Given two ostree commits, compute the diff between them. +pub(crate) fn diff>( + repo: &ostree::Repo, + from: &str, + to: &str, + subdir: Option

, +) -> Result { + let subdir = subdir.as_ref(); + let subdir = subdir.map(|s| s.as_ref()); + let (fromroot, _) = repo.read_commit(from, gio::NONE_CANCELLABLE)?; + let (toroot, _) = repo.read_commit(to, gio::NONE_CANCELLABLE)?; + let (fromroot, toroot) = if let Some(subdir) = subdir { + ( + fromroot.resolve_relative_path(subdir).expect("path"), + toroot.resolve_relative_path(subdir).expect("path"), + ) + } else { + (fromroot, toroot) + }; + let fromroot = fromroot.downcast::().expect("downcast"); + fromroot.ensure_resolved()?; + let toroot = toroot.downcast::().expect("downcast"); + toroot.ensure_resolved()?; + let mut diff = FileTreeDiff { + subdir: subdir.map(|s| s.to_string()), + ..Default::default() + }; + diff_recurse("/", &mut diff, &fromroot, &toroot)?; + Ok(diff) +} diff --git a/rust/src/ostree_utils.rs b/rust/src/ostree_utils.rs new file mode 100644 index 00000000..e16ee643 --- /dev/null +++ b/rust/src/ostree_utils.rs @@ -0,0 +1,20 @@ +//! Utility helpers or workarounds for incorrectly bound things in ostree-rs +use glib::translate::*; +use std::ptr; + +pub(crate) fn sysroot_query_deployments_for( + sysroot: &ostree::Sysroot, + osname: &str, +) -> (Option, Option) { + unsafe { + let mut out_pending = ptr::null_mut(); + let mut out_rollback = ptr::null_mut(); + ostree_sys::ostree_sysroot_query_deployments_for( + sysroot.to_glib_none().0, + osname.to_glib_none().0, + &mut out_pending, + &mut out_rollback, + ); + (from_glib_full(out_pending), from_glib_full(out_rollback)) + } +} diff --git a/rust/src/syscore.rs b/rust/src/syscore.rs deleted file mode 100644 index b8967827..00000000 --- a/rust/src/syscore.rs +++ /dev/null @@ -1,73 +0,0 @@ -use self::ffi::RpmOstreeOrigin; -use std::ptr::NonNull; - -/// Reference to an `RpmOstreeOrigin`. -#[derive(Debug)] -pub(crate) struct OriginRef { - origin: NonNull, -} - -impl OriginRef { - /// Build a reference object from a C pointer. - fn from_ffi_ptr(oref: *mut RpmOstreeOrigin) -> Self { - Self { - origin: NonNull::new(oref).expect("NULL RpmOstreeOrigin"), - } - } - - /// Get `livefs` details for this deployment origin. - pub(crate) fn get_live_state<'o>(&'o self) -> OriginLiveState<'o> { - use crate::includes::rpmostree_origin_get_live_state; - use glib::translate::from_glib_full; - - let mut out_inprogress: *mut libc::c_char = std::ptr::null_mut(); - let mut out_livereplaced: *mut libc::c_char = std::ptr::null_mut(); - unsafe { - rpmostree_origin_get_live_state( - self.origin.as_ptr(), - &mut out_inprogress, - &mut out_livereplaced, - ); - }; - let in_progress = unsafe { from_glib_full(out_inprogress) }; - let replaced = unsafe { from_glib_full(out_livereplaced) }; - - OriginLiveState { - _origin: self, - in_progress, - replaced, - } - } -} - -/// `livefs` state and details for a given deployment origin. -#[derive(Debug)] -pub(crate) struct OriginLiveState<'o> { - /// Underlying deployment origin. - _origin: &'o OriginRef, - /// Checksum for the in-progress livefs. - pub in_progress: Option, - /// Checksum for the underlying replaced commit. - pub replaced: Option, -} - -impl<'o> OriginLiveState<'o> { - /// Return whether the given deployment is live-modified. - pub(crate) fn is_live(self) -> bool { - self.in_progress.is_some() || self.replaced.is_some() - } -} - -pub mod ffi { - use super::OriginRef; - - /// Opaque type for C interop: RpmOstreeOrigin. - pub enum RpmOstreeOrigin {} - - #[no_mangle] - pub extern "C" fn ror_origin_is_live(origin_ptr: *mut RpmOstreeOrigin) -> libc::c_int { - let origin = OriginRef::from_ffi_ptr(origin_ptr); - let livestate = origin.get_live_state(); - livestate.is_live().into() - } -} diff --git a/src/app/rpmostree-builtin-livefs.c b/src/app/rpmostree-builtin-livefs.c index 1d5f1639..6fda9966 100644 --- a/src/app/rpmostree-builtin-livefs.c +++ b/src/app/rpmostree-builtin-livefs.c @@ -29,15 +29,10 @@ #include -static gboolean opt_dry_run; -static gboolean opt_replace; -static gboolean opt_consented; +static char *opt_target; static GOptionEntry option_entries[] = { - { "dry-run", 'n', 0, G_OPTION_ARG_NONE, &opt_dry_run, "Only perform analysis, do not make changes", NULL }, - { "i-like-danger", 0, 0, G_OPTION_ARG_NONE, &opt_consented, "Consent to the dangers that livefs may pose", NULL }, - /* Known broken with kernel updates; see https://github.com/projectatomic/rpm-ostree/issues/1495 */ - { "dangerous-do-not-use-replace", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &opt_replace, "Completely replace all files in /usr (known broken)", NULL }, + { "target", 0, 0, G_OPTION_ARG_NONE, &opt_target, "Target provided commit instead of pending deployment", NULL }, { NULL } }; @@ -47,8 +42,8 @@ get_args_variant (void) GVariantDict dict; g_variant_dict_init (&dict, NULL); - g_variant_dict_insert (&dict, "dry-run", "b", opt_dry_run); - g_variant_dict_insert (&dict, "replace", "b", opt_replace); + if (opt_target) + g_variant_dict_insert (&dict, "target", "s", opt_target); return g_variant_dict_end (&dict); } @@ -75,10 +70,6 @@ rpmostree_ex_builtin_livefs (int argc, error)) return FALSE; - if (!opt_consented) - return glnx_throw (error, "livefs is currently considered dangerous; " - "pass --i-like-danger to override"); - glnx_unref_object RPMOSTreeOS *os_proxy = NULL; glnx_unref_object RPMOSTreeOSExperimental *osexperimental_proxy = NULL; if (!rpmostree_load_os_proxies (sysroot_proxy, NULL, diff --git a/src/daemon/rpmostree-sysroot-core.c b/src/daemon/rpmostree-sysroot-core.c index cd8bb41d..3a2a9992 100644 --- a/src/daemon/rpmostree-sysroot-core.c +++ b/src/daemon/rpmostree-sysroot-core.c @@ -308,10 +308,31 @@ rpmostree_syscore_cleanup (OstreeSysroot *sysroot, cancellable, error)) return FALSE; + OstreeDeployment *booted = ostree_sysroot_get_booted_deployment (sysroot); + g_autofree char *live_inprogress = NULL; + g_autofree char *live_replaced = NULL; + if (booted) + { + if (!ror_livefs_get_state (sysroot, booted, &live_inprogress, &live_replaced, error)) + return FALSE; + } + /* And do a prune */ guint64 freed_space; gint n_objects_total, n_objects_pruned; { g_autoptr(GHashTable) reachable = ostree_repo_traverse_new_reachable (); + + /* We don't currently write refs for these since the content can be + * ephemeral; add them to the strong set */ + if (live_inprogress && + !ostree_repo_traverse_commit_union (repo, live_inprogress, 0, reachable, + cancellable, error)) + return FALSE; + if (live_replaced && + !ostree_repo_traverse_commit_union (repo, live_replaced, 0, reachable, + cancellable, error)) + return FALSE; + OstreeRepoPruneOptions opts = { OSTREE_REPO_PRUNE_FLAGS_REFS_ONLY, reachable }; if (!ostree_sysroot_cleanup_prune_repo (sysroot, &opts, &n_objects_total, &n_objects_pruned, &freed_space, @@ -351,6 +372,17 @@ rpmostree_syscore_get_origin_merge_deployment (OstreeSysroot *self, const char * return NULL; } +/* Return TRUE in *out_is_live if the target deployment has a live overlay */ +gboolean +rpmostree_syscore_livefs_query (OstreeSysroot *self, OstreeDeployment *deployment, gboolean *out_is_live, GError **error) +{ + g_autofree char *live_inprogress = NULL; + g_autofree char *live_replaced = NULL; + if (!ror_livefs_get_state (self, deployment, &live_inprogress, &live_replaced, error)) + return FALSE; + *out_is_live = (live_inprogress != NULL) || (live_replaced != NULL); + return TRUE; +} /* Copy of currently private _ostree_sysroot_bump_mtime() * until we decide to either formalize that, or have a method @@ -446,7 +478,7 @@ rpmostree_syscore_write_deployment (OstreeSysroot *sysroot, if (booted) { gboolean is_live; - if (!rpmostree_syscore_deployment_is_live (booted, &is_live, error)) + if (!rpmostree_syscore_livefs_query (sysroot, booted, &is_live, error)) return FALSE; if (is_live) flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_ROLLBACK; @@ -463,33 +495,3 @@ rpmostree_syscore_write_deployment (OstreeSysroot *sysroot, return TRUE; } - -/* Load the checksums that describe the "livefs" state of the given - * deployment. - */ -gboolean -rpmostree_syscore_deployment_get_live (OstreeDeployment *deployment, - char **out_inprogress_checksum, - char **out_livereplaced_checksum, - GError **error) -{ - g_autoptr(RpmOstreeOrigin) origin = rpmostree_origin_parse_deployment (deployment, error); - if (!origin) - return FALSE; - rpmostree_origin_get_live_state (origin, out_inprogress_checksum, out_livereplaced_checksum); - return TRUE; -} - -/* Set @out_is_live to %TRUE if the deployment is live-modified */ -gboolean -rpmostree_syscore_deployment_is_live (OstreeDeployment *deployment, - gboolean *out_is_live, - GError **error) -{ - g_autoptr(RpmOstreeOrigin) origin = rpmostree_origin_parse_deployment (deployment, error); - if (!origin) - return FALSE; - - *out_is_live = ror_origin_is_live(origin); - return TRUE; -} diff --git a/src/daemon/rpmostree-sysroot-core.h b/src/daemon/rpmostree-sysroot-core.h index 119335a4..6d5db54c 100644 --- a/src/daemon/rpmostree-sysroot-core.h +++ b/src/daemon/rpmostree-sysroot-core.h @@ -45,17 +45,7 @@ OstreeDeployment *rpmostree_syscore_get_origin_merge_deployment (OstreeSysroot * gboolean rpmostree_syscore_bump_mtime (OstreeSysroot *self, GError **error); -#define RPMOSTREE_LIVE_INPROGRESS_XATTR "user.rpmostree-live-inprogress" -#define RPMOSTREE_LIVE_REPLACED_XATTR "user.rpmostree-live-replaced" - -gboolean rpmostree_syscore_deployment_get_live (OstreeDeployment *deployment, - char **out_inprogress_checksum, - char **out_livereplaced_checksum, - GError **error); - -gboolean rpmostree_syscore_deployment_is_live (OstreeDeployment *deployment, - gboolean *out_is_live, - GError **error); +gboolean rpmostree_syscore_livefs_query (OstreeSysroot *self, OstreeDeployment *deployment, gboolean *out_is_live, GError **error); GPtrArray *rpmostree_syscore_filter_deployments (OstreeSysroot *sysroot, const char *osname, diff --git a/src/daemon/rpmostree-sysroot-upgrader.c b/src/daemon/rpmostree-sysroot-upgrader.c index 9d0b2d27..0d860d88 100644 --- a/src/daemon/rpmostree-sysroot-upgrader.c +++ b/src/daemon/rpmostree-sysroot-upgrader.c @@ -593,7 +593,7 @@ try_load_base_rsack_from_pending (RpmOstreeSysrootUpgrader *self, GError **error) { gboolean is_live; - if (!rpmostree_syscore_deployment_is_live (self->origin_merge_deployment, &is_live, error)) + if (!rpmostree_syscore_livefs_query (self->sysroot, self->origin_merge_deployment, &is_live, error)) return FALSE; /* livefs invalidates the deployment */ diff --git a/src/daemon/rpmostreed-deployment-utils.c b/src/daemon/rpmostreed-deployment-utils.c index 25bd5d33..24663f94 100644 --- a/src/daemon/rpmostreed-deployment-utils.c +++ b/src/daemon/rpmostreed-deployment-utils.c @@ -261,6 +261,8 @@ rpmostreed_deployment_generate_variant (OstreeSysroot *sysroot, if (!origin) return NULL; + const gboolean is_booted = g_strcmp0 (booted_id, id) == 0; + RpmOstreeRefspecType refspec_type; g_autofree char *refspec = rpmostree_origin_get_full_refspec (origin, &refspec_type); @@ -364,16 +366,19 @@ rpmostreed_deployment_generate_variant (OstreeSysroot *sysroot, break; } - g_autofree char *live_inprogress = NULL; - g_autofree char *live_replaced = NULL; - if (!rpmostree_syscore_deployment_get_live (deployment, &live_inprogress, - &live_replaced, error)) - return NULL; - if (live_inprogress) - g_variant_dict_insert (&dict, "live-inprogress", "s", live_inprogress); - if (live_replaced) - g_variant_dict_insert (&dict, "live-replaced", "s", live_replaced); + if (is_booted) + { + g_autofree char *live_inprogress = NULL; + g_autofree char *live_replaced = NULL; + if (!ror_livefs_get_state (sysroot, deployment, &live_inprogress, &live_replaced, error)) + return FALSE; + + if (live_inprogress) + g_variant_dict_insert (&dict, "live-inprogress", "s", live_inprogress); + if (live_replaced) + g_variant_dict_insert (&dict, "live-replaced", "s", live_replaced); + } if (ostree_deployment_is_staged (deployment)) { @@ -416,7 +421,7 @@ rpmostreed_deployment_generate_variant (OstreeSysroot *sysroot, rpmostree_origin_get_initramfs_etc_files (origin)); if (booted_id != NULL) - g_variant_dict_insert (&dict, "booted", "b", g_strcmp0 (booted_id, id) == 0); + g_variant_dict_insert (&dict, "booted", "b", is_booted); return g_variant_dict_end (&dict); } diff --git a/src/daemon/rpmostreed-os-experimental.c b/src/daemon/rpmostreed-os-experimental.c index 608369fa..68952ace 100644 --- a/src/daemon/rpmostreed-os-experimental.c +++ b/src/daemon/rpmostreed-os-experimental.c @@ -115,23 +115,6 @@ osexperimental_handle_moo (RPMOSTreeOSExperimental *interface, rpmostree_osexperimental_complete_moo (interface, invocation, result); return TRUE; } -static RpmOstreeTransactionLiveFsFlags -livefs_flags_from_options (GVariant *options) -{ - RpmOstreeTransactionLiveFsFlags ret = 0; - GVariantDict options_dict; - gboolean opt = FALSE; - - g_variant_dict_init (&options_dict, options); - if (g_variant_dict_lookup (&options_dict, "dry-run", "b", &opt) && opt) - ret |= RPMOSTREE_TRANSACTION_LIVEFS_FLAG_DRY_RUN; - if (g_variant_dict_lookup (&options_dict, "replace", "b", &opt) && opt) - ret |= RPMOSTREE_TRANSACTION_LIVEFS_FLAG_REPLACE; - - g_variant_dict_clear (&options_dict); - - return ret; -} static gboolean osexperimental_handle_live_fs (RPMOSTreeOSExperimental *interface, @@ -159,7 +142,7 @@ osexperimental_handle_live_fs (RPMOSTreeOSExperimental *interface, transaction = rpmostreed_transaction_new_livefs (invocation, ot_sysroot, - livefs_flags_from_options (arg_options), + arg_options, cancellable, &local_error); if (transaction == NULL) diff --git a/src/daemon/rpmostreed-transaction-livefs.c b/src/daemon/rpmostreed-transaction-livefs.c index b0322981..94fb0562 100644 --- a/src/daemon/rpmostreed-transaction-livefs.c +++ b/src/daemon/rpmostreed-transaction-livefs.c @@ -36,12 +36,9 @@ #include "rpmostree-core.h" #include "rpmostreed-utils.h" -#define RPMOSTREE_MESSAGE_LIVEFS_BEGIN SD_ID128_MAKE(30,60,1f,0b,bb,fe,4c,bd,a7,87,23,53,a2,ed,75,81) -#define RPMOSTREE_MESSAGE_LIVEFS_END SD_ID128_MAKE(d6,8a,b4,d9,d1,32,4a,32,8f,f8,c6,24,1c,6e,b3,c3) - typedef struct { RpmostreedTransaction parent; - RpmOstreeTransactionLiveFsFlags flags; + GVariant *options; } LiveFsTransaction; typedef RpmostreedTransactionClass LiveFsTransactionClass; @@ -58,839 +55,11 @@ livefs_transaction_finalize (GObject *object) G_GNUC_UNUSED LiveFsTransaction *self; self = (LiveFsTransaction *) object; + g_variant_unref (self->options); G_OBJECT_CLASS (livefs_transaction_parent_class)->finalize (object); } -typedef enum { - COMMIT_DIFF_FLAGS_ETC = (1<< 0), /* Change in /usr/etc */ - COMMIT_DIFF_FLAGS_BOOT = (1<< 1), /* Change in /boot */ - COMMIT_DIFF_FLAGS_ROOTFS = (1 << 2), /* Change in / */ - COMMIT_DIFF_FLAGS_REPLACEMENT = (1 << 3) /* Files in /usr were replaced */ -} CommitDiffFlags; - -typedef struct { - guint refcount; - CommitDiffFlags flags; - guint n_usretc; - guint n_tmpfilesd; - - char *from; - char *to; - - /* Files */ - GPtrArray *added; /* Set */ - GPtrArray *modified; /* Set */ - GPtrArray *removed; /* Set */ - - /* Package view */ - GPtrArray *removed_pkgs; - GPtrArray *added_pkgs; - GPtrArray *modified_pkgs_old; - GPtrArray *modified_pkgs_new; -} CommitDiff; - -static void -commit_diff_unref (CommitDiff *diff) -{ - diff->refcount--; - if (diff->refcount > 0) - return; - g_free (diff->from); - g_free (diff->to); - g_clear_pointer (&diff->added, g_ptr_array_unref); - g_clear_pointer (&diff->modified, g_ptr_array_unref); - g_clear_pointer (&diff->removed, g_ptr_array_unref); - g_clear_pointer (&diff->removed_pkgs, g_ptr_array_unref); - g_clear_pointer (&diff->added_pkgs, g_ptr_array_unref); - g_clear_pointer (&diff->modified_pkgs_old, g_ptr_array_unref); - g_clear_pointer (&diff->modified_pkgs_new, g_ptr_array_unref); -} -G_DEFINE_AUTOPTR_CLEANUP_FUNC(CommitDiff, commit_diff_unref); - -static gboolean -path_is_boot (const char *path) -{ - return g_str_has_prefix (path, "/boot/") || - g_str_has_prefix (path, "/usr/lib/ostree-boot/"); -} - -static gboolean -path_is_usretc (const char *path) -{ - return g_str_has_prefix (path, "/usr/etc/"); -} - -static gboolean -path_is_rpmdb (const char *path) -{ - return g_str_has_prefix (path, "/" RPMOSTREE_RPMDB_LOCATION "/"); -} - -static gboolean -path_is_rootfs (const char *path) -{ - return !g_str_has_prefix (path, "/usr/"); -} - -static gboolean -path_is_ignored_for_diff (const char *path) -{ - /* /proc SELinux labeling is broken, ignore it - * https://github.com/ostreedev/ostree/pull/768 - */ - return strcmp (path, "/proc") == 0; -} - -typedef enum { - FILE_DIFF_RESULT_KEEP, - FILE_DIFF_RESULT_OMIT, -} FileDiffResult; - -/* Given a file path, update @diff's global flags which track high level - * modifications, and return whether or not a change to this file should be - * ignored. - */ -static FileDiffResult -diff_one_path (CommitDiff *diff, - const char *path) -{ - if (path_is_ignored_for_diff (path) || - path_is_rpmdb (path)) - return FILE_DIFF_RESULT_OMIT; - else if (path_is_usretc (path)) - { - diff->flags |= COMMIT_DIFF_FLAGS_ETC; - diff->n_usretc++; - } - else if (g_str_has_prefix (path, "/usr/lib/tmpfiles.d")) - diff->n_tmpfilesd++; - else if (path_is_boot (path)) - diff->flags |= COMMIT_DIFF_FLAGS_BOOT; - else if (path_is_rootfs (path)) - diff->flags |= COMMIT_DIFF_FLAGS_ROOTFS; - return FILE_DIFF_RESULT_KEEP; -} - -static gboolean -copy_new_config_files (OstreeRepo *repo, - OstreeDeployment *merge_deployment, - int new_deployment_dfd, - OstreeSePolicy *sepolicy, - CommitDiff *diff, - GCancellable *cancellable, - GError **error) -{ - g_auto(RpmOstreeProgress) task = { 0, }; - rpmostree_output_task_begin (&task, "Copying new config files"); - - /* Initialize checkout options; we want to make copies, and don't replace any - * existing files. - */ - OstreeRepoCheckoutAtOptions etc_co_opts = { .force_copy = TRUE, - .overwrite_mode = OSTREE_REPO_CHECKOUT_OVERWRITE_ADD_FILES }; - /* Use SELinux policy if it's initialized */ - if (ostree_sepolicy_get_name (sepolicy) != NULL) - etc_co_opts.sepolicy = sepolicy; - - glnx_autofd int deployment_etc_dfd = -1; - if (!glnx_opendirat (new_deployment_dfd, "etc", TRUE, &deployment_etc_dfd, error)) - return FALSE; - - guint n_added = 0; - /* Avoid checking out added subdirs recursively */ - g_autoptr(GPtrArray) added_subdirs = g_ptr_array_new_with_free_func (g_free); - for (guint i = 0; i < diff->added->len; i++) - { - GFile *added_f = diff->added->pdata[i]; - const char *path = gs_file_get_path_cached (added_f); - if (!g_str_has_prefix (path, "/usr/etc/")) - continue; - const char *etc_path = path + strlen ("/usr"); - - etc_co_opts.subpath = path; - /* Strip off /usr for selinux labeling */ - etc_co_opts.sepolicy_prefix = etc_path; - - const char *sub_etc_relpath = etc_path + strlen ("/etc/"); - /* We keep track of added subdirectories and skip children of it, since - * both the diff and checkout are recursive, but we only need to checkout - * the directory, which will get all children. To do better I'd say we - * should add an option to ostree_repo_diff() to avoid recursing into - * changed subdirectories. But at the scale we're dealing with here the - * constants for this O(N²) algorithm are tiny. - */ - if (rpmostree_str_has_prefix_in_ptrarray (sub_etc_relpath, added_subdirs)) - continue; - - g_autoptr(GFileInfo) finfo = g_file_query_info (added_f, "standard::type", - G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, - cancellable, error); - if (!finfo) - return FALSE; - - /* If this is a directory, add it to our "added subdirs" set. See above. Add - * a trailing / to ensure we don't match other file prefixes. - */ - const gboolean is_dir = g_file_info_get_file_type (finfo) == G_FILE_TYPE_DIRECTORY; - if (is_dir) - g_ptr_array_add (added_subdirs, g_strconcat (sub_etc_relpath, "/", NULL)); - - /* And now, to deal with ostree semantics around subpath checkouts, - * we want '.' for files, otherwise get the real dir name. See also - * the comment in ostree-repo-checkout.c:checkout_tree_at(). - */ - /* First, get the destination parent dfd */ - glnx_autofd int dest_dfd = -1; - g_autofree char *dnbuf = g_strdup (sub_etc_relpath); - const char *dn = dirname (dnbuf); - if (!glnx_opendirat (deployment_etc_dfd, dn, TRUE, &dest_dfd, error)) - return FALSE; - const char *dest_path; - /* Is it a non-directory? OK, we check out into the parent directly */ - if (!is_dir) - { - dest_path = "."; - } - else - { - /* For directories, we need to match the target's name and hence - * create a new directory. */ - dest_path = glnx_basename (sub_etc_relpath); - } - - if (!ostree_repo_checkout_at (repo, &etc_co_opts, - dest_dfd, dest_path, - ostree_deployment_get_csum (merge_deployment), - cancellable, error)) - return g_prefix_error (error, "Copying %s: ", path), FALSE; - n_added++; - } - rpmostree_output_progress_end_msg (&task, "%u", n_added); - return TRUE; -} - -/* Generate a CommitDiff */ -static gboolean -analyze_commit_diff (OstreeRepo *repo, - const char *from_rev, - const char *to_rev, - CommitDiff **out_diff, - GCancellable *cancellable, - GError **error) -{ - g_autoptr(CommitDiff) diff = g_new0(CommitDiff, 1); - diff->refcount = 1; - - diff->from = g_strdup (from_rev); - diff->to = g_strdup (to_rev); - - /* Read the "from" and "to" commits */ - glnx_unref_object GFile *from_tree = NULL; - if (!ostree_repo_read_commit (repo, from_rev, &from_tree, NULL, - cancellable, error)) - return FALSE; - glnx_unref_object GFile *to_tree = NULL; - if (!ostree_repo_read_commit (repo, to_rev, &to_tree, NULL, - cancellable, error)) - return FALSE; - - /* Diff the two commits at the filesystem level */ - g_autoptr(GPtrArray) modified = g_ptr_array_new_with_free_func ((GDestroyNotify) ostree_diff_item_unref); - g_autoptr(GPtrArray) removed = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); - g_autoptr(GPtrArray) added = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); - if (!ostree_diff_dirs (0, from_tree, to_tree, modified, removed, added, - cancellable, error)) - return FALSE; - - /* We'll filter these arrays below. */ - diff->modified = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); - diff->removed = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); - diff->added = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); - - /* Analyze the differences */ - for (guint i = 0; i < modified->len; i++) - { - OstreeDiffItem *diffitem = modified->pdata[i]; - const char *path = gs_file_get_path_cached (diffitem->src); - - if (diff_one_path (diff, path) == FILE_DIFF_RESULT_KEEP) - g_ptr_array_add (diff->modified, g_object_ref (diffitem->src)); - } - - for (guint i = 0; i < removed->len; i++) - { - GFile *gfpath = removed->pdata[i]; - const char *path = gs_file_get_path_cached (gfpath); - - if (diff_one_path (diff, path) == FILE_DIFF_RESULT_KEEP) - g_ptr_array_add (diff->removed, g_object_ref (gfpath)); - } - - for (guint i = 0; i < added->len; i++) - { - GFile *added_f = added->pdata[i]; - const char *path = gs_file_get_path_cached (added_f); - - if (diff_one_path (diff, path) == FILE_DIFF_RESULT_KEEP) - g_ptr_array_add (diff->added, g_object_ref (added_f)); - } - - /* And gather the RPM level changes */ - if (!rpm_ostree_db_diff (repo, from_rev, to_rev, - &diff->removed_pkgs, &diff->added_pkgs, - &diff->modified_pkgs_old, &diff->modified_pkgs_new, - cancellable, error)) - return FALSE; - g_assert (diff->modified_pkgs_old->len == diff->modified_pkgs_new->len); - - *out_diff = g_steal_pointer (&diff); - return TRUE; -} - -static void -print_commit_diff (CommitDiff *diff) -{ - /* Print out the results of the two diffs */ - rpmostree_output_message ("Diff Analysis: %s => %s", diff->from, diff->to); - rpmostree_output_message ("Files: modified: %u removed: %u added: %u", - diff->modified->len, diff->removed->len, diff->added->len); - rpmostree_output_message ("Packages: modified: %u removed: %u added: %u", - diff->modified_pkgs_new->len, - diff->removed_pkgs->len, - diff->added_pkgs->len); - - if (diff->flags & COMMIT_DIFF_FLAGS_ETC) - { - rpmostree_output_message ("* Configuration changed in /etc"); - } - if (diff->flags & COMMIT_DIFF_FLAGS_ROOTFS) - { - rpmostree_output_message ("* Content outside of /usr and /etc is modified"); - } - if (diff->flags & COMMIT_DIFF_FLAGS_BOOT) - { - rpmostree_output_message ("* Kernel/initramfs changed"); - } -} - -/* We want to ensure the rollback deployment matches our booted checksum. If it - * doesn't, we'll push a new one, and GC the previous one(s). - */ -static OstreeDeployment * -get_rollback_deployment (OstreeSysroot *sysroot, - OstreeDeployment *booted) -{ - const char *booted_csum = ostree_deployment_get_csum (booted); - - g_autoptr(OstreeDeployment) rollback_deployment = NULL; - ostree_sysroot_query_deployments_for (sysroot, ostree_deployment_get_osname (booted), - NULL, &rollback_deployment); - /* If no rollback found, we're done */ - if (!rollback_deployment) - return NULL; - /* We found a rollback, but it needs to match our checksum */ - const char *csum = ostree_deployment_get_csum (rollback_deployment); - if (strcmp (csum, booted_csum) == 0) - return g_object_ref (rollback_deployment); - return NULL; -} - -static gboolean -prepare_rollback_deployment (OstreeSysroot *sysroot, - OstreeRepo *repo, - OstreeDeployment *booted_deployment, - GCancellable *cancellable, - GError **error) -{ - glnx_unref_object OstreeDeployment *new_deployment = NULL; - OstreeBootconfigParser *original_bootconfig = ostree_deployment_get_bootconfig (booted_deployment); - glnx_unref_object OstreeBootconfigParser *new_bootconfig = ostree_bootconfig_parser_clone (original_bootconfig); - - /* Ensure we have a clean slate */ - if (!ostree_sysroot_prepare_cleanup (sysroot, cancellable, error)) - return g_prefix_error (error, "Performing initial cleanup: "), FALSE; - - rpmostree_output_message ("Preparing new rollback matching currently booted deployment"); - - if (!ostree_sysroot_deploy_tree (sysroot, - ostree_deployment_get_osname (booted_deployment), - ostree_deployment_get_csum (booted_deployment), - ostree_deployment_get_origin (booted_deployment), - booted_deployment, - NULL, - &new_deployment, - cancellable, error)) - return FALSE; - - /* Inherit kernel arguments */ - ostree_deployment_set_bootconfig (new_deployment, new_bootconfig); - - if (!rpmostree_syscore_write_deployment (sysroot, new_deployment, booted_deployment, - TRUE, cancellable, error)) - return FALSE; - - return TRUE; -} - -static gboolean -checkout_add_usr (OstreeRepo *repo, - int deployment_dfd, - CommitDiff *diff, - const char *target_csum, - GCancellable *cancellable, - GError **error) -{ - OstreeRepoCheckoutAtOptions usr_checkout_opts = { .mode = OSTREE_REPO_CHECKOUT_MODE_NONE, - .overwrite_mode = OSTREE_REPO_CHECKOUT_OVERWRITE_ADD_FILES, - .no_copy_fallback = TRUE, - .subpath = "/usr" }; - - if (!ostree_repo_checkout_at (repo, &usr_checkout_opts, deployment_dfd, "usr", - target_csum, cancellable, error)) - return FALSE; - - return TRUE; -} - -/* Even when doing a pure "add" there are some things we need to actually - * replace: - * - * - rpm database - * - /usr/lib/{passwd,group} - * - * This function can swap in a new file/directory. - */ -static gboolean -replace_subpath (OstreeRepo *repo, - int deployment_dfd, - GLnxTmpDir *tmpdir, - const char *target_csum, - const char *subpath, - GCancellable *cancellable, - GError **error) -{ - /* The subpath for ostree_repo_checkout_at() must be absolute, but - * our real filesystem paths must be relative. - */ - g_assert_cmpint (*subpath, ==, '/'); - const char *relsubpath = subpath += strspn (subpath, "/"); - const char *bname = glnx_basename (relsubpath); - - /* See if it exists, if it does gather stat info; we need to handle - * directories differently from non-dirs. - */ - struct stat stbuf; - if (!glnx_fstatat_allow_noent (deployment_dfd, relsubpath, &stbuf, AT_SYMLINK_NOFOLLOW, error)) - return FALSE; - if (errno == ENOENT) - return TRUE; /* Do nothing if the path doesn't exist */ - - OstreeRepoCheckoutAtOptions replace_checkout_opts = { .mode = OSTREE_REPO_CHECKOUT_MODE_NONE, - .no_copy_fallback = TRUE, - .subpath = subpath }; - /* We need to differentiate nondirs vs dirs; for two reasons. First, - * see the /etc path code - ostree_repo_checkout_at() has - * some legacy bits around checking out individual files. - * - * Second, for directories we want RENAME_EXCHANGE if at all possible, but for - * non-dirs there's no reason not to just use plain old renameat() which will - * also work atomically even on old kernels (e.g. CentOS7). For some critical - * files like /usr/lib/passwd we really do want atomicity. - */ - const gboolean target_is_nondirectory = !S_ISDIR (stbuf.st_mode); - if (target_is_nondirectory) - { - if (!ostree_repo_checkout_at (repo, &replace_checkout_opts, tmpdir->fd, ".", - target_csum, cancellable, error)) - return FALSE; - if (!glnx_renameat (tmpdir->fd, bname, deployment_dfd, relsubpath, error)) - return FALSE; - } - else - { - if (!ostree_repo_checkout_at (repo, &replace_checkout_opts, tmpdir->fd, bname, - target_csum, cancellable, error)) - return FALSE; - - if (glnx_renameat2_exchange (tmpdir->fd, bname, deployment_dfd, relsubpath) < 0) - return glnx_throw_errno_prefix (error, "rename(..., RENAME_EXCHANGE) for %s", subpath); - /* And nuke the old one */ - if (!glnx_shutil_rm_rf_at (tmpdir->fd, bname, cancellable, error)) - return FALSE; - } - - return TRUE; -} - -/* The sledgehammer 🔨 approach. Because /usr is a mount point, we can't replace - * all of it. We could do a diff, but doing that precisely and quickly depends - * on https://github.com/ostreedev/ostree/issues/1224 - * - * In this approach, we iterate over and exchange just the subdirectories of - * /usr (not recursively, as exchanging the toplevels accomplishes that). Some - * issues here are processes that hold directory fds open will have those - * invalidated, even if they shouldn't otherwise be affected. Another issue is - * that if the kernel is too old to have `RENAME_EXCHANGE`, we'll have e.g. - * `/usr/bin` be temporarily broken. - * - * Yet another issue is that doing things all at once makes it much more likely - * that we'll e.g. replace code for a program before updating a shared library - * it depends on. There's really no fixing that problem in general, but we could - * minimize the race window in the same way package systems tend to do by doing - * the filesystem tree replacements in package reverse dependency order. - * - * On the other hand, this handles tricky cases like replacing a directory with - * a regfile or symlink. - * - * Probably what we really want is to have a lightweight replacement path that - * handles simple updates (e.g. 1-5 packages which just change file content). - */ -static gboolean -replace_usr (OstreeRepo *repo, - int deployment_dfd, - GLnxTmpDir *tmpdir, - CommitDiff *diff, - const char *target_csum, - GCancellable *cancellable, - GError **error) -{ - /* Grab a reference to the current /usr */ - glnx_autofd int deployment_usr_dfd = -1; - if (!glnx_opendirat (deployment_dfd, "usr", TRUE, &deployment_usr_dfd, error)) - return FALSE; - - /* Check out our new /usr */ - OstreeRepoCheckoutAtOptions usr_checkout_opts = { .mode = OSTREE_REPO_CHECKOUT_MODE_NONE, - .overwrite_mode = OSTREE_REPO_CHECKOUT_OVERWRITE_NONE, - .no_copy_fallback = TRUE, - .subpath = "/usr" }; - - if (!ostree_repo_checkout_at (repo, &usr_checkout_opts, tmpdir->fd, "usr", - target_csum, cancellable, error)) - return FALSE; - - /* Iterate over new /usr, see whether the context exists. If not, just - * rename(). If so, `RENAME_EXCHANGE`; the old content will be rm-rf'd since - * it will be moved to the tmpdir. - */ - g_autoptr(GHashTable) seen_new_children = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); - g_auto(GLnxDirFdIterator) dfd_iter = { FALSE, }; - if (!glnx_dirfd_iterator_init_at (tmpdir->fd, "usr", TRUE, &dfd_iter, error)) - return FALSE; - while (TRUE) - { - struct dirent *dent = NULL; - if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, cancellable, error)) - return FALSE; - if (dent == NULL) - break; - const char *name = dent->d_name; - /* Keep track of what entries are in the new /usr */ - g_hash_table_add (seen_new_children, g_strdup (name)); - if (!glnx_fstatat_allow_noent (deployment_usr_dfd, name, NULL, AT_SYMLINK_NOFOLLOW, error)) - return FALSE; - if (errno == ENOENT) - { - if (!glnx_renameat (dfd_iter.fd, name, deployment_usr_dfd, name, error)) - return FALSE; - } - else - { - if (glnx_renameat2_exchange (dfd_iter.fd, name, deployment_usr_dfd, name) < 0) - return glnx_throw_errno_prefix (error, "rename(..., RENAME_EXCHANGE) for %s", name); - } - } - - /* Iterate over the old /usr, and delete anything that isn't in the new dir */ - glnx_dirfd_iterator_clear (&dfd_iter); - if (!glnx_dirfd_iterator_init_at (deployment_usr_dfd, ".", TRUE, &dfd_iter, error)) - return FALSE; - while (TRUE) - { - struct dirent *dent = NULL; - if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, cancellable, error)) - return FALSE; - if (dent == NULL) - break; - const char *name = dent->d_name; - /* See whether this was in the new /usr */ - if (g_hash_table_contains (seen_new_children, name)) - continue; - /* If not, delete it */ - if (!glnx_shutil_rm_rf_at (dfd_iter.fd, name, cancellable, error)) - return FALSE; - } - - return TRUE; -} - -/* Update the origin for @booted with new livefs state */ -static gboolean -write_livefs_state (OstreeSysroot *sysroot, - OstreeDeployment *booted, - const char *live_inprogress, - const char *live, - GError **error) -{ - g_autoptr(RpmOstreeOrigin) new_origin = rpmostree_origin_parse_deployment (booted, error); - if (!new_origin) - return FALSE; - rpmostree_origin_set_live_state (new_origin, live_inprogress, live); - g_autoptr(GKeyFile) kf = rpmostree_origin_dup_keyfile (new_origin); - if (!ostree_sysroot_write_origin_file (sysroot, booted, kf, NULL, error)) - return FALSE; - return TRUE; -} - -static gboolean -livefs_transaction_execute_inner (LiveFsTransaction *self, - OstreeSysroot *sysroot, - GCancellable *cancellable, - GError **error) -{ - /* Initial setup - load sysroot, repo, and booted deployment */ - glnx_unref_object OstreeRepo *repo = NULL; - if (!ostree_sysroot_get_repo (sysroot, &repo, cancellable, error)) - return FALSE; - OstreeDeployment *booted_deployment = ostree_sysroot_get_booted_deployment (sysroot); - if (!booted_deployment) - return glnx_throw (error, "Not currently booted into an OSTree system"); - - /* Overlayfs doesn't support mutation of the lowerdir. And broadly speaking, - * our medium term goal here is to obviate most of the unlock usage. - */ - OstreeDeploymentUnlockedState unlockstate = ostree_deployment_get_unlocked (booted_deployment); - if (unlockstate != OSTREE_DEPLOYMENT_UNLOCKED_NONE) - return glnx_throw (error, "livefs is incompatible with unlocked state"); - - /* Find the source for /etc - either booted or pending, but down below we - require pending */ - OstreeDeployment *origin_merge_deployment = - rpmostree_syscore_get_origin_merge_deployment (sysroot, - ostree_deployment_get_osname (booted_deployment)); - g_assert (origin_merge_deployment); - const char *booted_csum = ostree_deployment_get_csum (booted_deployment); - const char *target_csum = ostree_deployment_get_csum (origin_merge_deployment); - - /* Require a pending deployment to use as a source - perhaps in the future we - * handle direct live overlays. - */ - if (origin_merge_deployment == booted_deployment) - return glnx_throw (error, "No pending deployment"); - if (!ostree_deployment_is_staged (origin_merge_deployment)) - return glnx_throw (error, "livefs requires staged deployments"); - - /* Open a fd for the booted deployment */ - g_autofree char *deployment_path = ostree_sysroot_get_deployment_dirpath (sysroot, booted_deployment); - glnx_autofd int deployment_dfd = -1; - if (!glnx_opendirat (ostree_sysroot_get_fd (sysroot), deployment_path, TRUE, - &deployment_dfd, error)) - return FALSE; - - /* Find out whether we already have a live overlay */ - g_autofree char *live_inprogress = NULL; - g_autofree char *live_replaced = NULL; - if (!rpmostree_syscore_deployment_get_live (booted_deployment, &live_inprogress, - &live_replaced, error)) - return FALSE; - const char *resuming_overlay = NULL; - if (live_inprogress != NULL) - { - if (strcmp (live_inprogress, target_csum) == 0) - resuming_overlay = target_csum; - } - const char *replacing_overlay = NULL; - if (live_replaced != NULL) - { - if (strcmp (live_replaced, target_csum) == 0) - return glnx_throw (error, "Current overlay is already %s", target_csum); - replacing_overlay = live_replaced; - } - - if (resuming_overlay) - rpmostree_output_message ("Note: Resuming interrupted overlay of %s", target_csum); - if (replacing_overlay) - rpmostree_output_message ("Note: Previous overlay: %s", replacing_overlay); - - /* Look at the difference between the two commits - we could also walk the - * filesystem, but doing it at the ostree level is potentially faster, since - * we know when two directories are the same. - */ - g_autoptr(CommitDiff) diff = NULL; - if (!analyze_commit_diff (repo, booted_csum, target_csum, - &diff, cancellable, error)) - return FALSE; - - print_commit_diff (diff); - - const gboolean replacing = (self->flags & RPMOSTREE_TRANSACTION_LIVEFS_FLAG_REPLACE) > 0; - const gboolean requires_etc_merge = (diff->flags & COMMIT_DIFF_FLAGS_ETC) > 0; - const gboolean adds_packages = diff->added_pkgs->len > 0; - const gboolean modifies_packages = diff->removed_pkgs->len > 0 || diff->modified_pkgs_new->len > 0; - if ((diff->flags & COMMIT_DIFF_FLAGS_ROOTFS) > 0) - return glnx_throw (error, "livefs update would modify non-/usr content"); - /* Is this a dry run? */ - /* Error out in various cases if we're not doing a replacement */ - if (!replacing) - { - if (!adds_packages) - return glnx_throw (error, "No packages added; cannot apply"); - if (modifies_packages) - return glnx_throw (error, "livefs update modifies/replaces packages; cannot apply"); - else if ((diff->flags & COMMIT_DIFF_FLAGS_REPLACEMENT) > 0) - return glnx_throw (error, "livefs update would replace files in /usr; cannot apply"); - } - if ((self->flags & RPMOSTREE_TRANSACTION_LIVEFS_FLAG_DRY_RUN) > 0) - { - rpmostree_output_message ("livefs OK (dry run)"); - /* Note early return */ - return TRUE; - } - - g_autoptr(GString) journal_msg = g_string_new (""); - g_string_append_printf (journal_msg, "Starting livefs for commit %s", target_csum); - if (resuming_overlay) - g_string_append (journal_msg, " (resuming)"); - - if (replacing) - g_string_append_printf (journal_msg, " replacement; %u/%u/%u pkgs (added, removed, modified); %u/%u/%u files", - diff->added_pkgs->len, diff->removed_pkgs->len, diff->modified_pkgs_old->len, - diff->added->len, diff->removed->len, diff->modified->len); - else - g_string_append_printf (journal_msg, " addition; %u pkgs, %u files", - diff->added_pkgs->len, diff->added->len); - - if (replacing_overlay) - g_string_append_printf (journal_msg, "; replacing %s", replacing_overlay); - sd_journal_send ("MESSAGE_ID=" SD_ID128_FORMAT_STR, SD_ID128_FORMAT_VAL(RPMOSTREE_MESSAGE_LIVEFS_BEGIN), - "MESSAGE=%s", journal_msg->str, - "RESUMING=%s", resuming_overlay ?: "", - "REPLACING=%s", replacing_overlay ?: "", - "BOOTED_COMMIT=%s", booted_csum, - "TARGET_COMMIT=%s", target_csum, - NULL); - g_string_truncate (journal_msg, 0); - - /* Ensure that we have a rollback deployment that matches our booted checksum, - * so that if something goes wrong, the user can get to it. If we have an - * older rollback, that gets GC'd. - */ - OstreeDeployment *rollback_deployment = get_rollback_deployment (sysroot, booted_deployment); - if (!rollback_deployment) - { - if (!prepare_rollback_deployment (sysroot, repo, booted_deployment, cancellable, error)) - return g_prefix_error (error, "Preparing rollback: "), FALSE; - } - - /* Reload this, the sysroot may have changed it */ - booted_deployment = ostree_sysroot_get_booted_deployment (sysroot); - - /* Load SELinux policy for making changes to /etc */ - g_autoptr(OstreeSePolicy) sepolicy = ostree_sepolicy_new_at (deployment_dfd, cancellable, error); - if (!sepolicy) - return FALSE; - - /* Note we inherit the previous value of `live_replaced` (which may be NULL) */ - if (!write_livefs_state (sysroot, booted_deployment, target_csum, live_replaced, error)) - return FALSE; - - g_auto(GLnxTmpDir) replace_tmpdir = { 0, }; - if (!glnx_mkdtempat (ostree_repo_get_dfd (repo), "tmp/rpmostree-livefs.XXXXXX", 0700, - &replace_tmpdir, error)) - return FALSE; - - if (!replacing) - { - g_auto(RpmOstreeProgress) task = { 0, }; - rpmostree_output_task_begin (&task, "Overlaying /usr"); - if (!checkout_add_usr (repo, deployment_dfd, diff, target_csum, cancellable, error)) - return FALSE; - - /* And files that we always need to replace; the rpmdb, /usr/lib/passwd. We - * make a tmpdir just for this since it's a more convenient place to put - * temporary files/dirs without generating tempnames. - */ - const char *replace_paths[] = { "/" RPMOSTREE_RPMDB_LOCATION, "/usr/lib/passwd", "/usr/lib/group" }; - for (guint i = 0; i < G_N_ELEMENTS(replace_paths); i++) - { - const char *replace_path = replace_paths[i]; - if (!replace_subpath (repo, deployment_dfd, &replace_tmpdir, - target_csum, replace_path, - cancellable, error)) - return FALSE; - } - } - else - { - /* Hold my beer 🍺, we're going loop over /usr and RENAME_EXCHANGE things - * that were modified. - */ - g_auto(RpmOstreeProgress) task = { 0, }; - rpmostree_output_task_begin (&task, "Replacing /usr"); - if (!replace_usr (repo, deployment_dfd, &replace_tmpdir, - diff, target_csum, - cancellable, error)) - return FALSE; - } - - if (diff->n_tmpfilesd > 0) - { - const char *tmpfiles_prefixes[] = { "/run/", "/var/"}; - const char *tmpfiles_argv[] = { "systemd-tmpfiles", "--create", - "--prefix", NULL, NULL }; - - g_auto(RpmOstreeProgress) task = { 0, }; - rpmostree_output_task_begin (&task, "Running systemd-tmpfiles for /run,/var"); - - for (guint i = 0; i < G_N_ELEMENTS (tmpfiles_prefixes); i++) - { - const char *prefix = tmpfiles_prefixes[i]; - GLNX_AUTO_PREFIX_ERROR ("Executing systemd-tmpfiles", error); - tmpfiles_argv[G_N_ELEMENTS(tmpfiles_argv)-2] = prefix; - g_autoptr(GSubprocess) subproc = g_subprocess_newv ((const char *const*)tmpfiles_argv, - G_SUBPROCESS_FLAGS_STDERR_PIPE | - G_SUBPROCESS_FLAGS_STDOUT_SILENCE, - error); - if (!subproc) - return FALSE; - g_autofree char *stderr_buf = NULL; - if (!g_subprocess_communicate_utf8 (subproc, NULL, cancellable, - NULL, &stderr_buf, error)) - return FALSE; - int estatus = g_subprocess_get_exit_status (subproc); - if (!g_spawn_check_exit_status (estatus, error)) - { - /* Only dump stderr if it actually failed; otherwise we know - * there are (harmless) warnings, no need to bother everyone. - */ - sd_journal_print (LOG_ERR, "systemd-tmpfiles failed: %s", stderr_buf); - return FALSE; - } - } - } - - if (requires_etc_merge) - { - if (!copy_new_config_files (repo, origin_merge_deployment, - deployment_dfd, sepolicy, diff, - cancellable, error)) - return FALSE; - } - - /* Write out the origin as having completed this */ - if (!write_livefs_state (sysroot, booted_deployment, NULL, target_csum, error)) - return FALSE; - - sd_journal_send ("MESSAGE_ID=" SD_ID128_FORMAT_STR, SD_ID128_FORMAT_VAL(RPMOSTREE_MESSAGE_LIVEFS_END), - "MESSAGE=Completed livefs for commit %s", target_csum, - "BOOTED_COMMIT=%s", booted_csum, - "TARGET_COMMIT=%s", target_csum, - NULL); - - return TRUE; -} - static gboolean livefs_transaction_execute (RpmostreedTransaction *transaction, GCancellable *cancellable, @@ -899,8 +68,13 @@ livefs_transaction_execute (RpmostreedTransaction *transaction, LiveFsTransaction *self = (LiveFsTransaction *) transaction; OstreeSysroot *sysroot = rpmostreed_transaction_get_sysroot (transaction); + g_auto(GVariantDict) options_dict; + g_variant_dict_init (&options_dict, self->options); + const char *target = NULL; + (void) g_variant_dict_lookup (&options_dict, "target", "&s", &target); + /* Run the transaction */ - gboolean ret = livefs_transaction_execute_inner (self, sysroot, cancellable, error); + gboolean ret = ror_transaction_livefs (sysroot, target, error); /* We use this to notify ourselves of changes, which is a bit silly, but it * keeps things consistent if `ostree admin` is invoked directly. Always @@ -911,7 +85,6 @@ livefs_transaction_execute (RpmostreedTransaction *transaction, return ret; } - static void livefs_transaction_class_init (LiveFsTransactionClass *class) { @@ -931,7 +104,7 @@ livefs_transaction_init (LiveFsTransaction *self) RpmostreedTransaction * rpmostreed_transaction_new_livefs (GDBusMethodInvocation *invocation, OstreeSysroot *sysroot, - RpmOstreeTransactionLiveFsFlags flags, + GVariant *options, GCancellable *cancellable, GError **error) { @@ -948,7 +121,7 @@ rpmostreed_transaction_new_livefs (GDBusMethodInvocation *invocation, if (self != NULL) { - self->flags = flags; + self->options = g_variant_ref (options); } return (RpmostreedTransaction *) self; diff --git a/src/daemon/rpmostreed-transaction-types.c b/src/daemon/rpmostreed-transaction-types.c index 4df9d995..65b54cb1 100644 --- a/src/daemon/rpmostreed-transaction-types.c +++ b/src/daemon/rpmostreed-transaction-types.c @@ -1232,7 +1232,7 @@ deploy_transaction_execute (RpmostreedTransaction *transaction, rpmostree_sysroot_upgrader_get_merge_deployment (upgrader); gboolean is_live; - if (!rpmostree_syscore_deployment_is_live (deployment, &is_live, error)) + if (!rpmostree_syscore_livefs_query (sysroot, deployment, &is_live, error)) return FALSE; if (is_live) diff --git a/src/daemon/rpmostreed-transaction-types.h b/src/daemon/rpmostreed-transaction-types.h index 4850831d..7270b213 100644 --- a/src/daemon/rpmostreed-transaction-types.h +++ b/src/daemon/rpmostreed-transaction-types.h @@ -114,15 +114,10 @@ rpmostreed_transaction_new_cleanup (GDBusMethodInvocation *invocation, GCancellable *cancellable, GError **error); -typedef enum { - RPMOSTREE_TRANSACTION_LIVEFS_FLAG_DRY_RUN = (1 << 0), - RPMOSTREE_TRANSACTION_LIVEFS_FLAG_REPLACE = (1 << 1), -} RpmOstreeTransactionLiveFsFlags; - RpmostreedTransaction * rpmostreed_transaction_new_livefs (GDBusMethodInvocation *invocation, OstreeSysroot *sysroot, - RpmOstreeTransactionLiveFsFlags flags, + GVariant *options, GCancellable *cancellable, GError **error); diff --git a/src/libpriv/rpmostree-origin.c b/src/libpriv/rpmostree-origin.c index e2f52aa8..6074e877 100644 --- a/src/libpriv/rpmostree-origin.c +++ b/src/libpriv/rpmostree-origin.c @@ -195,9 +195,6 @@ rpmostree_origin_remove_transient_state (RpmOstreeOrigin *origin) /* this is already covered by the above, but the below also updates the cached value */ rpmostree_origin_set_override_commit (origin, NULL, NULL); - - /* then rpm-ostree specific things */ - rpmostree_origin_set_live_state (origin, NULL, NULL); } const char * @@ -363,17 +360,6 @@ rpmostree_origin_may_require_local_assembly (RpmOstreeOrigin *origin) (g_hash_table_size (origin->cached_overrides_remove) > 0); } -void -rpmostree_origin_get_live_state (RpmOstreeOrigin *origin, - char **out_inprogress, - char **out_live) -{ - if (out_inprogress) - *out_inprogress = g_key_file_get_string (origin->kf, "rpmostree-ex-live", "inprogress", NULL); - if (out_live) - *out_live = g_key_file_get_string (origin->kf, "rpmostree-ex-live", "commit", NULL); -} - GKeyFile * rpmostree_origin_dup_keyfile (RpmOstreeOrigin *origin) { @@ -639,33 +625,6 @@ rpmostree_origin_set_rebase (RpmOstreeOrigin *origin, return rpmostree_origin_set_rebase_custom (origin, new_refspec, NULL, NULL, error); } -/* Like g_key_file_set_string(), but remove the key if @value is NULL */ -static void -set_or_unset_str (GKeyFile *kf, - const char *group, - const char *key, - const char *value) -{ - if (!value) - (void) g_key_file_remove_key (kf, group, key, NULL); - else - (void) g_key_file_set_string (kf, group, key, value); -} - -void -rpmostree_origin_set_live_state (RpmOstreeOrigin *origin, - const char *inprogress, - const char *live) -{ - if (!inprogress && !live) - (void) g_key_file_remove_group (origin->kf, "rpmostree-ex-live", NULL); - else - { - set_or_unset_str (origin->kf, "rpmostree-ex-live", "inprogress", inprogress); - set_or_unset_str (origin->kf, "rpmostree-ex-live", "commit", live); - } -} - static void update_keyfile_pkgs_from_cache (RpmOstreeOrigin *origin, const char *group, diff --git a/src/libpriv/rpmostree-origin.h b/src/libpriv/rpmostree-origin.h index d133f094..5b38f5e3 100644 --- a/src/libpriv/rpmostree-origin.h +++ b/src/libpriv/rpmostree-origin.h @@ -110,13 +110,6 @@ rpmostree_origin_get_unconfigured_state (RpmOstreeOrigin *origin); gboolean rpmostree_origin_may_require_local_assembly (RpmOstreeOrigin *origin); -// WARNING: This prototype is also redefined in Rust, if changing this -// please also update `includes.rs`. -void -rpmostree_origin_get_live_state (RpmOstreeOrigin *origin, - char **out_inprogress, - char **out_live); - char * rpmostree_origin_get_string (RpmOstreeOrigin *origin, const char *section, @@ -207,8 +200,3 @@ gboolean rpmostree_origin_remove_all_overrides (RpmOstreeOrigin *origin, gboolean *out_changed, GError **error); - -void -rpmostree_origin_set_live_state (RpmOstreeOrigin *origin, - const char *inprogress, - const char *live); diff --git a/src/libpriv/rpmostree-rust-prelude.h b/src/libpriv/rpmostree-rust-prelude.h index a59f2702..1b4d88d7 100644 --- a/src/libpriv/rpmostree-rust-prelude.h +++ b/src/libpriv/rpmostree-rust-prelude.h @@ -36,4 +36,6 @@ b(GPtrArray) b(GChecksum) b(OstreeRepo) b(RpmOstreeOrigin) +b(OstreeDeployment) +b(OstreeSysroot) #undef b diff --git a/tests/vmcheck/test-layering-scripts.sh b/tests/vmcheck/test-layering-scripts.sh index 2972ae57..e1b3ca18 100755 --- a/tests/vmcheck/test-layering-scripts.sh +++ b/tests/vmcheck/test-layering-scripts.sh @@ -108,7 +108,7 @@ vm_build_rpm scriptpkg2 \ vm_build_rpm scriptpkg3 \ post 'echo %%{_prefix} > /usr/lib/noprefixtest.txt' vm_rpmostree pkg-add scriptpkg{2,3} -vm_rpmostree ex livefs --i-like-danger +vm_rpmostree ex livefs vm_cmd cat /usr/lib/noprefixtest.txt > noprefixtest.txt assert_file_has_content noprefixtest.txt '%{_prefix}' vm_cmd cat /usr/lib/prefixtest.txt > prefixtest.txt @@ -123,7 +123,7 @@ vm_build_rpm rpmostree-lua-override-test-expand \ post_args "-e -p " \ post 'posix.stat("/")' vm_rpmostree install rpmostree-lua-override-test{,-expand} -vm_rpmostree ex livefs --i-like-danger +vm_rpmostree ex livefs vm_cmd cat /usr/share/rpmostree-lua-override-test > lua-override.txt assert_file_has_content lua-override.txt _install_langs vm_cmd rpm --eval '%{_install_langs}' > install-langs.txt @@ -147,7 +147,7 @@ vm_build_rpm scriptpkg5 \ transfiletriggerun "/usr/share/licenses/systemd /usr/share/licenses/rpm" 'sort >/usr/share/transfiletriggerun-license-systemd-rpm.txt' \ transfiletriggerin2 "/usr/share/licenses/sed /usr/share/licenses/tzdata" 'sort >/usr/share/transfiletriggerin-license-sed-tzdata.txt' vm_rpmostree pkg-add scriptpkg{4,5} -vm_rpmostree ex livefs --i-like-danger +vm_rpmostree ex livefs for combo in ${license_combos}; do vm_cmd cat /usr/share/transfiletriggerin-license-${combo}.txt > transfiletriggerin-license-${combo}.txt rm -f transfiletriggerin-fs-${combo}.txt.tmp @@ -229,7 +229,7 @@ echo "ok impervious to rm -rf post" # capabilities vm_build_rpm test-cap-drop post "capsh --print > /usr/share/rpmostree-capsh.txt" vm_rpmostree install test-cap-drop -vm_rpmostree ex livefs --i-like-danger +vm_rpmostree ex livefs vm_cmd cat /usr/share/rpmostree-capsh.txt > caps.txt assert_not_file_has_content caps.test '^Current: =.*cap_sys_admin' diff --git a/tests/vmcheck/test-livefs.sh b/tests/vmcheck/test-livefs.sh index 72a8da2d..86aa764c 100755 --- a/tests/vmcheck/test-livefs.sh +++ b/tests/vmcheck/test-livefs.sh @@ -31,6 +31,7 @@ vm_rpmostree cleanup -pr vm_assert_layered_pkg foo absent vm_build_rpm foo +vm_build_rpm bar vm_rpmostree install /var/tmp/vmcheck/yumrepo/packages/x86_64/foo-1.0-1.x86_64.rpm vm_assert_status_jq '.deployments|length == 2' echo "ok install foo locally" @@ -38,22 +39,36 @@ echo "ok install foo locally" if vm_cmd rpm -q foo; then assert_not_reached "have foo?" fi -assert_livefs_ok() { - vm_rpmostree ex livefs --i-like-danger -n > livefs-analysis.txt - assert_file_has_content livefs-analysis.txt 'livefs OK (dry run)' -} -assert_livefs_ok vm_assert_status_jq '.deployments|length == 2' \ '.deployments[0]["live-replaced"]|not' \ '.deployments[1]["live-replaced"]|not' -vm_rpmostree ex livefs --i-like-danger +vm_rpmostree ex livefs vm_cmd rpm -q foo > rpmq.txt assert_file_has_content rpmq.txt foo-1.0-1 -vm_assert_status_jq '.deployments|length == 3' '.deployments[0]["live-replaced"]|not' \ +vm_cmd ls -al /usr/bin/foo +vm_assert_status_jq '.deployments|length == 2' '.deployments[0]["live-replaced"]|not' \ '.deployments[1]["live-replaced"]' +if vm_cmd test -w /usr; then + fatal "Found writable /usr" +fi +echo "ok livefs basic" -echo "ok livefs stage1" +vm_rpmostree cleanup -p +vm_rpmostree install bar +vm_assert_status_jq '.deployments|length == 2' \ + '.deployments[0]["live-replaced"]|not' \ + '.deployments[1]["live-replaced"]' +vm_rpmostree ex livefs +vm_cmd rpm -qa > rpmq.txt +assert_file_has_content rpmq.txt bar-1.0-1 +assert_not_file_has_content rpmq.txt foo-1.0-1 +vm_cmd ls -al /usr/bin/bar +if vm_cmd test -f /usr/bin/foo; then + fatal "Still have /usr/bin/foo" +fi + +echo "ok livefs again" vm_build_rpm test-livefs-with-etc \ build 'echo "A config file for %{name}" > %{name}.conf' \ @@ -86,10 +101,9 @@ vm_cmd rm -rf /etc/test-livefs-with-etc \ /etc/opt/test-livefs-with-etc-opt.conf vm_rpmostree install /var/tmp/vmcheck/yumrepo/packages/x86_64/test-livefs-{with-etc,service}-1.0-1.x86_64.rpm -assert_livefs_ok -vm_rpmostree ex livefs --i-like-danger -vm_cmd rpm -q foo test-livefs-{with-etc,service} > rpmq.txt -assert_file_has_content rpmq.txt foo-1.0-1 test-livefs-{with-etc,service}-1.0-1 +vm_rpmostree ex livefs +vm_cmd rpm -q bar test-livefs-{with-etc,service} > rpmq.txt +assert_file_has_content rpmq.txt bar-1.0-1 test-livefs-{with-etc,service}-1.0-1 vm_cmd cat /etc/test-livefs-with-etc.conf > test-livefs-with-etc.conf assert_file_has_content test-livefs-with-etc.conf "A config file for test-livefs-with-etc" for v in subconfig-one subconfig-two subdir/subconfig-three; do @@ -107,91 +121,3 @@ assert_file_has_content test-livefs-group.txt livefs-group vm_cmd test -d /var/lib/test-livefs-service echo "ok livefs stage2" - -# Now, perform a further change in the pending -vm_rpmostree uninstall test-livefs-with-etc-1.0-1.x86_64 -vm_assert_status_jq '.deployments|length == 3' -echo "ok livefs preserved rollback" - -# Reset to rollback, undeploy pending -reset() { - vm_rpmostree reset - vm_reboot - vm_rpmostree cleanup -r - vm_assert_status_jq '.deployments|length == 1' '.deployments[0]["live-replaced"]|not' -} -reset - -# If the admin created a config file before, we need to keep it -vm_rpmostree install /var/tmp/vmcheck/yumrepo/packages/x86_64/test-livefs-with-etc-1.0-1.x86_64.rpm -vm_cmd cat /etc/test-livefs-with-etc.conf || true -vm_cmd echo custom \> /etc/test-livefs-with-etc.conf -vm_cmd cat /etc/test-livefs-with-etc.conf -vm_rpmostree ex livefs --i-like-danger -vm_cmd cat /etc/test-livefs-with-etc.conf > test-livefs-with-etc.conf -assert_file_has_content test-livefs-with-etc.conf "custom" -echo "ok livefs preserved modified config" - -vm_rpmostree cleanup -p -# make sure there's no layering going on somehow -vm_assert_status_jq '.deployments[0]["base-checksum"]|not' -vm_rpmostree deploy $(vm_get_booted_deployment_info checksum) -echo "ok livefs redeploy booted commit" - -reset -vm_rpmostree install /var/tmp/vmcheck/yumrepo/packages/x86_64/foo-1.0-1.x86_64.rpm -vm_rpmostree ex livefs --i-like-danger -# Picked a file that should be around, but harmless to change for testing. The -# first is available on Fedora, the second on CentOS (and newer too). -dummy_file_to_modify=usr/share/licenses/ostree/COPYING -if ! vm_cmd test -f /${dummy_file_to_modify}; then - dummy_file_to_modify=usr/share/ostree/trusted.gpg.d/README-gpg -fi -vm_cmd test -f /${dummy_file_to_modify} -generate_upgrade() { - # Create a modified vmcheck commit - vm_shell_inline_sysroot_rw <vmcheck/${dummy_file_to_modify}.new && mv vmcheck/${dummy_file_to_modify}{.new,} - $@ - ostree commit -b vmcheck --tree=dir=vmcheck --link-checkout-speedup - rm vmcheck -rf -EOF -} -generate_upgrade -# And remove the pending deployment so that our origin is now the booted -vm_rpmostree cleanup -p -vm_rpmostree upgrade -vm_assert_status_jq '.deployments|length == 3' '.deployments[0]["live-replaced"]|not' \ - '.deployments[1]["live-replaced"]' - -echo "ok livefs not carried over across upgrades" - -reset -generate_upgrade "mkdir -p vmcheck/usr/newsubdir && date > vmcheck/usr/newsubdir/date.txt" -vm_rpmostree upgrade -vm_assert_status_jq '.deployments|length == 2' '.deployments[0]["live-replaced"]|not' \ - '.deployments[1]["live-replaced"]|not' -if vm_rpmostree ex livefs --i-like-danger -n &> livefs-analysis.txt; then - assert_not_reached "livefs succeeded?" -fi -vm_assert_status_jq '.deployments|length == 2' '.deployments[0]["live-replaced"]|not' \ - '.deployments[1]["live-replaced"]|not' -assert_file_has_content livefs-analysis.txt 'No packages added' -echo "ok no modifications" - -# And now replacement -vm_rpmostree ex livefs -n --i-like-danger --dangerous-do-not-use-replace &> livefs-analysis.txt -assert_file_has_content livefs-analysis.txt 'livefs OK (dry run)' -vm_assert_status_jq '.deployments|length == 2' '.deployments[0]["live-replaced"]|not' \ - '.deployments[1]["live-replaced"]|not' -vm_rpmostree ex livefs --i-like-danger --dangerous-do-not-use-replace -vm_cmd cat /${dummy_file_to_modify} > dummyfile.txt -assert_file_has_content dummyfile.txt "JUST KIDDING DO WHATEVER" -vm_cmd test -f /usr/newsubdir/date.txt -vm_assert_status_jq '.deployments|length == 3' '.deployments[0]["live-replaced"]|not' \ - '.deployments[1]["live-replaced"]' '.deployments[1]["booted"]' -echo "ok modifications" -