apply-live: Rework to use refs to store state

Came out of discussion in https://github.com/coreos/rpm-ostree/pull/2581
around some racy code for checking for the live commit object.

The reliability of apply-live depends on the
underlying commits not being garbage collected.  Our diff logic
is in terms of ostree commits, not the physical filesystem (this
allows us to make various optimizations too).

Ultimately I think we should drive some of the live-apply
logic into libostree itself; we can more easily have an atomic
state file instead of the two split refs.

(Or perhaps what we should add to ostree is like a refs.d model
 where a single atomic file can refer to multiple commits)

For now though let's rework the code here to write refs.  We
retain the file in `/run` as just a "stamp file" that signals
that a deployment has had `apply-live` run.
This commit is contained in:
Colin Walters 2021-02-18 23:13:04 +00:00 committed by OpenShift Merge Robot
parent ce20267b2d
commit 5a79ca9035
5 changed files with 98 additions and 97 deletions

View File

@ -203,6 +203,7 @@ pub mod ffi {
sysroot: Pin<&mut OstreeSysroot>,
deployment: Pin<&mut OstreeDeployment>,
) -> Result<bool>;
fn applylive_sync_ref(sysroot: Pin<&mut OstreeSysroot>) -> Result<()>;
// FIXME/cxx make this Option<&str>
fn transaction_apply_live(sysroot: Pin<&mut OstreeSysroot>, target: &str) -> Result<()>;
fn applylive_client_finish() -> Result<()>;

View File

@ -14,7 +14,6 @@ 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};
@ -25,40 +24,14 @@ use std::process::Command;
/// 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 LIVE_STATE_NAME: &str = "rpmostree-live-state.json";
/// The model for live state. This representation is
/// just used "on disk" right now because
/// TODO(cxx-rs) doesn't support Option<T>
#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
struct LiveApplyStateSerialized {
/// The OSTree commit that the running root filesystem is using,
/// as distinct from the one it was booted with.
commit: Option<String>,
/// Set when an apply-live operation is in progress; if the process
/// is interrupted, some files from this commit may exist
/// on disk but in an incomplete state.
inprogress: Option<String>,
}
impl From<&LiveApplyStateSerialized> for LiveApplyState {
fn from(s: &LiveApplyStateSerialized) -> LiveApplyState {
LiveApplyState {
inprogress: s.inprogress.clone().unwrap_or_default(),
commit: s.commit.clone().unwrap_or_default(),
}
}
}
impl From<&LiveApplyState> for LiveApplyStateSerialized {
fn from(s: &LiveApplyState) -> LiveApplyStateSerialized {
LiveApplyStateSerialized {
inprogress: Some(s.inprogress.clone()).filter(|s| !s.is_empty()),
commit: Some(s.commit.clone()).filter(|s| !s.is_empty()),
}
}
}
/// Stamp file used to signal deployment was live-applied, stored in the above directory
const LIVE_STATE_NAME: &str = "rpmostree-is-live.stamp";
/// OSTree ref that follows the live state
const LIVE_REF: &str = "rpmostree/live-apply";
/// OSTree ref that will be set to the commit we are currently
/// updating to; if the process is interrupted, we can then
/// more reliably resynchronize.
const LIVE_REF_INPROGRESS: &str = "rpmostree/live-apply-inprogress";
/// Get the transient state directory for a deployment; TODO
/// upstream this into libostree.
@ -73,25 +46,53 @@ fn get_runstate_dir(deploy: &ostree::Deployment) -> PathBuf {
}
/// Get the live state
fn get_live_state(deploy: &ostree::Deployment) -> Result<Option<LiveApplyState>> {
fn get_live_state(
repo: &ostree::Repo,
deploy: &ostree::Deployment,
) -> Result<Option<LiveApplyState>> {
let root = openat::Dir::open("/")?;
if let Some(f) = root.open_file_optional(&get_runstate_dir(deploy).join(LIVE_STATE_NAME))? {
let s: LiveApplyStateSerialized = serde_json::from_reader(std::io::BufReader::new(f))?;
let s = &s;
Ok(Some(s.into()))
} else {
Ok(None)
if !root.exists(&get_runstate_dir(deploy).join(LIVE_STATE_NAME))? {
return Ok(None);
}
let live_commit = crate::ostree_utils::repo_resolve_ref_optional(repo, LIVE_REF)?;
let inprogress_commit =
crate::ostree_utils::repo_resolve_ref_optional(repo, LIVE_REF_INPROGRESS)?;
Ok(Some(LiveApplyState {
commit: live_commit.map(|s| s.to_string()).unwrap_or_default(),
inprogress: inprogress_commit.map(|s| s.to_string()).unwrap_or_default(),
}))
}
/// Write new livefs state
fn write_live_state(deploy: &ostree::Deployment, state: &LiveApplyState) -> Result<()> {
let rundir = get_runstate_dir(deploy);
let rundir = openat::Dir::open(&rundir)?;
let state: LiveApplyStateSerialized = state.into();
rundir.write_file_with(LIVE_STATE_NAME, 0o644, |w| -> Result<_> {
Ok(serde_json::to_writer(w, &state)?)
})?;
fn write_live_state(
repo: &ostree::Repo,
deploy: &ostree::Deployment,
state: &LiveApplyState,
) -> Result<()> {
let root = openat::Dir::open("/")?;
let rundir = if let Some(d) = root.sub_dir_optional(&get_runstate_dir(deploy))? {
d
} else {
return Ok(());
};
let found_live_stamp = rundir.exists(LIVE_STATE_NAME)?;
let commit = Some(state.commit.as_str()).filter(|s| !s.is_empty());
repo.set_ref_immediate(None, LIVE_REF, commit, gio::NONE_CANCELLABLE)?;
let inprogress_commit = Some(state.inprogress.as_str()).filter(|s| !s.is_empty());
repo.set_ref_immediate(
None,
LIVE_REF_INPROGRESS,
inprogress_commit,
gio::NONE_CANCELLABLE,
)?;
// Ensure the stamp file exists
if !found_live_stamp && commit.or(inprogress_commit).is_some() {
rundir.write_file_contents(LIVE_STATE_NAME, 0o644, b"")?;
}
Ok(())
}
@ -379,7 +380,7 @@ pub(crate) fn transaction_apply_live(
}
};
let state = get_live_state(&booted)?;
let state = get_live_state(repo, &booted)?;
if state.is_none() {
match booted.get_unlocked() {
DeploymentUnlockedState::None => {
@ -443,7 +444,7 @@ pub(crate) fn transaction_apply_live(
// Record that we're targeting this commit
state.inprogress = target_commit.to_string();
write_live_state(&booted, &state)?;
write_live_state(&repo, &booted, &state)?;
// Gather the current diff of /etc - we need to avoid changing
// any files which are locally modified.
@ -479,11 +480,34 @@ pub(crate) fn transaction_apply_live(
// Success! Update the recorded state.
state.commit = target_commit.to_string();
state.inprogress = "".to_string();
write_live_state(&booted, &state)?;
write_live_state(&repo, &booted, &state)?;
Ok(())
}
/// Writing a ref for the live-apply state can get out of sync
/// if we upgrade. This prunes the ref if the booted deployment
/// doesn't have a live apply state in /run.
pub(crate) fn applylive_sync_ref(
mut sysroot: Pin<&mut crate::ffi::OstreeSysroot>,
) -> CxxResult<()> {
let sysroot = sysroot.gobj_wrap();
let repo = &sysroot.get_repo(gio::NONE_CANCELLABLE)?;
let booted = if let Some(b) = sysroot.get_booted_deployment() {
b
} else {
return Ok(());
};
if get_live_state(&repo, &booted)?.is_some() {
return Ok(());
}
// Set the live state to empty
let state = Default::default();
write_live_state(&repo, &booted, &state).context("apply-live: failed to write state")?;
Ok(())
}
pub(crate) fn applylive_client_finish() -> CxxResult<()> {
let cancellable = gio::NONE_CANCELLABLE;
let sysroot = &ostree::Sysroot::new_default();
@ -493,7 +517,7 @@ pub(crate) fn applylive_client_finish() -> CxxResult<()> {
let booted_commit = booted.get_csum().expect("csum");
let booted_commit = booted_commit.as_str();
let live_state = get_live_state(booted)?
let live_state = get_live_state(repo, booted)?
.ok_or_else(|| anyhow!("Failed to find expected apply-live state"))?;
let pkgdiff = {
@ -523,33 +547,16 @@ mod test {
let s = subpath(&d, Path::new("/foo"));
assert_eq!(s.as_ref().map(|s| s.as_path()), Some(Path::new("/usr/foo")));
}
#[test]
fn test_repr() {
let s: LiveApplyStateSerialized = Default::default();
let b: LiveApplyState = (&s).into();
assert_eq!(b.commit, "");
assert_eq!(b.inprogress, "");
let rs: LiveApplyStateSerialized = (&b).into();
assert_eq!(rs, s);
let s = LiveApplyStateSerialized {
commit: Some("42".to_string()),
inprogress: None,
};
let b: LiveApplyState = (&s).into();
assert_eq!(b.commit, "42");
assert_eq!(b.inprogress, "");
let rs: LiveApplyStateSerialized = (&b).into();
assert_eq!(rs, s);
}
}
pub(crate) fn get_live_apply_state(
mut _sysroot: Pin<&mut crate::ffi::OstreeSysroot>,
mut sysroot: Pin<&mut crate::ffi::OstreeSysroot>,
mut deployment: Pin<&mut crate::ffi::OstreeDeployment>,
) -> CxxResult<LiveApplyState> {
let sysroot = sysroot.gobj_wrap();
let deployment = deployment.gobj_wrap();
if let Some(state) = get_live_state(&deployment)? {
let repo = &sysroot.get_repo(gio::NONE_CANCELLABLE)?;
if let Some(state) = get_live_state(&repo, &deployment)? {
Ok(state)
} else {
Ok(Default::default())

View File

@ -307,30 +307,13 @@ rpmostree_syscore_cleanup (OstreeSysroot *sysroot,
cancellable, error))
return FALSE;
OstreeDeployment *booted = ostree_sysroot_get_booted_deployment (sysroot);
auto live_state = std::unique_ptr<rpmostreecxx::LiveApplyState>();
if (booted)
live_state = std::make_unique<rpmostreecxx::LiveApplyState>(rpmostreecxx::get_live_apply_state(*sysroot, *booted));
/* Refs for the live state */
rpmostreecxx::applylive_sync_ref(*sysroot);
/* 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_state != nullptr)
{
if (live_state->inprogress.length() > 0 &&
!ostree_repo_traverse_commit_union (repo, live_state->inprogress.c_str(), 0, reachable,
cancellable, error))
return FALSE;
if (live_state->commit.length() > 0 &&
!ostree_repo_traverse_commit_union (repo, live_state->commit.c_str(), 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,

View File

@ -5,7 +5,8 @@ set -xeuo pipefail
cd $(mktemp -d)
# make sure that package-related entries are always present,
# even when they're empty
# even when they're empty.
# Validate there's no live state by default.
rpm-ostree status --json > status.json
assert_jq status.json \
'.deployments[0]["packages"]' \
@ -13,6 +14,8 @@ assert_jq status.json \
'.deployments[0]["requested-local-packages"]' \
'.deployments[0]["base-removals"]' \
'.deployments[0]["requested-base-removals"]' \
'.deployments[0]["live-inprogress"]|not' \
'.deployments[0]["live-replaced"]|not' \
'.deployments[0]["layered-commit-meta"]|not'
rm status.json
rpm-ostree testutils validate-parse-status

View File

@ -29,13 +29,20 @@ vm_assert_layered_pkg foo absent
vm_cmd ostree refs $(vm_get_deployment_info 0 checksum) --create vmcheck_tmp/without_foo
vm_build_rpm foo version 1.2 release 3
vm_rpmostree install /var/tmp/vmcheck/yumrepo/packages/x86_64/foo-1.2-3.x86_64.rpm
vm_assert_status_jq '.deployments[0]["packages"]|length == 0' \
'.deployments[0]["requested-packages"]|length == 0' \
'.deployments[0]["requested-local-packages"]|length == 1' \
'.deployments[0]["live-inprogress"]|not' \
'.deployments[0]["live-replaced"]|not'
echo "ok install foo locally"
vm_reboot
vm_assert_status_jq '.deployments[0]["packages"]|length == 0'
vm_assert_status_jq '.deployments[0]["requested-packages"]|length == 0'
vm_assert_status_jq '.deployments[0]["requested-local-packages"]|length == 1'
vm_assert_status_jq '.deployments[0]["packages"]|length == 0' \
'.deployments[0]["requested-packages"]|length == 0' \
'.deployments[0]["requested-local-packages"]|length == 1' \
'.deployments[0]["live-inprogress"]|not' \
'.deployments[0]["live-replaced"]|not'
vm_has_local_packages foo-1.2-3.x86_64
vm_assert_layered_pkg foo-1.2-3.x86_64 present
echo "ok pkg foo added locally"