Skip to content

Commit c88fcfd

Browse files
authored
Merge pull request #977 from jeckersb/rhsm
Add subscription-manager fact generation
2 parents f95d43c + 898bff0 commit c88fcfd

14 files changed

+256
-9
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ install:
2424
install -D -m 0644 -t $(DESTDIR)$(prefix)/share/man/man8 $$d/*.8; \
2525
fi; \
2626
done
27-
install -D -m 0644 -t $(DESTDIR)/$(prefix)/lib/systemd/system systemd/*.service systemd/*.timer
27+
install -D -m 0644 -t $(DESTDIR)/$(prefix)/lib/systemd/system systemd/*.service systemd/*.timer systemd/*.path systemd/*.target
2828

2929
# Run this to also take over the functionality of `ostree container` for example.
3030
# Only needed for OS/distros that have callers invoking `ostree container` and not bootc.

docs/src/SUMMARY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
- [`man bootc-rollback`](man/bootc-rollback.md)
2929
- [`man bootc-usr-overlay`](man/bootc-usr-overlay.md)
3030
- [`man bootc-fetch-apply-updates.service`](man-md/bootc-fetch-apply-updates.service.md)
31+
- [`man bootc-status-updated.path`](man-md/bootc-status-updated.path.md)
32+
- [`man bootc-status-updated.target`](man-md/bootc-status-updated.target.md)
3133
- [Controlling bootc via API](bootc-via-api.md)
3234

3335
# Using `bootc install`
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
% bootc-status-updated.path(8)
2+
3+
# NAME
4+
5+
bootc-status-updated.path
6+
7+
# DESCRIPTION
8+
9+
This unit watches the `bootc` root directory (/ostree/bootc) for
10+
modification, and triggers the companion `bootc-status-updated.target`
11+
systemd unit.
12+
13+
The `bootc` program updates the mtime on its root directory when the
14+
contents of `bootc status` changes as a result of an
15+
update/upgrade/edit/switch/rollback operation.
16+
17+
# SEE ALSO
18+
19+
**bootc**(1), **bootc-status-updated.target**(8)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
% bootc-status-updated.target(8)
2+
3+
# NAME
4+
5+
bootc-status-updated.target
6+
7+
# DESCRIPTION
8+
9+
This unit is triggered by the companion `bootc-status-updated.path`
10+
systemd unit. This target is intended to enable users to add custom
11+
services to trigger as a result of `bootc status` changing.
12+
13+
Add the following to your unit configuration to active it when `bootc
14+
status` changes:
15+
16+
```
17+
[Install]
18+
WantedBy=bootc-status-updated.target
19+
```
20+
21+
# SEE ALSO
22+
23+
**bootc**(1), **bootc-status-updated.path**(8)

lib/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ static_assertions = { workspace = true }
5656
default = ["install"]
5757
# This feature enables `bootc install`. Disable if you always want to use an external installer.
5858
install = []
59+
# This featuares enables `bootc internals publish-rhsm-facts` to integrate with
60+
# Red Hat Subscription Manager
61+
rhsm = []
5962
# Implementation detail of man page generation.
6063
docgen = ["clap_mangen"]
6164

lib/src/cli.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,9 @@ pub(crate) enum InternalsOpts {
393393
// The stateroot
394394
stateroot: String,
395395
},
396+
#[cfg(feature = "rhsm")]
397+
/// Publish subscription-manager facts to /etc/rhsm/facts/bootc.json
398+
PublishRhsmFacts,
396399
}
397400

398401
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
@@ -766,6 +769,8 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
766769
}
767770
}
768771
if changed {
772+
sysroot.update_mtime()?;
773+
769774
if opts.apply {
770775
crate::reboot::reboot()?;
771776
}
@@ -842,6 +847,8 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
842847
let stateroot = booted_deployment.osname();
843848
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
844849

850+
sysroot.update_mtime()?;
851+
845852
if opts.apply {
846853
crate::reboot::reboot()?;
847854
}
@@ -897,6 +904,8 @@ async fn edit(opts: EditOpts) -> Result<()> {
897904
let stateroot = booted_deployment.osname();
898905
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
899906

907+
sysroot.update_mtime()?;
908+
900909
Ok(())
901910
}
902911

@@ -1100,6 +1109,8 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
11001109
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
11011110
crate::install::completion::run_from_ostree(rootfs, &sysroot, &stateroot).await
11021111
}
1112+
#[cfg(feature = "rhsm")]
1113+
InternalsOpts::PublishRhsmFacts => crate::rhsm::publish_facts(&root).await,
11031114
},
11041115
#[cfg(feature = "docgen")]
11051116
Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory),

lib/src/deploy.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,9 @@ pub(crate) async fn rollback(sysroot: &Storage) -> Result<()> {
747747
} else {
748748
println!("Next boot: rollback deployment");
749749
}
750+
751+
sysroot.update_mtime()?;
752+
750753
Ok(())
751754
}
752755

lib/src/imgstorage.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::sync::Arc;
1313

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

38-
/// The path to the storage, relative to the physical system root.
39-
pub(crate) const SUBPATH: &str = "ostree/bootc/storage";
38+
/// The path to the image storage, relative to the bootc root directory.
39+
pub(crate) const SUBPATH: &str = "storage";
4040
/// The path to the "runroot" with transient runtime state; this is
4141
/// relative to the /run directory
4242
const RUNROOT: &str = "bootc/storage";
@@ -139,14 +139,15 @@ impl Storage {
139139
#[context("Creating imgstorage")]
140140
pub(crate) fn create(sysroot: &Dir, run: &Dir) -> Result<Self> {
141141
Self::init_globals()?;
142-
let subpath = Utf8Path::new(SUBPATH);
142+
let subpath = &Self::subpath();
143+
143144
// SAFETY: We know there's a parent
144145
let parent = subpath.parent().unwrap();
145146
if !sysroot
146147
.try_exists(subpath)
147148
.with_context(|| format!("Querying {subpath}"))?
148149
{
149-
let tmp = format!("{SUBPATH}.tmp");
150+
let tmp = format!("{subpath}.tmp");
150151
sysroot.remove_all_optional(&tmp).context("Removing tmp")?;
151152
sysroot
152153
.create_dir_all(parent)
@@ -174,9 +175,10 @@ impl Storage {
174175
pub(crate) fn open(sysroot: &Dir, run: &Dir) -> Result<Self> {
175176
tracing::trace!("Opening container image store");
176177
Self::init_globals()?;
178+
let subpath = &Self::subpath();
177179
let storage_root = sysroot
178-
.open_dir(SUBPATH)
179-
.with_context(|| format!("Opening {SUBPATH}"))?;
180+
.open_dir(subpath)
181+
.with_context(|| format!("Opening {subpath}"))?;
180182
// Always auto-create this if missing
181183
run.create_dir_all(RUNROOT)
182184
.with_context(|| format!("Creating {RUNROOT}"))?;
@@ -303,6 +305,10 @@ impl Storage {
303305
temp_runroot.close()?;
304306
Ok(())
305307
}
308+
309+
fn subpath() -> Utf8PathBuf {
310+
Utf8Path::new(crate::store::BOOTC_ROOT).join(SUBPATH)
311+
}
306312
}
307313

308314
#[cfg(test)]

lib/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ mod install;
4242
mod kernel;
4343
#[cfg(feature = "install")]
4444
pub(crate) mod mount;
45+
46+
#[cfg(feature = "rhsm")]
47+
mod rhsm;

lib/src/rhsm.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//! Integration with Red Hat Subscription Manager
2+
3+
use anyhow::Result;
4+
use cap_std::fs::{Dir, OpenOptions};
5+
use cap_std_ext::cap_std;
6+
use fn_error_context::context;
7+
use serde::Serialize;
8+
9+
const FACTS_PATH: &str = "etc/rhsm/facts/bootc.json";
10+
11+
#[derive(Serialize, PartialEq, Eq, Debug, Default)]
12+
struct RhsmFacts {
13+
#[serde(rename = "bootc.booted.image")]
14+
booted_image: String,
15+
#[serde(rename = "bootc.booted.version")]
16+
booted_version: String,
17+
#[serde(rename = "bootc.booted.digest")]
18+
booted_digest: String,
19+
#[serde(rename = "bootc.staged.image")]
20+
staged_image: String,
21+
#[serde(rename = "bootc.staged.version")]
22+
staged_version: String,
23+
#[serde(rename = "bootc.staged.digest")]
24+
staged_digest: String,
25+
#[serde(rename = "bootc.rollback.image")]
26+
rollback_image: String,
27+
#[serde(rename = "bootc.rollback.version")]
28+
rollback_version: String,
29+
#[serde(rename = "bootc.rollback.digest")]
30+
rollback_digest: String,
31+
#[serde(rename = "bootc.available.image")]
32+
available_image: String,
33+
#[serde(rename = "bootc.available.version")]
34+
available_version: String,
35+
#[serde(rename = "bootc.available.digest")]
36+
available_digest: String,
37+
}
38+
39+
/// Return the image reference, version and digest as owned strings.
40+
/// A missing version is serialized as the empty string.
41+
fn status_to_strings(imagestatus: &crate::spec::ImageStatus) -> (String, String, String) {
42+
let image = imagestatus.image.image.clone();
43+
let version = imagestatus.version.as_ref().cloned().unwrap_or_default();
44+
let digest = imagestatus.image_digest.clone();
45+
(image, version, digest)
46+
}
47+
48+
impl From<crate::spec::HostStatus> for RhsmFacts {
49+
fn from(hoststatus: crate::spec::HostStatus) -> Self {
50+
let (booted_image, booted_version, booted_digest) = hoststatus
51+
.booted
52+
.as_ref()
53+
.and_then(|boot_entry| boot_entry.image.as_ref().map(status_to_strings))
54+
.unwrap_or_default();
55+
56+
let (staged_image, staged_version, staged_digest) = hoststatus
57+
.staged
58+
.as_ref()
59+
.and_then(|boot_entry| boot_entry.image.as_ref().map(status_to_strings))
60+
.unwrap_or_default();
61+
62+
let (rollback_image, rollback_version, rollback_digest) = hoststatus
63+
.rollback
64+
.as_ref()
65+
.and_then(|boot_entry| boot_entry.image.as_ref().map(status_to_strings))
66+
.unwrap_or_default();
67+
68+
let (available_image, available_version, available_digest) = hoststatus
69+
.booted
70+
.as_ref()
71+
.and_then(|boot_entry| boot_entry.cached_update.as_ref().map(status_to_strings))
72+
.unwrap_or_default();
73+
74+
Self {
75+
booted_image,
76+
booted_version,
77+
booted_digest,
78+
staged_image,
79+
staged_version,
80+
staged_digest,
81+
rollback_image,
82+
rollback_version,
83+
rollback_digest,
84+
available_image,
85+
available_version,
86+
available_digest,
87+
}
88+
}
89+
}
90+
91+
/// Publish facts for subscription-manager consumption
92+
#[context("Publishing facts")]
93+
pub(crate) async fn publish_facts(root: &Dir) -> Result<()> {
94+
let sysroot = super::cli::get_storage().await?;
95+
let booted_deployment = sysroot.booted_deployment();
96+
let (_deployments, host) = crate::status::get_status(&sysroot, booted_deployment.as_ref())?;
97+
98+
let facts = RhsmFacts::from(host.status);
99+
let mut bootc_facts_file = root.open_with(
100+
FACTS_PATH,
101+
OpenOptions::new().write(true).create(true).truncate(true),
102+
)?;
103+
serde_json::to_writer_pretty(&mut bootc_facts_file, &facts)?;
104+
Ok(())
105+
}
106+
107+
#[cfg(test)]
108+
mod tests {
109+
use super::*;
110+
111+
use crate::spec::Host;
112+
113+
#[test]
114+
fn test_rhsm_facts_from_host() {
115+
let host: Host = serde_yaml::from_str(include_str!("fixtures/spec-staged-booted.yaml"))
116+
.expect("No spec found");
117+
let facts = RhsmFacts::from(host.status);
118+
119+
assert_eq!(
120+
facts,
121+
RhsmFacts {
122+
booted_image: "quay.io/example/someimage:latest".into(),
123+
booted_version: "nightly".into(),
124+
booted_digest:
125+
"sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34".into(),
126+
staged_image: "quay.io/example/someimage:latest".into(),
127+
staged_version: "nightly".into(),
128+
staged_digest:
129+
"sha256:16dc2b6256b4ff0d2ec18d2dbfb06d117904010c8cf9732cdb022818cf7a7566".into(),
130+
..Default::default()
131+
}
132+
);
133+
}
134+
}

0 commit comments

Comments
 (0)