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

Add subscription-manager fact generation #977

Merged
merged 6 commits into from
Dec 19, 2024
Merged
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
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 = []
Copy link
Collaborator

Choose a reason for hiding this comment

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

OK as is, but we will also need to do a corresponding change to the Fedora spec file to turn it on (bcond that is default off in Fedora, on in RHEL?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

# 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()?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Unrelated to this particular changed line but I think we could test this in one of the integration tests pretty easily.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thinking about punting this to a followup. Unless I'm blind, it doesn't look like in integration we currently test any of upgrade/edit/switch/rollback so that's a whole other thing.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We do in test-image-pushpull-upgrade.nu for example

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yeah I was just looking at the stuff under tests-integration


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 {
jeckersb marked this conversation as resolved.
Show resolved Hide resolved
fn from(hoststatus: crate::spec::HostStatus) -> Self {
let (booted_image, booted_version, booted_digest) = hoststatus
.booted
.as_ref()
cgwalters marked this conversation as resolved.
Show resolved Hide resolved
.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
Loading