diff --git a/rust/src/history.rs b/rust/src/history.rs index 995cd86479..5b84abcd45 100644 --- a/rust/src/history.rs +++ b/rust/src/history.rs @@ -16,6 +16,42 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ +//! High-level interface to retrieve host RPM-OSTree history. The two main C +//! APIs are `ror_history_ctx_new()` which creates a context object +//! (`HistoryCtx`), and `ror_history_ctx_next()`, which iterates through history +//! entries (`HistoryEntry`). +//! +//! The basic idea is that at deployment creation time, the upgrader does two +//! things: (1) it writes a GVariant file describing the deployment to +//! `/var/lib/rpm-ostree/history` and (2) it logs a journal message. These two +//! pieces are tied together through the deployment root timestamp (which is +//! used as the filename for the GVariant and is included in the message under +//! `DEPLOYMENT_TIMESTAMP`). Thus, we can retrieve the GVariant corresponding to +//! a specific journal message. See the upgrader code for more details. +//! +//! This journal message also includes the deployment path in `DEPLOYMENT_PATH`. +//! At boot time, `ostree-prepare-root` logs the resolved deployment path +//! in *its* message's `DEPLOYMENT_PATH` too. Thus, we can tie together boot +//! messages with their corresponding deployment messages. To do this, we do +//! something akin to the following: +//! +//! - starting from the most recent journal entry, go backwards searching for +//! OSTree boot messages +//! - when a boot message is found, keep going backwards to find its matching +//! RPM-OSTree deploy message by comparing the two messages' deployment path +//! fields +//! - when a match is found, return a `HistoryEntry` +//! - start up the search again for the next boot message +//! +//! There's some added complexity to deal with ordering between boot events and +//! deployment events, and some "reboot" squashing to yield a single +//! `HistoryEntry` if the system booted into the same deployment multiple times +//! in a row. +//! +//! The algorithm is streaming, i.e. it yields entries as it finds them, rather +//! than scanning the whole journal upfront. This can then be e.g. piped through +//! a pager, stopped after N entries, etc... + use failure::{bail, Fallible}; use openat::{Dir, SimpleType}; use std::collections::VecDeque; @@ -35,29 +71,68 @@ static OSTREE_BOOT_MSG: &'static str = "7170336a73ba4601bad31af888aa0df7"; // msg rpm-ostree emits when it creates the deployment */ static RPMOSTREE_DEPLOY_MSG: &'static str = "9bddbda177cd44d891b1b561a8a0ce9e"; -static RPMOSTREE_UPGRADER_DATA_DIR: &'static str = "/var/lib/rpm-ostree/history"; +static RPMOSTREE_HISTORY_DIR: &'static str = "/var/lib/rpm-ostree/history"; +/// Context object used to iterate through `HistoryEntry` events. pub struct HistoryCtx { journal: journal::Journal, - event_queue: VecDeque, + marker_queue: VecDeque, current_entry: Option, - search_mode: Option, + search_mode: Option, reached_eof: bool, } -// This whole struct is exposed to the C side. +// Markers are essentially deserialized journal messages, where all the +// interesting bits have been parsed out. + +/// Marker for OSTree boot messages. +struct BootMarker { + timestamp: u64, + path: String, + node: DevIno, +} + +/// Marker for RPM-OSTree deployment messages. +#[derive(Clone)] +struct DeploymentMarker { + timestamp: u64, + path: String, + node: DevIno, + cmdline: Option, +} + +enum Marker { + Boot(BootMarker), + Deployment(DeploymentMarker), +} + +#[derive(Clone, PartialEq)] +struct DevIno { + device: u64, + inode: u64, +} + +/// A history entry in the journal. It may represent multiple consecutive boots +/// into the same deployment. This struct is exposed directly via FFI to C. #[repr(C)] #[derive(PartialEq, Debug)] pub struct HistoryEntry { - first_boot_timestamp: u64, - last_boot_timestamp: u64, + /// The deployment root timestamp. deploy_timestamp: u64, + /// The command-line that was used to create the deployment, if any. deploy_cmdline: *mut libc::c_char, + /// The number of consecutive times the deployment was booted. boot_count: u64, + /// The first time the deployment was booted if multiple consecutive times. + first_boot_timestamp: u64, + /// The last time the deployment was booted if multiple consecutive times. + last_boot_timestamp: u64, + /// `true` if there are no more entries. eof: bool, } impl HistoryEntry { + /// Create a new `HistoryEntry` from a boot marker and a deployment marker. fn new_from_markers(boot: BootMarker, deploy: DeploymentMarker) -> HistoryEntry { HistoryEntry { first_boot_timestamp: boot.timestamp, @@ -84,33 +159,8 @@ impl HistoryEntry { } } -enum HistoryEvent { - Boot(BootMarker), - Deployment(DeploymentMarker), -} - -struct BootMarker { - timestamp: u64, - path: String, - node: DevIno, -} - -#[derive(Clone)] -pub struct DeploymentMarker { - timestamp: u64, - path: String, - node: DevIno, - cmdline: Option, -} - -#[derive(Clone, PartialEq)] -struct DevIno { - device: u64, - inode: u64, -} - #[derive(PartialEq)] -enum SearchMode { +enum JournalSearchMode { BootMsgs, BootAndDeploymentMsgs, } @@ -147,12 +197,15 @@ fn history_get_oldest_deployment_msg_timestamp() -> Fallible> { Ok(None) } +/// Gets the oldest deployment message in the journal, and nuke all the GVariant data files +/// that correspond to deployments older than that one. Essentially, this binds pruning to +/// journal pruning. Called from C through `ror_history_prune()`. fn history_prune() -> Fallible<()> { let oldest_timestamp = history_get_oldest_deployment_msg_timestamp()?; // Cleanup any entry older than the oldest entry in the journal. Also nuke anything else that // doesn't belong here; we own this dir. - let dir = Dir::open(RPMOSTREE_UPGRADER_DATA_DIR)?; + let dir = Dir::open(RPMOSTREE_HISTORY_DIR)?; for entry in dir.list_dir(".")? { let entry = entry?; let ftype = match entry.simple_type() { @@ -173,7 +226,7 @@ fn history_prune() -> Fallible<()> { } if ftype == SimpleType::Dir { - fs::remove_dir_all(Path::new(RPMOSTREE_UPGRADER_DATA_DIR).join(fname))?; + fs::remove_dir_all(Path::new(RPMOSTREE_HISTORY_DIR).join(fname))?; } else { dir.remove_file(fname)?; } @@ -183,13 +236,14 @@ fn history_prune() -> Fallible<()> { } impl HistoryCtx { + /// Create a new context object. Called from C through `ror_history_ctx_new()`. fn new_boxed() -> Fallible> { let mut journal = journal::Journal::open(journal::JournalFiles::System, false, true)?; journal.seek(journal::JournalSeek::Tail)?; Ok(Box::new(HistoryCtx { journal: journal, - event_queue: VecDeque::new(), + marker_queue: VecDeque::new(), current_entry: None, search_mode: None, reached_eof: false, @@ -197,11 +251,11 @@ impl HistoryCtx { } /// Ensures the journal filters are set up for the messages we're interested in. - fn set_search_mode(&mut self, mode: SearchMode) -> Fallible<()> { + fn set_search_mode(&mut self, mode: JournalSearchMode) -> Fallible<()> { if Some(&mode) != self.search_mode.as_ref() { self.journal.match_flush()?; self.journal.match_add("MESSAGE_ID", OSTREE_BOOT_MSG)?; - if mode == SearchMode::BootAndDeploymentMsgs { + if mode == JournalSearchMode::BootAndDeploymentMsgs { self.journal.match_add("MESSAGE_ID", RPMOSTREE_DEPLOY_MSG)?; } self.search_mode = Some(mode); @@ -209,41 +263,34 @@ impl HistoryCtx { Ok(()) } - /// Assembles a BootMarker from a record. Returns None if record is incomplete. - fn boot_record_to_marker(&self, record: &JournalRecord) -> Fallible> { + /// Creates a marker from an OSTree boot message. Uses the timestamp of the message + /// itself as the boot time. Returns None if record is incomplete. + fn boot_record_to_marker(&self, record: &JournalRecord) -> Fallible> { if let (Some(path), Some(device), Some(inode)) = ( record.get("DEPLOYMENT_PATH"), map_to_u64(record.get("DEPLOYMENT_DEVICE")), map_to_u64(record.get("DEPLOYMENT_INODE")), ) { - return Ok(Some(BootMarker { + return Ok(Some(Marker::Boot(BootMarker { timestamp: journal_record_timestamp(&self.journal)?, path: path.clone(), node: DevIno { device, inode }, - })); + }))); } Ok(None) } - /// Creates an event from an OSTree boot message. Uses the timestamp of the message itself as - /// the boot time. Returns None if record is incomplete. - fn boot_record_to_event(&self, record: &JournalRecord) -> Fallible> { - Ok(self - .boot_record_to_marker(&record)? - .map(|m| HistoryEvent::Boot(m))) - } - - /// Creates an event from an RPM-OSTree deploy message. Uses the `DEPLOYMENT_TIMESTAMP` in the - /// message as the deploy time. This matches the history gv filename for that deployment. - /// Returns None if record is incomplete. - fn deployment_record_to_event(&self, record: &JournalRecord) -> Fallible> { + /// Creates a marker from an RPM-OSTree deploy message. Uses the `DEPLOYMENT_TIMESTAMP` + /// in the message as the deploy time. This matches the history gv filename for that + /// deployment. Returns None if record is incomplete. + fn deployment_record_to_marker(&self, record: &JournalRecord) -> Fallible> { if let (Some(timestamp), Some(device), Some(inode), Some(path)) = ( map_to_u64(record.get("DEPLOYMENT_TIMESTAMP")), map_to_u64(record.get("DEPLOYMENT_DEVICE")), map_to_u64(record.get("DEPLOYMENT_INODE")), record.get("DEPLOYMENT_PATH"), ) { - return Ok(Some(HistoryEvent::Deployment(DeploymentMarker { + return Ok(Some(Marker::Deployment(DeploymentMarker { timestamp, node: DevIno { device, inode }, path: path.clone(), @@ -255,32 +302,33 @@ impl HistoryCtx { Ok(None) } - fn record_to_event(&self, record: &JournalRecord) -> Fallible> { - Ok(match record.get("MESSAGE_ID").unwrap() { - m if m == OSTREE_BOOT_MSG => self.boot_record_to_event(&record)?, - m if m == RPMOSTREE_DEPLOY_MSG => self.deployment_record_to_event(&record)?, - m => panic!("matched an unwanted message: {:?}", m), - }) - } - /// Goes to the next OSTree boot msg in the journal and returns its marker. - fn find_next_boot(&mut self) -> Fallible> { - self.set_search_mode(SearchMode::BootMsgs)?; + fn find_next_boot_marker(&mut self) -> Fallible> { + self.set_search_mode(JournalSearchMode::BootMsgs)?; while let Some(rec) = self.journal.previous_record()? { - if let Some(m) = self.boot_record_to_marker(&rec)? { + if let Some(Marker::Boot(m)) = self.boot_record_to_marker(&rec)? { return Ok(Some(m)); } } Ok(None) } - /// Goes to the next OSTree boot or RPM-OSTree deploy msg in the journal, creates an event for - /// it, and returns it. - fn find_next_event(&mut self) -> Fallible> { - self.set_search_mode(SearchMode::BootAndDeploymentMsgs)?; + /// Returns a marker of the appropriate kind for a given journal message. + fn record_to_marker(&self, record: &JournalRecord) -> Fallible> { + Ok(match record.get("MESSAGE_ID").unwrap() { + m if m == OSTREE_BOOT_MSG => self.boot_record_to_marker(&record)?, + m if m == RPMOSTREE_DEPLOY_MSG => self.deployment_record_to_marker(&record)?, + m => panic!("matched an unwanted message: {:?}", m), + }) + } + + /// Goes to the next OSTree boot or RPM-OSTree deploy msg in the journal, creates a + /// marker for it, and returns it. + fn find_next_marker(&mut self) -> Fallible> { + self.set_search_mode(JournalSearchMode::BootAndDeploymentMsgs)?; while let Some(rec) = self.journal.previous_record()? { - if let Some(event) = self.record_to_event(&rec)? { - return Ok(Some(event)); + if let Some(marker) = self.record_to_marker(&rec)? { + return Ok(Some(marker)); } } Ok(None) @@ -290,10 +338,10 @@ impl HistoryCtx { fn scan_until_path_match(&mut self) -> Fallible> { // keep popping & scanning until we get to the next boot marker let boot_marker = loop { - match self.event_queue.pop_front() { - Some(HistoryEvent::Boot(m)) => break m, - Some(HistoryEvent::Deployment(_)) => continue, - None => match self.find_next_boot()? { + match self.marker_queue.pop_front() { + Some(Marker::Boot(m)) => break m, + Some(Marker::Deployment(_)) => continue, + None => match self.find_next_boot_marker()? { Some(m) => break m, None => return Ok(None), }, @@ -301,8 +349,8 @@ impl HistoryCtx { }; // check if its corresponding deployment is already in the queue - for event in self.event_queue.iter() { - if let HistoryEvent::Deployment(m) = event { + for marker in self.marker_queue.iter() { + if let Marker::Deployment(m) = marker { if m.path == boot_marker.path { return Ok(Some((boot_marker, m.clone()))); } @@ -310,12 +358,12 @@ impl HistoryCtx { } // keep collecting until we get a matching path - while let Some(event) = self.find_next_event()? { - self.event_queue.push_back(event); + while let Some(marker) = self.find_next_marker()? { + self.marker_queue.push_back(marker); // ...and now borrow it back; might be a cleaner way to do this - let event = self.event_queue.back().unwrap(); + let marker = self.marker_queue.back().unwrap(); - if let HistoryEvent::Deployment(m) = event { + if let Marker::Deployment(m) = marker { if m.path == boot_marker.path { return Ok(Some((boot_marker, m.clone()))); } @@ -325,8 +373,8 @@ impl HistoryCtx { Ok(None) } - /// Returns the next history entry, which consists of a boot timestamp and its matching deploy - /// timestamp. + /// Returns the next history entry, which consists of a boot timestamp and its matching + /// deploy timestamp. fn scan_until_next_entry(&mut self) -> Fallible> { while let Some((boot_marker, deployment_marker)) = self.scan_until_path_match()? { if boot_marker.node != deployment_marker.node { @@ -346,9 +394,10 @@ impl HistoryCtx { Ok(None) } - /// Returns the next *new* event. This essentially collapses multiple subsequent boots of the - /// same deployment into a single entry. The `boot_count` field represents the number of boots - /// squashed, and `*_boot_timestamp` fields provide the timestamp of the first and last boots. + /// Returns the next *new* entry. This essentially collapses multiple subsequent boots + /// of the same deployment into a single entry. The `boot_count` field represents the + /// number of boots squashed, and `*_boot_timestamp` fields provide the timestamp of the + /// first and last boots. fn scan_until_next_new_entry(&mut self) -> Fallible> { while let Some(entry) = self.scan_until_next_entry()? { if self.current_entry.is_none() { @@ -373,8 +422,9 @@ impl HistoryCtx { Ok(self.current_entry.take()) } - /// Returns the next entry. This is a thin wrapper around `scan_until_next_new_entry` that - /// mostly just handles the `Option` -> EOF conversion for the C side. + /// Returns the next entry. This is a thin wrapper around `scan_until_next_new_entry` + /// that mostly just handles the `Option` -> EOF conversion for the C side. Called from + /// C through `ror_history_ctx_next()`. fn next_entry(&mut self) -> Fallible { if self.reached_eof { bail!("next_entry() called after having reached EOF!") @@ -390,8 +440,8 @@ impl HistoryCtx { } } -/// A minimal mock journal interface so we can unit test various code paths without adding stuff in -/// the host journal; in fact without needing any system journal access at all. +/// A minimal mock journal interface so we can unit test various code paths without adding +/// stuff in the host journal; in fact without needing any system journal access at all. #[cfg(test)] mod mock_journal { use super::Fallible; diff --git a/src/app/rpmostree-builtin-status.c b/src/app/rpmostree-builtin-status.c index 71ffbfca14..f5cecf17ab 100644 --- a/src/app/rpmostree-builtin-status.c +++ b/src/app/rpmostree-builtin-status.c @@ -1090,7 +1090,7 @@ fetch_history_deployment_gvariant (RORHistoryEntry *entry, GError **error) { g_autofree char *fn = - g_strdup_printf ("%s/%lu", RPMOSTREE_UPGRADER_HISTORY_DIR, entry->deploy_timestamp); + g_strdup_printf ("%s/%lu", RPMOSTREE_HISTORY_DIR, entry->deploy_timestamp); *out_deployment = NULL; diff --git a/src/app/rpmostree-ex-builtin-history.c b/src/app/rpmostree-ex-builtin-history.c deleted file mode 100644 index c353f23bff..0000000000 --- a/src/app/rpmostree-ex-builtin-history.c +++ /dev/null @@ -1,52 +0,0 @@ -/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- - * - * Copyright (C) 2019 Jonathan Lebon - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published - * by the Free Software Foundation; either version 2 of the licence or (at - * your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General - * Public License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place, Suite 330, - * Boston, MA 02111-1307, USA. - */ - -#include "config.h" - -#include "rpmostree-ex-builtins.h" - -static gboolean opt_verbose; - -static GOptionEntry option_entries[] = { - { "verbose", 'v', 0, G_OPTION_ARG_NONE, &opt_verbose, "Print additional fields", NULL }, - { NULL } -}; - -gboolean -rpmostree_ex_builtin_history (int argc, - char **argv, - RpmOstreeCommandInvocation *invocation, - GCancellable *cancellable, - GError **error) -{ - g_autoptr(GOptionContext) context = g_option_context_new (""); - if (!rpmostree_option_context_parse (context, - option_entries, - &argc, &argv, - invocation, - cancellable, - NULL, NULL, NULL, NULL, NULL, - error)) - return FALSE; - - g_print ("hello world\n"); - - return TRUE; -} diff --git a/src/daemon/rpmostree-sysroot-upgrader.c b/src/daemon/rpmostree-sysroot-upgrader.c index 2c00959cd0..9aad2133ea 100644 --- a/src/daemon/rpmostree-sysroot-upgrader.c +++ b/src/daemon/rpmostree-sysroot-upgrader.c @@ -1253,11 +1253,19 @@ write_history (RpmOstreeSysrootUpgrader *self, return FALSE; g_autofree char *fn = - g_strdup_printf ("%s/%ld", RPMOSTREE_UPGRADER_HISTORY_DIR, stbuf.st_ctime); - if (!glnx_shutil_mkdir_p_at (AT_FDCWD, RPMOSTREE_UPGRADER_HISTORY_DIR, + g_strdup_printf ("%s/%ld", RPMOSTREE_HISTORY_DIR, stbuf.st_ctime); + if (!glnx_shutil_mkdir_p_at (AT_FDCWD, RPMOSTREE_HISTORY_DIR, 0775, cancellable, error)) return FALSE; + /* Write out GVariant to a file. One obvious question here is: why not keep this in the + * journal itself since it supports binary data? We *could* do this, and it would simplify + * querying and pruning, but IMO I find binary data in journal messages not appealing and + * it breaks the expectation that journal messages should be somewhat easily + * introspectable. We could also serialize it to JSON first, though we wouldn't be able to + * re-use the printing code in `status.c` as is. Note also the GVariant can be large (e.g. + * we include the full `rpmostree.rpmdb.pkglist` in there). */ + if (!glnx_file_replace_contents_at (AT_FDCWD, fn, g_variant_get_data (deployment_variant), g_variant_get_size (deployment_variant), diff --git a/src/libpriv/rpmostree-core.h b/src/libpriv/rpmostree-core.h index b9abd8a4eb..4ca1766486 100644 --- a/src/libpriv/rpmostree-core.h +++ b/src/libpriv/rpmostree-core.h @@ -40,8 +40,8 @@ /* put it in cache dir so it gets destroyed naturally with a `cleanup -m` */ #define RPMOSTREE_AUTOUPDATES_CACHE_FILE RPMOSTREE_CORE_CACHEDIR "cached-update.gv" -#define RPMOSTREE_UPGRADER_DATA_DIR "/var/lib/rpm-ostree/" -#define RPMOSTREE_UPGRADER_HISTORY_DIR RPMOSTREE_UPGRADER_DATA_DIR "history" +#define RPMOSTREE_STATE_DIR "/var/lib/rpm-ostree/" +#define RPMOSTREE_HISTORY_DIR RPMOSTREE_STATE_DIR "history" #define RPMOSTREE_TYPE_CONTEXT (rpmostree_context_get_type ()) G_DECLARE_FINAL_TYPE (RpmOstreeContext, rpmostree_context, RPMOSTREE, CONTEXT, GObject)