Skip to content

Commit

Permalink
Merge pull request #977 from jeckersb/rhsm
Browse files Browse the repository at this point in the history
Add subscription-manager fact generation
  • Loading branch information
cgwalters authored Dec 19, 2024
2 parents f95d43c + 898bff0 commit c88fcfd
Show file tree
Hide file tree
Showing 14 changed files with 256 additions and 9 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ install:
install -D -m 0644 -t $(DESTDIR)$(prefix)/share/man/man8 $$d/*.8; \
fi; \
done
install -D -m 0644 -t $(DESTDIR)/$(prefix)/lib/systemd/system systemd/*.service systemd/*.timer
install -D -m 0644 -t $(DESTDIR)/$(prefix)/lib/systemd/system systemd/*.service systemd/*.timer systemd/*.path systemd/*.target

# Run this to also take over the functionality of `ostree container` for example.
# Only needed for OS/distros that have callers invoking `ostree container` and not bootc.
Expand Down
2 changes: 2 additions & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
- [`man bootc-rollback`](man/bootc-rollback.md)
- [`man bootc-usr-overlay`](man/bootc-usr-overlay.md)
- [`man bootc-fetch-apply-updates.service`](man-md/bootc-fetch-apply-updates.service.md)
- [`man bootc-status-updated.path`](man-md/bootc-status-updated.path.md)
- [`man bootc-status-updated.target`](man-md/bootc-status-updated.target.md)
- [Controlling bootc via API](bootc-via-api.md)

# Using `bootc install`
Expand Down
19 changes: 19 additions & 0 deletions docs/src/man-md/bootc-status-updated.path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
% bootc-status-updated.path(8)

# NAME

bootc-status-updated.path

# DESCRIPTION

This unit watches the `bootc` root directory (/ostree/bootc) for
modification, and triggers the companion `bootc-status-updated.target`
systemd unit.

The `bootc` program updates the mtime on its root directory when the
contents of `bootc status` changes as a result of an
update/upgrade/edit/switch/rollback operation.

# SEE ALSO

**bootc**(1), **bootc-status-updated.target**(8)
23 changes: 23 additions & 0 deletions docs/src/man-md/bootc-status-updated.target.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
% bootc-status-updated.target(8)

# NAME

bootc-status-updated.target

# DESCRIPTION

This unit is triggered by the companion `bootc-status-updated.path`
systemd unit. This target is intended to enable users to add custom
services to trigger as a result of `bootc status` changing.

Add the following to your unit configuration to active it when `bootc
status` changes:

```
[Install]
WantedBy=bootc-status-updated.target
```

# SEE ALSO

**bootc**(1), **bootc-status-updated.path**(8)
3 changes: 3 additions & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
11 changes: 11 additions & 0 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,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)]
Expand Down Expand Up @@ -766,6 +769,8 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
}
}
if changed {
sysroot.update_mtime()?;

if opts.apply {
crate::reboot::reboot()?;
}
Expand Down Expand Up @@ -842,6 +847,8 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
let stateroot = booted_deployment.osname();
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;

sysroot.update_mtime()?;

if opts.apply {
crate::reboot::reboot()?;
}
Expand Down Expand Up @@ -897,6 +904,8 @@ async fn edit(opts: EditOpts) -> Result<()> {
let stateroot = booted_deployment.osname();
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;

sysroot.update_mtime()?;

Ok(())
}

Expand Down Expand Up @@ -1100,6 +1109,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),
Expand Down
3 changes: 3 additions & 0 deletions lib/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,9 @@ pub(crate) async fn rollback(sysroot: &Storage) -> Result<()> {
} else {
println!("Next boot: rollback deployment");
}

sysroot.update_mtime()?;

Ok(())
}

Expand Down
20 changes: 13 additions & 7 deletions lib/src/imgstorage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use std::sync::Arc;

use anyhow::{Context, Result};
use bootc_utils::{AsyncCommandRunExt, CommandRunExt, ExitStatusExt};
use camino::Utf8Path;
use camino::{Utf8Path, Utf8PathBuf};
use cap_std_ext::cap_std;
use cap_std_ext::cap_std::fs::Dir;
use cap_std_ext::cap_tempfile::TempDir;
Expand All @@ -35,8 +35,8 @@ pub(crate) const STORAGE_ALIAS_DIR: &str = "/run/bootc/storage";
/// We pass this via /proc/self/fd to the child process.
const STORAGE_RUN_FD: i32 = 3;

/// The path to the storage, relative to the physical system root.
pub(crate) const SUBPATH: &str = "ostree/bootc/storage";
/// The path to the image storage, relative to the bootc root directory.
pub(crate) const SUBPATH: &str = "storage";
/// The path to the "runroot" with transient runtime state; this is
/// relative to the /run directory
const RUNROOT: &str = "bootc/storage";
Expand Down Expand Up @@ -139,14 +139,15 @@ impl Storage {
#[context("Creating imgstorage")]
pub(crate) fn create(sysroot: &Dir, run: &Dir) -> Result<Self> {
Self::init_globals()?;
let subpath = Utf8Path::new(SUBPATH);
let subpath = &Self::subpath();

// SAFETY: We know there's a parent
let parent = subpath.parent().unwrap();
if !sysroot
.try_exists(subpath)
.with_context(|| format!("Querying {subpath}"))?
{
let tmp = format!("{SUBPATH}.tmp");
let tmp = format!("{subpath}.tmp");
sysroot.remove_all_optional(&tmp).context("Removing tmp")?;
sysroot
.create_dir_all(parent)
Expand Down Expand Up @@ -174,9 +175,10 @@ impl Storage {
pub(crate) fn open(sysroot: &Dir, run: &Dir) -> Result<Self> {
tracing::trace!("Opening container image store");
Self::init_globals()?;
let subpath = &Self::subpath();
let storage_root = sysroot
.open_dir(SUBPATH)
.with_context(|| format!("Opening {SUBPATH}"))?;
.open_dir(subpath)
.with_context(|| format!("Opening {subpath}"))?;
// Always auto-create this if missing
run.create_dir_all(RUNROOT)
.with_context(|| format!("Creating {RUNROOT}"))?;
Expand Down Expand Up @@ -303,6 +305,10 @@ impl Storage {
temp_runroot.close()?;
Ok(())
}

fn subpath() -> Utf8PathBuf {
Utf8Path::new(crate::store::BOOTC_ROOT).join(SUBPATH)
}
}

#[cfg(test)]
Expand Down
3 changes: 3 additions & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ mod install;
mod kernel;
#[cfg(feature = "install")]
pub(crate) mod mount;

#[cfg(feature = "rhsm")]
mod rhsm;
134 changes: 134 additions & 0 deletions lib/src/rhsm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//! 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, PartialEq, Eq, Debug, Default)]
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,
}

/// Return the image reference, version and digest as owned strings.
/// A missing version is serialized as the empty string.
fn status_to_strings(imagestatus: &crate::spec::ImageStatus) -> (String, String, String) {
let image = imagestatus.image.image.clone();
let version = imagestatus.version.as_ref().cloned().unwrap_or_default();
let digest = imagestatus.image_digest.clone();
(image, version, digest)
}

impl From<crate::spec::HostStatus> 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(status_to_strings))
.unwrap_or_default();

let (staged_image, staged_version, staged_digest) = hoststatus
.staged
.as_ref()
.and_then(|boot_entry| boot_entry.image.as_ref().map(status_to_strings))
.unwrap_or_default();

let (rollback_image, rollback_version, rollback_digest) = hoststatus
.rollback
.as_ref()
.and_then(|boot_entry| boot_entry.image.as_ref().map(status_to_strings))
.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(status_to_strings))
.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(())
}

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

use crate::spec::Host;

#[test]
fn test_rhsm_facts_from_host() {
let host: Host = serde_yaml::from_str(include_str!("fixtures/spec-staged-booted.yaml"))
.expect("No spec found");
let facts = RhsmFacts::from(host.status);

assert_eq!(
facts,
RhsmFacts {
booted_image: "quay.io/example/someimage:latest".into(),
booted_version: "nightly".into(),
booted_digest:
"sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34".into(),
staged_image: "quay.io/example/someimage:latest".into(),
staged_version: "nightly".into(),
staged_digest:
"sha256:16dc2b6256b4ff0d2ec18d2dbfb06d117904010c8cf9732cdb022818cf7a7566".into(),
..Default::default()
}
);
}
}
20 changes: 19 additions & 1 deletion lib/src/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ use std::cell::OnceCell;
use std::env;
use std::ops::Deref;

use anyhow::Result;
use anyhow::{Context, Result};
use cap_std_ext::cap_std::fs::Dir;
use cap_std_ext::dirext::CapStdExtDirExt;
use clap::ValueEnum;
use fn_error_context::context;

use ostree_ext::container::OstreeImageReference;
use ostree_ext::keyfileext::KeyFileExt;
Expand All @@ -15,6 +17,10 @@ use crate::spec::ImageStatus;

mod ostree_container;

/// The path to the bootc root directory, relative to the physical
/// system root
pub(crate) const BOOTC_ROOT: &str = "ostree/bootc";

pub(crate) struct Storage {
pub sysroot: SysrootLock,
run: Dir,
Expand Down Expand Up @@ -82,6 +88,18 @@ impl Storage {
let imgstore = crate::imgstorage::Storage::create(&sysroot_dir, &self.run)?;
Ok(self.imgstore.get_or_init(|| imgstore))
}

/// Update the mtime on the storage root directory
#[context("Updating storage root mtime")]
pub(crate) fn update_mtime(&self) -> Result<()> {
let sysroot_dir =
crate::utils::sysroot_dir(&self.sysroot).context("Reopen sysroot directory")?;

sysroot_dir
.update_timestamps(std::path::Path::new(BOOTC_ROOT))
.context("update_timestamps")
.map_err(Into::into)
}
}

impl ContainerImageStore for ostree::Deployment {
Expand Down
11 changes: 11 additions & 0 deletions systemd/bootc-publish-rhsm-facts.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[Unit]
Description=Publish bootc facts to Red Hat Subscription Manager
Documentation=man:bootc(8)
ConditionPathExists=/etc/rhsm/facts

[Service]
Type=oneshot
ExecStart=/usr/bin/bootc internals publish-rhsm-facts

[Install]
WantedBy=bootc-status-updated.target
10 changes: 10 additions & 0 deletions systemd/bootc-status-updated.path
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=Monitor bootc for status changes
Documentation=man:bootc-status-updated.path(8)

[Path]
PathChanged=/ostree/bootc
Unit=bootc-status-updated.target

[Install]
WantedBy=multi-user.target
Loading

0 comments on commit c88fcfd

Please sign in to comment.