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:
parent
9e2ceca06f
commit
10755592ea
@ -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
781
rust/src/history.rs
Normal 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::*;
|
@ -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;
|
||||
|
@ -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 },
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ BUILTINPROTO(unpack);
|
||||
BUILTINPROTO(livefs);
|
||||
BUILTINPROTO(commit2rojig);
|
||||
BUILTINPROTO(rojig2commit);
|
||||
BUILTINPROTO(history);
|
||||
|
||||
#undef BUILTINPROTO
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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
114
tests/vmcheck/test-history.sh
Executable 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"
|
Loading…
Reference in New Issue
Block a user