Rewrite livefs

Now always based on an overlayfs:
f2773c1b55
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.
This commit is contained in:
Colin Walters 2020-10-24 17:31:39 +00:00 committed by OpenShift Merge Robot
parent 213d8f0aa2
commit a76ddf0cef
21 changed files with 792 additions and 1171 deletions

View File

@ -35,7 +35,6 @@ subprocess = "0.2.6"
chrono = { version = "0.4.19", features = ["serde"] } chrono = { version = "0.4.19", features = ["serde"] }
libdnf-sys = { path = "libdnf-sys", version = "0.1.0" } libdnf-sys = { path = "libdnf-sys", version = "0.1.0" }
[lib] [lib]
name = "rpmostree_rust" name = "rpmostree_rust"
path = "src/lib.rs" path = "src/lib.rs"

View File

@ -8,7 +8,6 @@ NOTICE: The C header definitions are canonical, please update those first
then synchronize the entries here. then synchronize the entries here.
!*/ !*/
use crate::syscore::ffi::RpmOstreeOrigin;
use libdnf_sys::DnfPackage; use libdnf_sys::DnfPackage;
// From `libpriv/rpmostree-rpm-util.h`. // From `libpriv/rpmostree-rpm-util.h`.
@ -19,12 +18,3 @@ extern "C" {
gerror: *mut *mut glib_sys::GError, gerror: *mut *mut glib_sys::GError,
) -> libc::c_int; ) -> 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,
);
}

View File

@ -20,10 +20,12 @@ mod initramfs;
pub use self::initramfs::ffi::*; pub use self::initramfs::ffi::*;
mod lockfile; mod lockfile;
pub use self::lockfile::*; pub use self::lockfile::*;
mod livefs;
pub use self::livefs::*;
mod ostree_diff;
mod ostree_utils;
mod progress; mod progress;
pub use self::progress::*; pub use self::progress::*;
mod syscore;
pub use self::syscore::ffi::*;
mod testutils; mod testutils;
pub use self::testutils::*; pub use self::testutils::*;
mod treefile; mod treefile;

499
rust/src/livefs.rs Normal file
View File

@ -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<String>,
/// 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<String>,
}
/// 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<Option<LiveFsState>> {
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<PathBuf> {
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<PathBuf>, &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<Option<GString>> = 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::*;

170
rust/src/ostree_diff.rs Normal file
View File

@ -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<Option<gio::FileInfo>> {
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::<gio::IOErrorEnum>() {
match e2 {
gio::IOErrorEnum::NotFound => Ok(None),
_ => return Err(e.into()),
}
} else {
return Err(e.into());
}
}
}
}
pub(crate) type FileSet = BTreeSet<String>;
/// 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<String>,
/// 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::<ostree::RepoFile>().expect("downcast");
to_child.ensure_resolved()?;
let from_child = from_child.downcast::<ostree::RepoFile>().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<P: AsRef<str>>(
repo: &ostree::Repo,
from: &str,
to: &str,
subdir: Option<P>,
) -> Result<FileTreeDiff> {
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::<ostree::RepoFile>().expect("downcast");
fromroot.ensure_resolved()?;
let toroot = toroot.downcast::<ostree::RepoFile>().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)
}

20
rust/src/ostree_utils.rs Normal file
View File

@ -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<ostree::Deployment>, Option<ostree::Deployment>) {
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))
}
}

View File

@ -1,73 +0,0 @@
use self::ffi::RpmOstreeOrigin;
use std::ptr::NonNull;
/// Reference to an `RpmOstreeOrigin`.
#[derive(Debug)]
pub(crate) struct OriginRef {
origin: NonNull<RpmOstreeOrigin>,
}
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<String>,
/// Checksum for the underlying replaced commit.
pub replaced: Option<String>,
}
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()
}
}

View File

@ -29,15 +29,10 @@
#include <libglnx.h> #include <libglnx.h>
static gboolean opt_dry_run; static char *opt_target;
static gboolean opt_replace;
static gboolean opt_consented;
static GOptionEntry option_entries[] = { static GOptionEntry option_entries[] = {
{ "dry-run", 'n', 0, G_OPTION_ARG_NONE, &opt_dry_run, "Only perform analysis, do not make changes", NULL }, { "target", 0, 0, G_OPTION_ARG_NONE, &opt_target, "Target provided commit instead of pending deployment", 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 },
{ NULL } { NULL }
}; };
@ -47,8 +42,8 @@ get_args_variant (void)
GVariantDict dict; GVariantDict dict;
g_variant_dict_init (&dict, NULL); g_variant_dict_init (&dict, NULL);
g_variant_dict_insert (&dict, "dry-run", "b", opt_dry_run); if (opt_target)
g_variant_dict_insert (&dict, "replace", "b", opt_replace); g_variant_dict_insert (&dict, "target", "s", opt_target);
return g_variant_dict_end (&dict); return g_variant_dict_end (&dict);
} }
@ -75,10 +70,6 @@ rpmostree_ex_builtin_livefs (int argc,
error)) error))
return FALSE; 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 RPMOSTreeOS *os_proxy = NULL;
glnx_unref_object RPMOSTreeOSExperimental *osexperimental_proxy = NULL; glnx_unref_object RPMOSTreeOSExperimental *osexperimental_proxy = NULL;
if (!rpmostree_load_os_proxies (sysroot_proxy, NULL, if (!rpmostree_load_os_proxies (sysroot_proxy, NULL,

View File

@ -308,10 +308,31 @@ rpmostree_syscore_cleanup (OstreeSysroot *sysroot,
cancellable, error)) cancellable, error))
return FALSE; 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 */ /* And do a prune */
guint64 freed_space; guint64 freed_space;
gint n_objects_total, n_objects_pruned; gint n_objects_total, n_objects_pruned;
{ g_autoptr(GHashTable) reachable = ostree_repo_traverse_new_reachable (); { 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 }; OstreeRepoPruneOptions opts = { OSTREE_REPO_PRUNE_FLAGS_REFS_ONLY, reachable };
if (!ostree_sysroot_cleanup_prune_repo (sysroot, &opts, &n_objects_total, if (!ostree_sysroot_cleanup_prune_repo (sysroot, &opts, &n_objects_total,
&n_objects_pruned, &freed_space, &n_objects_pruned, &freed_space,
@ -351,6 +372,17 @@ rpmostree_syscore_get_origin_merge_deployment (OstreeSysroot *self, const char *
return NULL; 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() /* Copy of currently private _ostree_sysroot_bump_mtime()
* until we decide to either formalize that, or have a method * until we decide to either formalize that, or have a method
@ -446,7 +478,7 @@ rpmostree_syscore_write_deployment (OstreeSysroot *sysroot,
if (booted) if (booted)
{ {
gboolean is_live; 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; return FALSE;
if (is_live) if (is_live)
flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_ROLLBACK; flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_ROLLBACK;
@ -463,33 +495,3 @@ rpmostree_syscore_write_deployment (OstreeSysroot *sysroot,
return TRUE; 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;
}

View File

@ -45,17 +45,7 @@ OstreeDeployment *rpmostree_syscore_get_origin_merge_deployment (OstreeSysroot *
gboolean rpmostree_syscore_bump_mtime (OstreeSysroot *self, GError **error); gboolean rpmostree_syscore_bump_mtime (OstreeSysroot *self, GError **error);
#define RPMOSTREE_LIVE_INPROGRESS_XATTR "user.rpmostree-live-inprogress" gboolean rpmostree_syscore_livefs_query (OstreeSysroot *self, OstreeDeployment *deployment, gboolean *out_is_live, GError **error);
#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);
GPtrArray *rpmostree_syscore_filter_deployments (OstreeSysroot *sysroot, GPtrArray *rpmostree_syscore_filter_deployments (OstreeSysroot *sysroot,
const char *osname, const char *osname,

View File

@ -593,7 +593,7 @@ try_load_base_rsack_from_pending (RpmOstreeSysrootUpgrader *self,
GError **error) GError **error)
{ {
gboolean is_live; 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; return FALSE;
/* livefs invalidates the deployment */ /* livefs invalidates the deployment */

View File

@ -261,6 +261,8 @@ rpmostreed_deployment_generate_variant (OstreeSysroot *sysroot,
if (!origin) if (!origin)
return NULL; return NULL;
const gboolean is_booted = g_strcmp0 (booted_id, id) == 0;
RpmOstreeRefspecType refspec_type; RpmOstreeRefspecType refspec_type;
g_autofree char *refspec = rpmostree_origin_get_full_refspec (origin, &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; 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) if (is_booted)
g_variant_dict_insert (&dict, "live-inprogress", "s", live_inprogress); {
if (live_replaced) g_autofree char *live_inprogress = NULL;
g_variant_dict_insert (&dict, "live-replaced", "s", live_replaced); 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)) if (ostree_deployment_is_staged (deployment))
{ {
@ -416,7 +421,7 @@ rpmostreed_deployment_generate_variant (OstreeSysroot *sysroot,
rpmostree_origin_get_initramfs_etc_files (origin)); rpmostree_origin_get_initramfs_etc_files (origin));
if (booted_id != NULL) 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); return g_variant_dict_end (&dict);
} }

View File

@ -115,23 +115,6 @@ osexperimental_handle_moo (RPMOSTreeOSExperimental *interface,
rpmostree_osexperimental_complete_moo (interface, invocation, result); rpmostree_osexperimental_complete_moo (interface, invocation, result);
return TRUE; 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 static gboolean
osexperimental_handle_live_fs (RPMOSTreeOSExperimental *interface, osexperimental_handle_live_fs (RPMOSTreeOSExperimental *interface,
@ -159,7 +142,7 @@ osexperimental_handle_live_fs (RPMOSTreeOSExperimental *interface,
transaction = rpmostreed_transaction_new_livefs (invocation, transaction = rpmostreed_transaction_new_livefs (invocation,
ot_sysroot, ot_sysroot,
livefs_flags_from_options (arg_options), arg_options,
cancellable, cancellable,
&local_error); &local_error);
if (transaction == NULL) if (transaction == NULL)

View File

@ -36,12 +36,9 @@
#include "rpmostree-core.h" #include "rpmostree-core.h"
#include "rpmostreed-utils.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 { typedef struct {
RpmostreedTransaction parent; RpmostreedTransaction parent;
RpmOstreeTransactionLiveFsFlags flags; GVariant *options;
} LiveFsTransaction; } LiveFsTransaction;
typedef RpmostreedTransactionClass LiveFsTransactionClass; typedef RpmostreedTransactionClass LiveFsTransactionClass;
@ -58,839 +55,11 @@ livefs_transaction_finalize (GObject *object)
G_GNUC_UNUSED LiveFsTransaction *self; G_GNUC_UNUSED LiveFsTransaction *self;
self = (LiveFsTransaction *) object; self = (LiveFsTransaction *) object;
g_variant_unref (self->options);
G_OBJECT_CLASS (livefs_transaction_parent_class)->finalize (object); 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<GFile> */
GPtrArray *modified; /* Set<OstreeDiffItem> */
GPtrArray *removed; /* Set<GFile> */
/* 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 static gboolean
livefs_transaction_execute (RpmostreedTransaction *transaction, livefs_transaction_execute (RpmostreedTransaction *transaction,
GCancellable *cancellable, GCancellable *cancellable,
@ -899,8 +68,13 @@ livefs_transaction_execute (RpmostreedTransaction *transaction,
LiveFsTransaction *self = (LiveFsTransaction *) transaction; LiveFsTransaction *self = (LiveFsTransaction *) transaction;
OstreeSysroot *sysroot = rpmostreed_transaction_get_sysroot (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 */ /* 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 /* 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 * keeps things consistent if `ostree admin` is invoked directly. Always
@ -911,7 +85,6 @@ livefs_transaction_execute (RpmostreedTransaction *transaction,
return ret; return ret;
} }
static void static void
livefs_transaction_class_init (LiveFsTransactionClass *class) livefs_transaction_class_init (LiveFsTransactionClass *class)
{ {
@ -931,7 +104,7 @@ livefs_transaction_init (LiveFsTransaction *self)
RpmostreedTransaction * RpmostreedTransaction *
rpmostreed_transaction_new_livefs (GDBusMethodInvocation *invocation, rpmostreed_transaction_new_livefs (GDBusMethodInvocation *invocation,
OstreeSysroot *sysroot, OstreeSysroot *sysroot,
RpmOstreeTransactionLiveFsFlags flags, GVariant *options,
GCancellable *cancellable, GCancellable *cancellable,
GError **error) GError **error)
{ {
@ -948,7 +121,7 @@ rpmostreed_transaction_new_livefs (GDBusMethodInvocation *invocation,
if (self != NULL) if (self != NULL)
{ {
self->flags = flags; self->options = g_variant_ref (options);
} }
return (RpmostreedTransaction *) self; return (RpmostreedTransaction *) self;

View File

@ -1232,7 +1232,7 @@ deploy_transaction_execute (RpmostreedTransaction *transaction,
rpmostree_sysroot_upgrader_get_merge_deployment (upgrader); rpmostree_sysroot_upgrader_get_merge_deployment (upgrader);
gboolean is_live; 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; return FALSE;
if (is_live) if (is_live)

View File

@ -114,15 +114,10 @@ rpmostreed_transaction_new_cleanup (GDBusMethodInvocation *invocation,
GCancellable *cancellable, GCancellable *cancellable,
GError **error); GError **error);
typedef enum {
RPMOSTREE_TRANSACTION_LIVEFS_FLAG_DRY_RUN = (1 << 0),
RPMOSTREE_TRANSACTION_LIVEFS_FLAG_REPLACE = (1 << 1),
} RpmOstreeTransactionLiveFsFlags;
RpmostreedTransaction * RpmostreedTransaction *
rpmostreed_transaction_new_livefs (GDBusMethodInvocation *invocation, rpmostreed_transaction_new_livefs (GDBusMethodInvocation *invocation,
OstreeSysroot *sysroot, OstreeSysroot *sysroot,
RpmOstreeTransactionLiveFsFlags flags, GVariant *options,
GCancellable *cancellable, GCancellable *cancellable,
GError **error); GError **error);

View File

@ -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 */ /* this is already covered by the above, but the below also updates the cached value */
rpmostree_origin_set_override_commit (origin, NULL, NULL); rpmostree_origin_set_override_commit (origin, NULL, NULL);
/* then rpm-ostree specific things */
rpmostree_origin_set_live_state (origin, NULL, NULL);
} }
const char * const char *
@ -363,17 +360,6 @@ rpmostree_origin_may_require_local_assembly (RpmOstreeOrigin *origin)
(g_hash_table_size (origin->cached_overrides_remove) > 0); (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 * GKeyFile *
rpmostree_origin_dup_keyfile (RpmOstreeOrigin *origin) 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); 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 static void
update_keyfile_pkgs_from_cache (RpmOstreeOrigin *origin, update_keyfile_pkgs_from_cache (RpmOstreeOrigin *origin,
const char *group, const char *group,

View File

@ -110,13 +110,6 @@ rpmostree_origin_get_unconfigured_state (RpmOstreeOrigin *origin);
gboolean gboolean
rpmostree_origin_may_require_local_assembly (RpmOstreeOrigin *origin); 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 * char *
rpmostree_origin_get_string (RpmOstreeOrigin *origin, rpmostree_origin_get_string (RpmOstreeOrigin *origin,
const char *section, const char *section,
@ -207,8 +200,3 @@ gboolean
rpmostree_origin_remove_all_overrides (RpmOstreeOrigin *origin, rpmostree_origin_remove_all_overrides (RpmOstreeOrigin *origin,
gboolean *out_changed, gboolean *out_changed,
GError **error); GError **error);
void
rpmostree_origin_set_live_state (RpmOstreeOrigin *origin,
const char *inprogress,
const char *live);

View File

@ -36,4 +36,6 @@ b(GPtrArray)
b(GChecksum) b(GChecksum)
b(OstreeRepo) b(OstreeRepo)
b(RpmOstreeOrigin) b(RpmOstreeOrigin)
b(OstreeDeployment)
b(OstreeSysroot)
#undef b #undef b

View File

@ -108,7 +108,7 @@ vm_build_rpm scriptpkg2 \
vm_build_rpm scriptpkg3 \ vm_build_rpm scriptpkg3 \
post 'echo %%{_prefix} > /usr/lib/noprefixtest.txt' post 'echo %%{_prefix} > /usr/lib/noprefixtest.txt'
vm_rpmostree pkg-add scriptpkg{2,3} 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 vm_cmd cat /usr/lib/noprefixtest.txt > noprefixtest.txt
assert_file_has_content noprefixtest.txt '%{_prefix}' assert_file_has_content noprefixtest.txt '%{_prefix}'
vm_cmd cat /usr/lib/prefixtest.txt > prefixtest.txt 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 <lua>" \ post_args "-e -p <lua>" \
post 'posix.stat("/")' post 'posix.stat("/")'
vm_rpmostree install rpmostree-lua-override-test{,-expand} 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 vm_cmd cat /usr/share/rpmostree-lua-override-test > lua-override.txt
assert_file_has_content lua-override.txt _install_langs assert_file_has_content lua-override.txt _install_langs
vm_cmd rpm --eval '%{_install_langs}' > install-langs.txt 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' \ 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' 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 pkg-add scriptpkg{4,5}
vm_rpmostree ex livefs --i-like-danger vm_rpmostree ex livefs
for combo in ${license_combos}; do for combo in ${license_combos}; do
vm_cmd cat /usr/share/transfiletriggerin-license-${combo}.txt > transfiletriggerin-license-${combo}.txt vm_cmd cat /usr/share/transfiletriggerin-license-${combo}.txt > transfiletriggerin-license-${combo}.txt
rm -f transfiletriggerin-fs-${combo}.txt.tmp rm -f transfiletriggerin-fs-${combo}.txt.tmp
@ -229,7 +229,7 @@ echo "ok impervious to rm -rf post"
# capabilities # capabilities
vm_build_rpm test-cap-drop post "capsh --print > /usr/share/rpmostree-capsh.txt" vm_build_rpm test-cap-drop post "capsh --print > /usr/share/rpmostree-capsh.txt"
vm_rpmostree install test-cap-drop 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 vm_cmd cat /usr/share/rpmostree-capsh.txt > caps.txt
assert_not_file_has_content caps.test '^Current: =.*cap_sys_admin' assert_not_file_has_content caps.test '^Current: =.*cap_sys_admin'

View File

@ -31,6 +31,7 @@ vm_rpmostree cleanup -pr
vm_assert_layered_pkg foo absent vm_assert_layered_pkg foo absent
vm_build_rpm foo 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_rpmostree install /var/tmp/vmcheck/yumrepo/packages/x86_64/foo-1.0-1.x86_64.rpm
vm_assert_status_jq '.deployments|length == 2' vm_assert_status_jq '.deployments|length == 2'
echo "ok install foo locally" echo "ok install foo locally"
@ -38,22 +39,36 @@ echo "ok install foo locally"
if vm_cmd rpm -q foo; then if vm_cmd rpm -q foo; then
assert_not_reached "have foo?" assert_not_reached "have foo?"
fi 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' \ vm_assert_status_jq '.deployments|length == 2' \
'.deployments[0]["live-replaced"]|not' \ '.deployments[0]["live-replaced"]|not' \
'.deployments[1]["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 vm_cmd rpm -q foo > rpmq.txt
assert_file_has_content rpmq.txt foo-1.0-1 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"]' '.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 \ vm_build_rpm test-livefs-with-etc \
build 'echo "A config file for %{name}" > %{name}.conf' \ 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 /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 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
vm_rpmostree ex livefs --i-like-danger vm_cmd rpm -q bar test-livefs-{with-etc,service} > rpmq.txt
vm_cmd rpm -q foo 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
assert_file_has_content rpmq.txt foo-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 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" 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 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 vm_cmd test -d /var/lib/test-livefs-service
echo "ok livefs stage2" 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 <<EOF
cd /ostree/repo/tmp
rm vmcheck -rf
ostree checkout vmcheck vmcheck --fsync=0
(date; echo "JUST KIDDING DO WHATEVER") >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"