From d7e34faeb431b1a772fb9e4b0ef294ca6e5e9140 Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Mon, 16 Dec 2024 16:37:55 -0500 Subject: [PATCH] Add "rhsm" feature for integration with Red Hat Subscription Manager This adds a new subcommand `bootc internals publish-rhsm-facts` which writes out facts data to /etc/rhsm/facts/bootc.json Signed-off-by: John Eckersberg --- lib/Cargo.toml | 3 ++ lib/src/cli.rs | 5 ++ lib/src/lib.rs | 3 ++ lib/src/rhsm.rs | 128 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 lib/src/rhsm.rs diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 793aafcb..01bc9217 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -56,6 +56,9 @@ static_assertions = { workspace = true } default = ["install"] # This feature enables `bootc install`. Disable if you always want to use an external installer. install = [] +# This featuares enables `bootc internals publish-rhsm-facts` to integrate with +# Red Hat Subscription Manager +rhsm = [] # Implementation detail of man page generation. docgen = ["clap_mangen"] diff --git a/lib/src/cli.rs b/lib/src/cli.rs index d746b5e6..81e1892a 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -373,6 +373,9 @@ pub(crate) enum InternalsOpts { // The stateroot stateroot: String, }, + #[cfg(feature = "rhsm")] + /// Publish subscription-manager facts to /etc/rhsm/facts/bootc.json + PublishRhsmFacts, } #[derive(Debug, clap::Subcommand, PartialEq, Eq)] @@ -1094,6 +1097,8 @@ async fn run_from_opt(opt: Opt) -> Result<()> { let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?; crate::install::completion::run_from_ostree(rootfs, &sysroot, &stateroot).await } + #[cfg(feature = "rhsm")] + InternalsOpts::PublishRhsmFacts => crate::rhsm::publish_facts(&root).await, }, #[cfg(feature = "docgen")] Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory), diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 67aedfbc..daa4bfc4 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -42,3 +42,6 @@ mod install; mod kernel; #[cfg(feature = "install")] pub(crate) mod mount; + +#[cfg(feature = "rhsm")] +mod rhsm; diff --git a/lib/src/rhsm.rs b/lib/src/rhsm.rs new file mode 100644 index 00000000..81e76b50 --- /dev/null +++ b/lib/src/rhsm.rs @@ -0,0 +1,128 @@ +//! Integration with Red Hat Subscription Manager + +use anyhow::Result; +use cap_std::fs::{Dir, OpenOptions}; +use cap_std_ext::cap_std; +use fn_error_context::context; +use serde::Serialize; + +const FACTS_PATH: &str = "etc/rhsm/facts/bootc.json"; + +#[derive(Serialize)] +struct RhsmFacts { + #[serde(rename = "bootc.booted.image")] + booted_image: String, + #[serde(rename = "bootc.booted.version")] + booted_version: String, + #[serde(rename = "bootc.booted.digest")] + booted_digest: String, + #[serde(rename = "bootc.staged.image")] + staged_image: String, + #[serde(rename = "bootc.staged.version")] + staged_version: String, + #[serde(rename = "bootc.staged.digest")] + staged_digest: String, + #[serde(rename = "bootc.rollback.image")] + rollback_image: String, + #[serde(rename = "bootc.rollback.version")] + rollback_version: String, + #[serde(rename = "bootc.rollback.digest")] + rollback_digest: String, + #[serde(rename = "bootc.available.image")] + available_image: String, + #[serde(rename = "bootc.available.version")] + available_version: String, + #[serde(rename = "bootc.available.digest")] + available_digest: String, +} + +impl From for RhsmFacts { + fn from(hoststatus: crate::spec::HostStatus) -> Self { + let (booted_image, booted_version, booted_digest) = hoststatus + .booted + .as_ref() + .and_then(|boot_entry| { + boot_entry.image.as_ref().map(|imagestatus| { + let image = imagestatus.image.image.clone(); + let version = imagestatus.version.as_ref().cloned().unwrap_or_default(); + let digest = imagestatus.image_digest.clone(); + + (image, digest, version) + }) + }) + .unwrap_or_default(); + + let (staged_image, staged_version, staged_digest) = hoststatus + .staged + .as_ref() + .and_then(|boot_entry| { + boot_entry.image.as_ref().map(|imagestatus| { + let image = imagestatus.image.image.clone(); + let version = imagestatus.version.as_ref().cloned().unwrap_or_default(); + let digest = imagestatus.image_digest.clone(); + + (image, digest, version) + }) + }) + .unwrap_or_default(); + + let (rollback_image, rollback_version, rollback_digest) = hoststatus + .rollback + .as_ref() + .and_then(|boot_entry| { + boot_entry.image.as_ref().map(|imagestatus| { + let image = imagestatus.image.image.clone(); + let version = imagestatus.version.as_ref().cloned().unwrap_or_default(); + let digest = imagestatus.image_digest.clone(); + + (image, digest, version) + }) + }) + .unwrap_or_default(); + + let (available_image, available_version, available_digest) = hoststatus + .booted + .as_ref() + .and_then(|boot_entry| { + boot_entry.cached_update.as_ref().map(|imagestatus| { + let image = imagestatus.image.image.clone(); + let version = imagestatus.version.as_ref().cloned().unwrap_or_default(); + let digest = imagestatus.image_digest.clone(); + + (image, digest, version) + }) + }) + .unwrap_or_default(); + + Self { + booted_image, + booted_version, + booted_digest, + staged_image, + staged_version, + staged_digest, + rollback_image, + rollback_version, + rollback_digest, + available_image, + available_version, + available_digest, + } + } +} + +/// Publish facts for subscription-manager consumption +#[context("Publishing facts")] +pub(crate) async fn publish_facts(root: &Dir) -> Result<()> { + let sysroot = super::cli::get_storage().await?; + let booted_deployment = sysroot.booted_deployment(); + let (_deployments, host) = crate::status::get_status(&sysroot, booted_deployment.as_ref())?; + + let facts = RhsmFacts::from(host.status); + let mut bootc_facts_file = root.open_with( + FACTS_PATH, + OpenOptions::new().write(true).create(true).truncate(true), + )?; + serde_json::to_writer_pretty(&mut bootc_facts_file, &facts)?; + Ok(()) +}