Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rpm-ostree: Use upstream Rust bindings #491

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
153 changes: 30 additions & 123 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,83 +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(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks sketchy, and may break other logic in the app.

I don't know in which cases we can end up with a version-less commit, but it may be good to log a warning and filter it out.

age_index: None,
}
}

/// Return the deployment base revision.
pub fn base_revision(&self) -> String {
cgwalters marked this conversation as resolved.
Show resolved Hide resolved
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();
deployments.insert(release);
deployments.insert(entry.into());
}
Ok(deployments)
}
Expand All @@ -123,29 +72,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 +94,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 +106,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");
cgwalters marked this conversation as resolved.
Show resolved Hide resolved

// 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> {
cgwalters marked this conversation as resolved.
Show resolved Hide resolved
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 +147,4 @@ mod tests {
assert_eq!(deployments.len(), 1);
}
}

#[test]
fn mock_booted_basearch() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two are some basic unit tests to ensure we don't regress on parsing FCOS-specific metadata, I'd rather not drop it.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Metadata key the base (RPM) architecture; injected by coreos-assembler
/// Metadata key for 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