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:
parent
213d8f0aa2
commit
a76ddf0cef
@ -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"
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
499
rust/src/livefs.rs
Normal file
499
rust/src/livefs.rs
Normal 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
170
rust/src/ostree_diff.rs
Normal 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
20
rust/src/ostree_utils.rs
Normal 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))
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -29,15 +29,10 @@
|
||||
|
||||
#include <libglnx.h>
|
||||
|
||||
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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 */
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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<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
|
||||
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;
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -36,4 +36,6 @@ b(GPtrArray)
|
||||
b(GChecksum)
|
||||
b(OstreeRepo)
|
||||
b(RpmOstreeOrigin)
|
||||
b(OstreeDeployment)
|
||||
b(OstreeSysroot)
|
||||
#undef b
|
||||
|
@ -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 <lua>" \
|
||||
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'
|
||||
|
||||
|
@ -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 <<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"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user