Skip to content

Commit

Permalink
rpm-ostree: Use upstream Rust bindings
Browse files Browse the repository at this point in the history
Depends: coreos/rpm-ostree#2636

Use the upstream client bindings for the status data.

An interesting note: In the upstream API, the commit
metadata is exposed as a generic `HashMap<>` because
that's how it's used by both ostree and rpm-ostree.  It'd
be a layering violation for us to hardcode `coreos-assembler.basearch`
in the rpm-ostree git for example, not to mention `fedora-coreos.stream`.
So those constants stay here in zincati.

I dropped the `_json` terminology from various functions
because that's weird - we parsed the data from JSON, but
that's not really very relevant except as an implementation
detail.  It's just a `Deployment`, not a `DeploymentJSON`.

Also I dropped for now the optimization of using `--booted`;
it's not a huge amount of data.  If we care we can re-add that
later.  Note it makes caching more complex because then we
need to carefully check the booted state too.
  • Loading branch information
cgwalters committed Mar 8, 2021
1 parent 0ce1397 commit c457ecb
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 136 deletions.
26 changes: 22 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ prometheus = { version = "0.11", default-features = false }
rand = "0.8"
regex = "1.4"
reqwest = { version = "0.10", features = ["json"] }
# Not published to crates.io right now
rpmostree-client = { git = "https://github.com/coreos/rpm-ostree", rev = "3041d648bbce3beaef2d6b69c8461532e508329d" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tempfile = "^3.2"
Expand Down
24 changes: 17 additions & 7 deletions src/identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use libsystemd::id128;
use ordered_float::NotNan;
use prometheus::{Gauge, IntGaugeVec};
use regex::Regex;
use rpm_ostree::Release;
use serde::Serialize;
use std::collections::HashMap;

Expand Down Expand Up @@ -90,19 +91,28 @@ impl Identity {
/// Try to build default agent identity.
pub fn try_default() -> Fallible<Self> {
// Invoke rpm-ostree to get the status of the currently booted deployment.
let status = rpm_ostree::invoke_cli_status(true)?;
let basearch = rpm_ostree::parse_basearch(&status)
.context("failed to introspect OS base architecture")?;
let current_os =
rpm_ostree::parse_booted(&status).context("failed to introspect booted OS image")?;
let status = rpmostree_client::query_status(&*rpm_ostree::CLI_CLIENT)
.map_err(failure::Error::from_boxed_compat)?;
let booted = status
.require_booted()
.map_err(failure::Error::from_boxed_compat)?;
let basearch = booted
.find_base_commitmeta_string(rpm_ostree::COSA_BASEARCH)
.map_err(failure::Error::from_boxed_compat)
.context("failed to introspect OS base architecture")?
.to_string();
let node_uuid = {
let app_id = id128::Id128::try_from_slice(APP_ID)
.map_err(|e| format_err!("failed to parse application ID: {}", e))?;
compute_node_uuid(&app_id)?
};
let platform = platform::read_id("/proc/cmdline")?;
let stream = rpm_ostree::parse_updates_stream(&status)
.context("failed to introspect OS updates stream")?;
let stream = booted
.find_base_commitmeta_string(rpm_ostree::FCOS_STREAM)
.map_err(failure::Error::from_boxed_compat)
.context("failed to introspect OS updates stream")?
.to_string();
let current_os: Release = booted.into();

let id = Self {
basearch,
Expand Down
3 changes: 1 addition & 2 deletions src/rpm_ostree/actor.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
//! rpm-ostree client actor.

use super::cli_status::StatusJSON;
use super::Release;
use actix::prelude::*;
use failure::Fallible;
Expand All @@ -12,7 +11,7 @@ use std::rc::Rc;
/// Cache of local deployments.
#[derive(Clone, Debug)]
pub struct StatusCache {
pub status: Rc<StatusJSON>,
pub status: Rc<rpmostree_client::Status>,
pub mtime: FileTime,
}

Expand Down
152 changes: 30 additions & 122 deletions src/rpm_ostree/cli_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

use super::actor::{RpmOstreeClient, StatusCache};
use super::Release;
use failure::{bail, ensure, format_err, Fallible, ResultExt};
use failure::{format_err, Fallible, ResultExt};
use filetime::FileTime;
use log::trace;
use prometheus::IntCounter;
use serde::Deserialize;
use std::collections::BTreeSet;
use std::fs;
use std::rc::Rc;

use super::CLI_CLIENT;

/// Path to local OSTree deployments. We use its mtime to check for modifications (e.g. new deployments)
/// to local deployments that might warrant querying `rpm-ostree status` again to update our knowledge
/// of the current state of deployments.
Expand All @@ -37,82 +38,31 @@ lazy_static::lazy_static! {
)).unwrap();
}

/// JSON output from `rpm-ostree status --json`
#[derive(Clone, Debug, Deserialize)]
pub struct StatusJSON {
deployments: Vec<DeploymentJSON>,
}

/// Partial deployment object (only fields relevant to zincati).
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DeploymentJSON {
booted: bool,
base_checksum: Option<String>,
#[serde(rename = "base-commit-meta")]
base_metadata: BaseCommitMetaJSON,
checksum: String,
// NOTE(lucab): missing field means "not staged".
#[serde(default)]
staged: bool,
version: String,
}

/// Metadata from base commit (only fields relevant to zincati).
#[derive(Clone, Debug, Deserialize)]
struct BaseCommitMetaJSON {
#[serde(rename = "coreos-assembler.basearch")]
basearch: String,
#[serde(rename = "fedora-coreos.stream")]
stream: String,
}

impl DeploymentJSON {
/// Convert into `Release`.
pub fn into_release(self) -> Release {
impl From<&rpmostree_client::Deployment> for Release {
fn from(d: &rpmostree_client::Deployment) -> Self {
Release {
checksum: self.base_revision(),
version: self.version,
checksum: d
.base_checksum
.clone()
.unwrap_or_else(|| d.checksum.clone()),
version: d.version.clone().unwrap_or_default(),
age_index: None,
}
}

/// Return the deployment base revision.
pub fn base_revision(&self) -> String {
self.base_checksum
.clone()
.unwrap_or_else(|| self.checksum.clone())
}
}

/// Parse base architecture for booted deployment from status object.
pub fn parse_basearch(status: &StatusJSON) -> Fallible<String> {
let json = booted_json(status)?;
Ok(json.base_metadata.basearch)
}

/// Parse the booted deployment from status object.
pub fn parse_booted(status: &StatusJSON) -> Fallible<Release> {
let json = booted_json(status)?;
Ok(json.into_release())
}

/// Parse updates stream for booted deployment from status object.
pub fn parse_updates_stream(status: &StatusJSON) -> Fallible<String> {
let json = booted_json(status)?;
ensure!(!json.base_metadata.stream.is_empty(), "empty stream value");
Ok(json.base_metadata.stream)
}

/// Parse local deployments from a status object.
fn parse_local_deployments(status: &StatusJSON, omit_staged: bool) -> Fallible<BTreeSet<Release>> {
fn parse_local_deployments(
status: &rpmostree_client::Status,
omit_staged: bool,
) -> Fallible<BTreeSet<Release>> {
let mut deployments = BTreeSet::<Release>::new();
for entry in &status.deployments {
if omit_staged && entry.staged {
if omit_staged && entry.staged.unwrap_or_default() {
continue;
}

let release = entry.clone().into_release();
let release = entry.clone().into();
deployments.insert(release);
}
Ok(deployments)
Expand All @@ -123,29 +73,14 @@ pub fn local_deployments(
client: &mut RpmOstreeClient,
omit_staged: bool,
) -> Fallible<BTreeSet<Release>> {
let status = status_json(client)?;
let status = query_status(client)?;
let local_depls = parse_local_deployments(&status, omit_staged)?;

Ok(local_depls)
}

/// Return JSON object for booted deployment.
fn booted_json(status: &StatusJSON) -> Fallible<DeploymentJSON> {
let booted = status
.clone()
.deployments
.into_iter()
.find(|d| d.booted)
.ok_or_else(|| format_err!("no booted deployment found"))?;

ensure!(!booted.base_revision().is_empty(), "empty base revision");
ensure!(!booted.version.is_empty(), "empty version");
ensure!(!booted.base_metadata.basearch.is_empty(), "empty basearch");
Ok(booted)
}

/// Ensure our status cache is up to date; if empty or out of date, run `rpm-ostree status` to populate it.
fn status_json(client: &mut RpmOstreeClient) -> Fallible<Rc<StatusJSON>> {
fn query_status_inner(client: &mut RpmOstreeClient) -> Fallible<Rc<rpmostree_client::Status>> {
STATUS_CACHE_ATTEMPTS.inc();
let ostree_depls_data = fs::metadata(OSTREE_DEPLS_PATH)
.with_context(|e| format_err!("failed to query directory {}: {}", OSTREE_DEPLS_PATH, e))?;
Expand All @@ -160,7 +95,9 @@ fn status_json(client: &mut RpmOstreeClient) -> Fallible<Rc<StatusJSON>> {

STATUS_CACHE_MISSES.inc();
trace!("cache stale, invoking rpm-ostree to retrieve local deployments");
let status = Rc::new(invoke_cli_status(false)?);
let status = Rc::new(
rpmostree_client::query_status(&*CLI_CLIENT).map_err(failure::Error::from_boxed_compat)?,
);
client.status_cache = Some(StatusCache {
status: Rc::clone(&status),
mtime: ostree_depls_data_mtime,
Expand All @@ -170,41 +107,26 @@ fn status_json(client: &mut RpmOstreeClient) -> Fallible<Rc<StatusJSON>> {
}

/// CLI executor for `rpm-ostree status --json`.
pub fn invoke_cli_status(booted_only: bool) -> Fallible<StatusJSON> {
pub fn query_status(client: &mut RpmOstreeClient) -> Fallible<Rc<rpmostree_client::Status>> {
RPM_OSTREE_STATUS_ATTEMPTS.inc();

let mut cmd = std::process::Command::new("rpm-ostree");
cmd.arg("status").env("RPMOSTREE_CLIENT_ID", "zincati");

// Try to request the minimum scope we need.
if booted_only {
cmd.arg("--booted");
}

let cmdrun = cmd
.arg("--json")
.output()
.with_context(|_| "failed to run 'rpm-ostree' binary")?;

if !cmdrun.status.success() {
RPM_OSTREE_STATUS_FAILURES.inc();
bail!(
"rpm-ostree status failed:\n{}",
String::from_utf8_lossy(&cmdrun.stderr)
);
match query_status_inner(client) {
Ok(s) => Ok(s),
Err(e) => {
RPM_OSTREE_STATUS_FAILURES.inc();
Err(e)
}
}
let status: StatusJSON = serde_json::from_slice(&cmdrun.stdout)?;
Ok(status)
}

#[cfg(test)]
mod tests {
use super::*;

fn mock_status(path: &str) -> Fallible<StatusJSON> {
fn mock_status(path: &str) -> Fallible<rpmostree_client::Status> {
let fp = std::fs::File::open(path).unwrap();
let bufrd = std::io::BufReader::new(fp);
let status: StatusJSON = serde_json::from_reader(bufrd)?;
let status = serde_json::from_reader(bufrd)?;
Ok(status)
}

Expand All @@ -226,18 +148,4 @@ mod tests {
assert_eq!(deployments.len(), 1);
}
}

#[test]
fn mock_booted_basearch() {
let status = mock_status("tests/fixtures/rpm-ostree-status.json").unwrap();
let booted = booted_json(&status).unwrap();
assert_eq!(booted.base_metadata.basearch, "x86_64");
}

#[test]
fn mock_booted_updates_stream() {
let status = mock_status("tests/fixtures/rpm-ostree-status.json").unwrap();
let booted = booted_json(&status).unwrap();
assert_eq!(booted.base_metadata.stream, "testing-devel");
}
}
13 changes: 12 additions & 1 deletion src/rpm_ostree/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
mod cli_deploy;
mod cli_finalize;
mod cli_status;
pub use cli_status::{invoke_cli_status, parse_basearch, parse_booted, parse_updates_stream};
pub use cli_status::query_status;
use lazy_static::lazy_static;

mod actor;
pub use actor::{
Expand All @@ -16,6 +17,16 @@ use failure::{ensure, format_err, Fallible, ResultExt};
use serde::Serialize;
use std::cmp::Ordering;

/// Metadata key the base (RPM) architecture; injected by coreos-assembler
pub(crate) const COSA_BASEARCH: &str = "coreos-assembler.basearch";
/// Metadata key for the Fedora CoreOS stream, injected by FCOS tooling via cosa.
pub(crate) const FCOS_STREAM: &str = "fedora-coreos.stream";

lazy_static! {
pub(crate) static ref CLI_CLIENT: rpmostree_client::CliClient =
rpmostree_client::CliClient::new("zincati");
}

/// An OS release.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Release {
Expand Down

0 comments on commit c457ecb

Please sign in to comment.