Add new ex history command

This is the rpm-ostree equivalent of `dnf history`. As opposed to the
history of the refspec (i.e. `ostree log`), this shows the history of
the system, i.e. the refspecs the host deployed, checksums, versions,
layered packages, etc... The amount of details remembered is similar to
what shows up in `status`.

There's definitely some further enhancements possible (e.g. printing
package diffs, displaying rollbacks), though this seems in good enough
shape as a first cut.

Closes: #1489

Closes: #1813
Approved by: cgwalters
This commit is contained in:
Jonathan Lebon 2019-04-25 11:18:33 -04:00 committed by Atomic Bot
parent 9e2ceca06f
commit 10755592ea
10 changed files with 1154 additions and 0 deletions

View File

@ -3,6 +3,8 @@ language = "C"
header = "#pragma once\n#include <gio/gio.h>\ntypedef GError RORGError;\ntypedef GHashTable RORGHashTable;\ntypedef GPtrArray RORGPtrArray;"
trailer = """
G_DEFINE_AUTOPTR_CLEANUP_FUNC(RORTreefile, ror_treefile_free)
G_DEFINE_AUTOPTR_CLEANUP_FUNC(RORHistoryCtx, ror_history_ctx_free)
G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(RORHistoryEntry, ror_history_entry_clear)
"""

781
rust/src/history.rs Normal file
View File

@ -0,0 +1,781 @@
/*
* Copyright (C) 2019 Jonathan Lebon <jonathan@jlebon.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* 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::{self, Dir, SimpleType};
use std::collections::VecDeque;
use std::ffi::CString;
use std::ops::Deref;
use std::path::Path;
use std::{fs, ptr};
use systemd::journal::JournalRecord;
use openat_ext::OpenatDirExt;
#[cfg(test)]
use self::mock_journal as journal;
#[cfg(not(test))]
use systemd::journal;
// msg ostree-prepare-root emits at boot time when it resolved the deployment */
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_HISTORY_DIR: &'static str = "/var/lib/rpm-ostree/history";
/// Context object used to iterate through `HistoryEntry` events.
pub struct HistoryCtx {
journal: journal::Journal,
marker_queue: VecDeque<Marker>,
current_entry: Option<HistoryEntry>,
search_mode: Option<JournalSearchMode>,
reached_eof: bool,
}
// 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<CString>,
}
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 {
/// 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,
last_boot_timestamp: boot.timestamp,
deploy_timestamp: deploy.timestamp,
deploy_cmdline: deploy
.cmdline
.map(|s| s.into_raw())
.unwrap_or(ptr::null_mut()),
boot_count: 1,
eof: false,
}
}
fn eof() -> HistoryEntry {
HistoryEntry {
eof: true,
first_boot_timestamp: 0,
last_boot_timestamp: 0,
deploy_timestamp: 0,
deploy_cmdline: ptr::null_mut(),
boot_count: 0,
}
}
}
#[derive(PartialEq)]
enum JournalSearchMode {
BootMsgs,
BootAndDeploymentMsgs,
}
#[cfg(not(test))]
fn journal_record_timestamp(journal: &journal::Journal) -> Fallible<u64> {
Ok(journal
.timestamp()?
.duration_since(std::time::UNIX_EPOCH)?
.as_secs())
}
#[cfg(test)]
fn journal_record_timestamp(journal: &journal::Journal) -> Fallible<u64> {
Ok(journal.current_timestamp.unwrap())
}
fn map_to_u64<T>(s: Option<&T>) -> Option<u64>
where
T: Deref<Target = str>,
{
s.and_then(|s| s.parse::<u64>().ok())
}
fn history_get_oldest_deployment_msg_timestamp() -> Fallible<Option<u64>> {
let mut journal = journal::Journal::open(journal::JournalFiles::System, false, true)?;
journal.seek(journal::JournalSeek::Head)?;
journal.match_add("MESSAGE_ID", RPMOSTREE_DEPLOY_MSG)?;
while let Some(rec) = journal.next_record()? {
if let Some(ts) = map_to_u64(rec.get("DEPLOYMENT_TIMESTAMP")) {
return Ok(Some(ts));
}
}
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_HISTORY_DIR)?;
for entry in dir.list_dir(".")? {
let entry = entry?;
let ftype = dir.get_file_type(&entry)?;
let fname = entry.file_name();
if let Some(oldest_ts) = oldest_timestamp {
if ftype == SimpleType::File {
if let Some(ts) = map_to_u64(fname.to_str().as_ref()) {
if ts >= oldest_ts {
continue;
}
}
}
}
if ftype == SimpleType::Dir {
fs::remove_dir_all(Path::new(RPMOSTREE_HISTORY_DIR).join(fname))?;
} else {
dir.remove_file(fname)?;
}
}
Ok(())
}
impl HistoryCtx {
/// Create a new context object. Called from C through `ror_history_ctx_new()`.
fn new_boxed() -> Fallible<Box<HistoryCtx>> {
let mut journal = journal::Journal::open(journal::JournalFiles::System, false, true)?;
journal.seek(journal::JournalSeek::Tail)?;
Ok(Box::new(HistoryCtx {
journal: journal,
marker_queue: VecDeque::new(),
current_entry: None,
search_mode: None,
reached_eof: false,
}))
}
/// Ensures the journal filters are set up for the messages we're interested in.
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 == JournalSearchMode::BootAndDeploymentMsgs {
self.journal.match_add("MESSAGE_ID", RPMOSTREE_DEPLOY_MSG)?;
}
self.search_mode = Some(mode);
}
Ok(())
}
/// 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<Option<Marker>> {
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(Marker::Boot(BootMarker {
timestamp: journal_record_timestamp(&self.journal)?,
path: path.clone(),
node: DevIno { device, inode },
})));
}
Ok(None)
}
/// 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<Option<Marker>> {
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(Marker::Deployment(DeploymentMarker {
timestamp,
node: DevIno { device, inode },
path: path.clone(),
cmdline: record
.get("COMMAND_LINE")
.and_then(|s| CString::new(s.as_str()).ok()),
})));
}
Ok(None)
}
/// Goes to the next OSTree boot msg in the journal and returns its marker.
fn find_next_boot_marker(&mut self) -> Fallible<Option<BootMarker>> {
self.set_search_mode(JournalSearchMode::BootMsgs)?;
while let Some(rec) = self.journal.previous_record()? {
if let Some(Marker::Boot(m)) = self.boot_record_to_marker(&rec)? {
return Ok(Some(m));
}
}
Ok(None)
}
/// Returns a marker of the appropriate kind for a given journal message.
fn record_to_marker(&self, record: &JournalRecord) -> Fallible<Option<Marker>> {
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<Option<Marker>> {
self.set_search_mode(JournalSearchMode::BootAndDeploymentMsgs)?;
while let Some(rec) = self.journal.previous_record()? {
if let Some(marker) = self.record_to_marker(&rec)? {
return Ok(Some(marker));
}
}
Ok(None)
}
/// Finds the matching deployment marker for the next boot marker in the queue.
fn scan_until_path_match(&mut self) -> Fallible<Option<(BootMarker, DeploymentMarker)>> {
// keep popping & scanning until we get to the next boot marker
let boot_marker = loop {
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),
},
}
};
// check if its corresponding deployment is already in the queue
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())));
}
}
}
// keep collecting until we get a matching path
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 marker = self.marker_queue.back().unwrap();
if let Marker::Deployment(m) = marker {
if m.path == boot_marker.path {
return Ok(Some((boot_marker, m.clone())));
}
}
}
Ok(None)
}
/// Returns the next history entry, which consists of a boot timestamp and its matching
/// deploy timestamp.
fn scan_until_next_entry(&mut self) -> Fallible<Option<HistoryEntry>> {
while let Some((boot_marker, deployment_marker)) = self.scan_until_path_match()? {
if boot_marker.node != deployment_marker.node {
// This is a non-foolproof safety valve to ensure that the boot is definitely
// referring to the matched up deployment. E.g. if the correct, more recent,
// matching deployment somehow had its journal entry lost, we don't want to report
// this boot with the wrong match. For now, just silently skip over that boot. No
// history is better than wrong history. In the future, we could consider printing
// this somehow too.
continue;
}
return Ok(Some(HistoryEntry::new_from_markers(
boot_marker,
deployment_marker,
)));
}
Ok(None)
}
/// 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<Option<HistoryEntry>> {
while let Some(entry) = self.scan_until_next_entry()? {
if self.current_entry.is_none() {
/* first scan ever; prime with first entry */
self.current_entry.replace(entry);
continue;
}
let current_deploy_timestamp = self.current_entry.as_ref().unwrap().deploy_timestamp;
if entry.deploy_timestamp == current_deploy_timestamp {
/* we found an older boot for the same deployment: update first boot */
let current_entry = &mut self.current_entry.as_mut().unwrap();
current_entry.first_boot_timestamp = entry.first_boot_timestamp;
current_entry.boot_count += 1;
} else {
/* found a new boot for a different deployment; flush out current one */
return Ok(self.current_entry.replace(entry));
}
}
/* flush out final entry if any */
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. Called from
/// C through `ror_history_ctx_next()`.
fn next_entry(&mut self) -> Fallible<HistoryEntry> {
if self.reached_eof {
bail!("next_entry() called after having reached EOF!")
}
match self.scan_until_next_new_entry()? {
Some(e) => Ok(e),
None => {
self.reached_eof = true;
Ok(HistoryEntry::eof())
}
}
}
}
/// 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;
pub use systemd::journal::{JournalFiles, JournalRecord, JournalSeek};
pub struct Journal {
pub entries: Vec<(u64, JournalRecord)>,
pub current_timestamp: Option<u64>,
msg_ids: Vec<String>,
}
impl Journal {
pub fn open(_: JournalFiles, _: bool, _: bool) -> Fallible<Journal> {
Ok(Journal {
entries: Vec::new(),
current_timestamp: None,
msg_ids: Vec::new(),
})
}
pub fn seek(&mut self, _: JournalSeek) -> Fallible<()> {
Ok(())
}
pub fn match_flush(&mut self) -> Fallible<()> {
self.msg_ids.clear();
Ok(())
}
pub fn match_add(&mut self, _: &str, msg_id: &str) -> Fallible<()> {
self.msg_ids.push(msg_id.into());
Ok(())
}
pub fn previous_record(&mut self) -> Fallible<Option<JournalRecord>> {
while let Some((timestamp, record)) = self.entries.pop() {
if self.msg_ids.contains(record.get("MESSAGE_ID").unwrap()) {
self.current_timestamp = Some(timestamp);
return Ok(Some(record));
}
}
Ok(None)
}
// This is only used by the prune path, which we're not unit testing.
pub fn next_record(&mut self) -> Fallible<Option<JournalRecord>> {
unimplemented!();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
impl HistoryCtx {
fn add_boot_record_inode(&mut self, ts: u64, path: &str, inode: u64) {
if let Some(entry) = self.journal.entries.last() {
assert!(ts > entry.0);
}
let mut record = JournalRecord::new();
record.insert("MESSAGE_ID".into(), OSTREE_BOOT_MSG.into());
record.insert("DEPLOYMENT_PATH".into(), path.into());
record.insert("DEPLOYMENT_DEVICE".into(), inode.to_string());
record.insert("DEPLOYMENT_INODE".into(), inode.to_string());
self.journal.entries.push((ts, record));
}
fn add_boot_record(&mut self, ts: u64, path: &str) {
self.add_boot_record_inode(ts, path, 0);
}
fn add_deployment_record_inode(&mut self, ts: u64, path: &str, inode: u64) {
if let Some(entry) = self.journal.entries.last() {
assert!(ts > entry.0);
}
let mut record = JournalRecord::new();
record.insert("MESSAGE_ID".into(), RPMOSTREE_DEPLOY_MSG.into());
record.insert("DEPLOYMENT_TIMESTAMP".into(), ts.to_string());
record.insert("DEPLOYMENT_PATH".into(), path.into());
record.insert("DEPLOYMENT_DEVICE".into(), inode.to_string());
record.insert("DEPLOYMENT_INODE".into(), inode.to_string());
self.journal.entries.push((ts, record));
}
fn add_deployment_record(&mut self, ts: u64, path: &str) {
self.add_deployment_record_inode(ts, path, 0);
}
fn assert_next_entry(
&mut self,
first_boot_timestamp: u64,
last_boot_timestamp: u64,
deploy_timestamp: u64,
boot_count: u64,
) {
assert!(
self.next_entry().unwrap()
== HistoryEntry {
first_boot_timestamp: first_boot_timestamp,
last_boot_timestamp: last_boot_timestamp,
deploy_timestamp: deploy_timestamp,
deploy_cmdline: ptr::null_mut(),
boot_count: boot_count,
eof: false,
}
);
}
fn assert_eof(&mut self) {
assert!(self.next_entry().unwrap().eof);
}
}
#[test]
fn basic() {
let mut ctx = HistoryCtx::new_boxed().unwrap();
assert!(ctx.next_entry().unwrap().eof);
assert!(ctx.next_entry().is_err());
}
#[test]
fn basic_deploy() {
let mut ctx = HistoryCtx::new_boxed().unwrap();
ctx.add_deployment_record(0, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.assert_eof();
}
#[test]
fn basic_boot() {
let mut ctx = HistoryCtx::new_boxed().unwrap();
ctx.add_boot_record(0, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.assert_eof();
}
#[test]
fn basic_match() {
let mut ctx = HistoryCtx::new_boxed().unwrap();
ctx.add_deployment_record(0, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(1, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.assert_next_entry(1, 1, 0, 1);
ctx.assert_eof();
}
#[test]
fn multi_boot() {
let mut ctx = HistoryCtx::new_boxed().unwrap();
ctx.add_boot_record(0, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(1, "/ostree/deploy/fedora/deploy/deadcafe.1");
ctx.add_boot_record(3, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_deployment_record(4, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(5, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(6, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(7, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.assert_next_entry(5, 7, 4, 3);
ctx.assert_eof();
}
#[test]
fn multi_deployment() {
let mut ctx = HistoryCtx::new_boxed().unwrap();
ctx.add_deployment_record(0, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_deployment_record(1, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_deployment_record(2, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(3, "/ostree/deploy/fedora/deploy/deadcafe.1");
ctx.add_boot_record(4, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_deployment_record(5, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_deployment_record(6, "/ostree/deploy/fedora/deploy/deadcafe.1");
ctx.assert_next_entry(4, 4, 2, 1);
ctx.assert_eof();
}
#[test]
fn multi1() {
let mut ctx = HistoryCtx::new_boxed().unwrap();
ctx.add_deployment_record(0, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(1, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(2, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(3, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_deployment_record(4, "/ostree/deploy/fedora/deploy/deadcafe.1");
ctx.add_boot_record(5, "/ostree/deploy/fedora/deploy/deadcafe.1");
ctx.add_boot_record(6, "/ostree/deploy/fedora/deploy/deadcafe.1");
ctx.assert_next_entry(5, 6, 4, 2);
ctx.assert_next_entry(1, 3, 0, 3);
ctx.assert_eof();
}
#[test]
fn multi2() {
let mut ctx = HistoryCtx::new_boxed().unwrap();
ctx.add_deployment_record(0, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_deployment_record(1, "/ostree/deploy/fedora/deploy/deadcafe.1");
ctx.add_deployment_record(2, "/ostree/deploy/fedora/deploy/deadcafe.2");
ctx.add_deployment_record(3, "/ostree/deploy/fedora/deploy/deadcafe.3");
ctx.add_deployment_record(4, "/ostree/deploy/fedora/deploy/deadcafe.4");
ctx.add_boot_record(5, "/ostree/deploy/fedora/deploy/deadcafe.4");
ctx.add_boot_record(6, "/ostree/deploy/fedora/deploy/deadcafe.3");
ctx.add_boot_record(7, "/ostree/deploy/fedora/deploy/deadcafe.2");
ctx.add_boot_record(8, "/ostree/deploy/fedora/deploy/deadcafe.1");
ctx.add_boot_record(9, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.assert_next_entry(9, 9, 0, 1);
ctx.assert_next_entry(8, 8, 1, 1);
ctx.assert_next_entry(7, 7, 2, 1);
ctx.assert_next_entry(6, 6, 3, 1);
ctx.assert_next_entry(5, 5, 4, 1);
ctx.assert_eof();
}
#[test]
fn multi3() {
let mut ctx = HistoryCtx::new_boxed().unwrap();
ctx.add_deployment_record(0, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_deployment_record(1, "/ostree/deploy/fedora/deploy/deadcafe.1");
ctx.add_deployment_record(2, "/ostree/deploy/fedora/deploy/deadcafe.2");
ctx.add_deployment_record(3, "/ostree/deploy/fedora/deploy/deadcafe.3");
ctx.add_deployment_record(4, "/ostree/deploy/fedora/deploy/deadcafe.4");
ctx.add_boot_record(5, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(6, "/ostree/deploy/fedora/deploy/deadcafe.1");
ctx.add_boot_record(7, "/ostree/deploy/fedora/deploy/deadcafe.2");
ctx.add_boot_record(8, "/ostree/deploy/fedora/deploy/deadcafe.3");
ctx.add_boot_record(9, "/ostree/deploy/fedora/deploy/deadcafe.4");
ctx.assert_next_entry(9, 9, 4, 1);
ctx.assert_next_entry(8, 8, 3, 1);
ctx.assert_next_entry(7, 7, 2, 1);
ctx.assert_next_entry(6, 6, 1, 1);
ctx.assert_next_entry(5, 5, 0, 1);
ctx.assert_eof();
}
#[test]
fn multi4() {
let mut ctx = HistoryCtx::new_boxed().unwrap();
ctx.add_deployment_record(0, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(1, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_deployment_record(2, "/ostree/deploy/fedora/deploy/deadcafe.2");
ctx.add_boot_record(3, "/ostree/deploy/fedora/deploy/deadcafe.2");
ctx.add_deployment_record(4, "/ostree/deploy/fedora/deploy/deadcafe.4");
ctx.add_boot_record(5, "/ostree/deploy/fedora/deploy/deadcafe.4");
ctx.add_deployment_record(6, "/ostree/deploy/fedora/deploy/deadcafe.6");
ctx.add_boot_record(7, "/ostree/deploy/fedora/deploy/deadcafe.6");
ctx.add_deployment_record(8, "/ostree/deploy/fedora/deploy/deadcafe.8");
ctx.add_boot_record(9, "/ostree/deploy/fedora/deploy/deadcafe.8");
ctx.assert_next_entry(9, 9, 8, 1);
ctx.assert_next_entry(7, 7, 6, 1);
ctx.assert_next_entry(5, 5, 4, 1);
ctx.assert_next_entry(3, 3, 2, 1);
ctx.assert_next_entry(1, 1, 0, 1);
ctx.assert_eof();
}
#[test]
fn multi5() {
let mut ctx = HistoryCtx::new_boxed().unwrap();
ctx.add_deployment_record(0, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_deployment_record(1, "/ostree/deploy/fedora/deploy/deadcafe.1");
ctx.add_boot_record(2, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(3, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(4, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(5, "/ostree/deploy/fedora/deploy/deadcafe.1");
ctx.add_boot_record(6, "/ostree/deploy/fedora/deploy/deadcafe.1");
ctx.add_boot_record(7, "/ostree/deploy/fedora/deploy/deadcafe.0");
ctx.add_boot_record(8, "/ostree/deploy/fedora/deploy/deadcafe.1");
ctx.assert_next_entry(8, 8, 1, 1);
ctx.assert_next_entry(7, 7, 0, 1);
ctx.assert_next_entry(5, 6, 1, 2);
ctx.assert_next_entry(2, 4, 0, 3);
ctx.assert_eof();
}
#[test]
fn inode1() {
let mut ctx = HistoryCtx::new_boxed().unwrap();
ctx.add_deployment_record_inode(0, "/ostree/deploy/fedora/deploy/deadcafe.0", 1000);
ctx.add_deployment_record_inode(1, "/ostree/deploy/fedora/deploy/deadcafe.1", 2000);
ctx.add_boot_record_inode(2, "/ostree/deploy/fedora/deploy/deadcafe.0", 1000);
ctx.add_boot_record_inode(3, "/ostree/deploy/fedora/deploy/deadcafe.1", 2000);
ctx.assert_next_entry(3, 3, 1, 1);
ctx.assert_next_entry(2, 2, 0, 1);
ctx.assert_eof();
}
#[test]
fn inode2() {
let mut ctx = HistoryCtx::new_boxed().unwrap();
ctx.add_deployment_record_inode(0, "/ostree/deploy/fedora/deploy/deadcafe.0", 1000);
ctx.add_deployment_record_inode(1, "/ostree/deploy/fedora/deploy/deadcafe.1", 2000);
ctx.add_boot_record_inode(2, "/ostree/deploy/fedora/deploy/deadcafe.1", 2000);
ctx.add_boot_record_inode(3, "/ostree/deploy/fedora/deploy/deadcafe.0", 1000);
ctx.add_boot_record_inode(4, "/ostree/deploy/fedora/deploy/deadcafe.0", 1001);
ctx.assert_next_entry(3, 3, 0, 1);
ctx.assert_next_entry(2, 2, 1, 1);
ctx.assert_eof();
}
}
mod ffi {
use super::*;
use glib_sys;
use libc;
use crate::ffiutil::*;
#[no_mangle]
pub extern "C" fn ror_history_ctx_new(gerror: *mut *mut glib_sys::GError) -> *mut HistoryCtx {
ptr_glib_error(HistoryCtx::new_boxed(), gerror)
}
#[no_mangle]
pub extern "C" fn ror_history_ctx_next(
hist: *mut HistoryCtx,
entry: *mut HistoryEntry,
gerror: *mut *mut glib_sys::GError,
) -> libc::c_int {
let hist = ref_from_raw_ptr(hist);
let entry = ref_from_raw_ptr(entry);
int_glib_error(hist.next_entry().map(|e| *entry = e), gerror)
}
#[no_mangle]
pub extern "C" fn ror_history_ctx_free(hist: *mut HistoryCtx) {
if hist.is_null() {
return;
}
unsafe {
Box::from_raw(hist);
}
}
#[no_mangle]
pub extern "C" fn ror_history_entry_clear(entry: *mut HistoryEntry) {
let entry = ref_from_raw_ptr(entry);
if !entry.deploy_cmdline.is_null() {
unsafe {
CString::from_raw(entry.deploy_cmdline);
}
}
}
#[no_mangle]
pub extern "C" fn ror_history_prune(gerror: *mut *mut glib_sys::GError) -> libc::c_int {
int_glib_error(history_prune(), gerror)
}
}
pub use self::ffi::*;

View File

@ -10,6 +10,8 @@ mod libdnf_sys;
mod composepost;
pub use self::composepost::*;
mod history;
pub use self::history::*;
mod journal;
pub use self::journal::*;
mod progress;

View File

@ -32,6 +32,8 @@ static RpmOstreeCommand ex_subcommands[] = {
"Convert an OSTree commit into an rpm-ostree rojig", rpmostree_ex_builtin_commit2rojig },
{ "rojig2commit", RPM_OSTREE_BUILTIN_FLAG_LOCAL_CMD,
"Convert an rpm-ostree rojig into an OSTree commit", rpmostree_ex_builtin_rojig2commit },
{ "history", RPM_OSTREE_BUILTIN_FLAG_LOCAL_CMD,
"Inspect RPM-OSTree history of the system", rpmostree_ex_builtin_history },
/* temporary aliases; nuke in next version */
{ "reset", RPM_OSTREE_BUILTIN_FLAG_SUPPORTS_PKG_INSTALLS | RPM_OSTREE_BUILTIN_FLAG_HIDDEN,
NULL, rpmostree_builtin_reset },

View File

@ -29,6 +29,7 @@
#include <libdnf/libdnf.h>
#include "rpmostree-builtins.h"
#include "rpmostree-ex-builtins.h"
#include "rpmostree-libbuiltin.h"
#include "rpmostree-dbus-helpers.h"
#include "rpmostree-util.h"
@ -1097,3 +1098,174 @@ rpmostree_builtin_status (int argc,
return TRUE;
}
/* XXX
static gboolean opt_no_pager;
static gboolean opt_deployments;
*/
static GOptionEntry history_option_entries[] = {
{ "verbose", 'v', 0, G_OPTION_ARG_NONE, &opt_verbose, "Print additional fields (e.g. StateRoot)", NULL },
{ "json", 0, 0, G_OPTION_ARG_NONE, &opt_json, "Output JSON", NULL },
/* XXX { "deployments", 0, 0, G_OPTION_ARG_NONE, &opt_deployments, "Print all deployments, not just those booted into", NULL }, */
/* XXX { "no-pager", 0, 0, G_OPTION_ARG_NONE, &opt_no_pager, "Don't use a pager to display output", NULL }, */
{ NULL }
};
/* Read from history db, sets @out_deployment to NULL on ENOENT. */
static gboolean
fetch_history_deployment_gvariant (RORHistoryEntry *entry,
GVariant **out_deployment,
GError **error)
{
g_autofree char *fn =
g_strdup_printf ("%s/%lu", RPMOSTREE_HISTORY_DIR, entry->deploy_timestamp);
*out_deployment = NULL;
glnx_autofd int fd = -1;
g_autoptr(GError) local_error = NULL;
if (!glnx_openat_rdonly (AT_FDCWD, fn, TRUE, &fd, &local_error))
{
if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
return g_propagate_error (error, g_steal_pointer (&local_error)), FALSE;
return TRUE; /* Note early return */
}
g_autoptr(GBytes) data = glnx_fd_readall_bytes (fd, NULL, error);
if (!data)
return FALSE;
*out_deployment =
g_variant_ref_sink (g_variant_new_from_bytes (G_VARIANT_TYPE_VARDICT, data, FALSE));
return TRUE;
}
static void
print_timestamp_and_relative (const char* key, guint64 t)
{
g_autofree char *ts = rpmostree_timestamp_str_from_unix_utc (t);
char time_rel[FORMAT_TIMESTAMP_RELATIVE_MAX] = "";
libsd_format_timestamp_relative (time_rel, sizeof(time_rel), t * USEC_PER_SEC);
if (key)
g_print ("%s: ", key);
g_print ("%s (%s)\n", ts, time_rel);
}
static gboolean
print_history_entry (RORHistoryEntry *entry,
GError **error)
{
g_autoptr(GVariant) deployment = NULL;
if (!fetch_history_deployment_gvariant (entry, &deployment, error))
return FALSE;
if (!opt_json)
{
print_timestamp_and_relative ("BootTimestamp", entry->last_boot_timestamp);
if (entry->boot_count > 1)
{
g_print ("%s BootCount: %lu; first booted on ",
libsd_special_glyph (TREE_RIGHT), entry->boot_count);
print_timestamp_and_relative (NULL, entry->first_boot_timestamp);
}
print_timestamp_and_relative ("CreateTimestamp", entry->deploy_timestamp);
if (entry->deploy_cmdline)
g_print ("CreateCommand: %s%s%s\n",
get_bold_start (), entry->deploy_cmdline, get_bold_end ());
if (!deployment)
/* somehow we're missing an entry? XXX: just fallback to checksum, version, refspec
* from journal entry in this case */
g_print (" << Missing history information >>\n");
/* XXX: factor out interesting bits from print_one_deployment() */
else if (!print_one_deployment (NULL, deployment, TRUE, FALSE, FALSE,
NULL, NULL, NULL, NULL, error))
return FALSE;
}
else
{
/* NB: notice we implicitly print as a stream of objects rather than an array */
glnx_unref_object JsonBuilder *builder = json_builder_new ();
json_builder_begin_object (builder);
if (deployment)
{
json_builder_set_member_name (builder, "deployment");
json_builder_add_value (builder, json_gvariant_serialize (deployment));
}
json_builder_set_member_name (builder, "deployment-create-timestamp");
json_builder_add_int_value (builder, entry->deploy_timestamp);
json_builder_set_member_name (builder, "deployment-create-command-line");
json_builder_add_string_value (builder, entry->deploy_cmdline);
json_builder_set_member_name (builder, "boot-count");
json_builder_add_int_value (builder, entry->boot_count);
json_builder_set_member_name (builder, "first-boot-timestamp");
json_builder_add_int_value (builder, entry->first_boot_timestamp);
json_builder_set_member_name (builder, "last-boot-timestamp");
json_builder_add_int_value (builder, entry->last_boot_timestamp);
json_builder_end_object (builder);
glnx_unref_object JsonGenerator *generator = json_generator_new ();
json_generator_set_pretty (generator, TRUE);
json_generator_set_root (generator, json_builder_get_root (builder));
glnx_unref_object GOutputStream *stdout_gio = g_unix_output_stream_new (1, FALSE);
/* NB: watch out for the misleading API docs */
if (json_generator_to_stream (generator, stdout_gio, NULL, error) <= 0
|| (error != NULL && *error != NULL))
return FALSE;
}
g_print ("\n");
return TRUE;
}
/* The `history` also lives here since the printing bits re-use a lot of the `status`
* machinery. */
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,
history_option_entries,
&argc, &argv,
invocation,
cancellable,
NULL, NULL, NULL, NULL, NULL,
error))
return FALSE;
/* initiate a history context, then iterate over each (boot time, deploy time), then print */
/* XXX: enhance with option for going in reverse (oldest first) */
g_autoptr(RORHistoryCtx) history_ctx = ror_history_ctx_new (error);
if (!history_ctx)
return FALSE;
/* XXX: use pager here */
gboolean at_least_one = FALSE;
while (TRUE)
{
g_auto(RORHistoryEntry) entry = { 0, };
if (!ror_history_ctx_next (history_ctx, &entry, error))
return FALSE;
if (entry.eof)
break;
if (!print_history_entry (&entry, error))
return FALSE;
at_least_one = TRUE;
}
if (!at_least_one)
g_print ("<< No entries found >>\n");
return TRUE;
}

View File

@ -34,6 +34,7 @@ BUILTINPROTO(unpack);
BUILTINPROTO(livefs);
BUILTINPROTO(commit2rojig);
BUILTINPROTO(rojig2commit);
BUILTINPROTO(history);
#undef BUILTINPROTO

View File

@ -33,6 +33,7 @@
#include "rpmostree-rpm-util.h"
#include "rpmostree-postprocess.h"
#include "rpmostree-output.h"
#include "rpmostree-rust.h"
#include "ostree-repo.h"
@ -295,6 +296,9 @@ rpmostree_syscore_cleanup (OstreeSysroot *sysroot,
if (!glnx_shutil_rm_rf_at (repo_dfd, RPMOSTREE_TMP_ROOTFS_DIR,
cancellable, error))
return FALSE;
/* also delete extra history entries */
if (!ror_history_prune (error))
return FALSE;
/* Regenerate all refs */
guint n_pkgcache_freed = 0;

View File

@ -30,14 +30,18 @@
#include "rpmostree-origin.h"
#include "rpmostree-kernel.h"
#include "rpmostreed-daemon.h"
#include "rpmostreed-deployment-utils.h"
#include "rpmostree-kernel.h"
#include "rpmostree-rpm-util.h"
#include "rpmostree-postprocess.h"
#include "rpmostree-output.h"
#include "rpmostree-scripts.h"
#include "rpmostree-rust.h"
#include "ostree-repo.h"
#define RPMOSTREE_NEW_DEPLOYMENT_MSG SD_ID128_MAKE(9b,dd,bd,a1,77,cd,44,d8,91,b1,b5,61,a8,a0,ce,9e)
/**
* SECTION:rpmostree-sysroot-upgrader
* @title: Simple upgrade class
@ -1226,6 +1230,72 @@ rpmostree_sysroot_upgrader_set_kargs (RpmOstreeSysrootUpgrader *self,
self->kargs_strv = g_strdupv (kernel_args);
}
static gboolean
write_history (RpmOstreeSysrootUpgrader *self,
OstreeDeployment *new_deployment,
const char *initiating_command_line,
GCancellable *cancellable,
GError **error)
{
g_autoptr(GVariant) deployment_variant =
rpmostreed_deployment_generate_variant (self->sysroot, new_deployment, NULL,
self->repo, FALSE, error);
if (!deployment_variant)
return FALSE;
g_autofree char *deployment_dirpath =
ostree_sysroot_get_deployment_dirpath (self->sysroot, new_deployment);
struct stat stbuf;
if (!glnx_fstatat (ostree_sysroot_get_fd (self->sysroot),
deployment_dirpath, &stbuf, 0, error))
return FALSE;
g_autofree char *fn =
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),
0, cancellable, error))
return FALSE;
g_autofree char *version = NULL;
{ g_autoptr(GVariant) commit = NULL;
if (!ostree_repo_load_commit (self->repo, ostree_deployment_get_csum (new_deployment),
&commit, NULL, error))
return FALSE;
version = rpmostree_checksum_version (commit);
}
sd_journal_send ("MESSAGE_ID=" SD_ID128_FORMAT_STR,
SD_ID128_FORMAT_VAL(RPMOSTREE_NEW_DEPLOYMENT_MSG),
"MESSAGE=Created new deployment /%s", deployment_dirpath,
"DEPLOYMENT_PATH=/%s", deployment_dirpath,
"DEPLOYMENT_TIMESTAMP=%ld", stbuf.st_ctime,
"DEPLOYMENT_DEVICE=%u", stbuf.st_dev,
"DEPLOYMENT_INODE=%u", stbuf.st_ino,
"DEPLOYMENT_CHECKSUM=%s", ostree_deployment_get_csum (new_deployment),
"DEPLOYMENT_REFSPEC=%s", rpmostree_origin_get_refspec (self->origin),
/* we could use iovecs here and sd_journal_sendv to make these truly
* conditional, but meh, empty field works fine too */
"DEPLOYMENT_VERSION=%s", version ?: "",
"COMMAND_LINE=%s", initiating_command_line ?: "",
NULL);
return TRUE;
}
/**
* rpmostree_sysroot_upgrader_deploy:
* @self: Self
@ -1329,6 +1399,9 @@ rpmostree_sysroot_upgrader_deploy (RpmOstreeSysrootUpgrader *self,
return FALSE;
}
if (!write_history (self, new_deployment, initiating_command_line, cancellable, error))
return FALSE;
/* Also do a sanitycheck even if there's no local mutation; it's basically free
* and might save someone in the future. The RPMOSTREE_SKIP_SANITYCHECK
* environment variable is just used by test-basic.sh currently.

View File

@ -40,6 +40,9 @@
/* 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_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)

114
tests/vmcheck/test-history.sh Executable file
View File

@ -0,0 +1,114 @@
#!/bin/bash
#
# Copyright (C) 2019 Jonathan Lebon <jonathan@jlebon.com>
#
# This library 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 License, 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.
set -euo pipefail
. ${commondir}/libtest.sh
. ${commondir}/libvm.sh
set -x
# Simple e2e test for history; note there are many more extensive corner-case
# style unit tests in history.rs.
# XXX: hack around the latest ostree in f29 not having
# https://github.com/ostreedev/ostree/pull/1842
vm_rpmostree initramfs --enable
vm_build_rpm foo
vm_rpmostree install foo
vm_reboot
echo "ok setup"
vm_rpmostree ex history > out.txt
assert_file_has_content out.txt "CreateCommand: install foo"
assert_file_has_content out.txt "LayeredPackages: foo"
vm_rpmostree ex history --json | jq . --slurp > out.json
assert_jq out.json \
'.[0]["deployment-create-command-line"] == "install foo"' \
'.[0]["deployment-create-timestamp"] != null' \
'.[0]["deployment"]["packages"][0] == "foo"' \
'.[0]["first-boot-timestamp"] != null' \
'.[0]["last-boot-timestamp"] != null' \
'.[0]["boot-count"] == 1'
echo "ok install first boot"
vm_reboot
vm_rpmostree ex history > out.txt
assert_file_has_content out.txt "BootCount: 2"
vm_rpmostree ex history --json | jq . --slurp > out.json
assert_jq out.json \
'.[0]["deployment-create-command-line"] == "install foo"' \
'.[0]["deployment-create-timestamp"] != null' \
'.[0]["deployment"]["packages"][0] == "foo"' \
'.[0]["first-boot-timestamp"] != null' \
'.[0]["last-boot-timestamp"] != null' \
'.[0]["boot-count"] == 2'
echo "ok install second boot"
vm_rpmostree uninstall foo
vm_reboot
vm_rpmostree ex history > out.txt
assert_file_has_content out.txt "CreateCommand: uninstall foo"
vm_rpmostree ex history --json | jq . --slurp > out.json
assert_jq out.json \
'.[0]["deployment-create-command-line"] == "uninstall foo"' \
'.[0]["deployment-create-timestamp"] != null' \
'.[0]["first-boot-timestamp"] != null' \
'.[0]["last-boot-timestamp"] != null' \
'.[0]["boot-count"] == 1'
assert_jq out.json \
'.[1]["deployment-create-command-line"] == "install foo"' \
'.[1]["deployment-create-timestamp"] != null' \
'.[1]["deployment"]["packages"][0] == "foo"' \
'.[1]["first-boot-timestamp"] != null' \
'.[1]["last-boot-timestamp"] != null' \
'.[1]["boot-count"] == 2'
echo "ok uninstall"
# and check history pruning since that's one bit we can't really test from the
# unit tests
vm_cmd find /var/lib/rpm-ostree/history | xargs -n 1 basename | sort -g > entries.txt
if [ ! $(wc -l entries.txt) -gt 1 ]; then
assert_not_reached "Expected more than 1 entry, got $(cat entries.txt)"
fi
# get the most recent entry
entry=$(tail -n 1 entries.txt)
# And now nuke all the journal entries except the latest, but we don't want to
# actually lose everything since e.g. some of the previous vmcheck tests that
# ran on this machine may have failed and we would've rendered the journal
# useless for debugging. So hack around that... yeah, would be cleaner if we
# could just spawn individual VMs per test.
vm_cmd systemctl stop systemd-journald.service
vm_cmd cp -r /var/log/journal{,.bak}
vm_cmd journalctl --vacuum-time=$((entry - 1))s
vm_rpmostree cleanup -b
vm_cmd systemctl stop systemd-journald.service
vm_cmd rm -rf /var/log/journal
vm_cmd mv /var/log/journal{.bak,}
vm_cmd ls -l /var/lib/rpm-ostree/history > entries.txt
if [ $(wc -l entries.txt) != 1 ]; then
assert_not_reached "Expected only 1 entry, got $(cat entries.txt)"
fi
echo "ok prune"