Skip to content

Commit 4e7e665

Browse files
committed
Add a systemd generator to fixup Anaconda's /etc/fstab
This is a giant and hacky workaround for ostreedev/ostree#3193 The better real fix is probably in either systemd or anaconda (more realistically both) but let's paper over things here for now. Having code to run as a generator will likely be useful in the future anyways. Signed-off-by: Colin Walters <[email protected]>
1 parent 231aad9 commit 4e7e665

File tree

7 files changed

+362
-2
lines changed

7 files changed

+362
-2
lines changed

Makefile

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ all-test:
88

99
install:
1010
install -D -m 0755 -t $(DESTDIR)$(prefix)/bin target/release/bootc
11+
install -d -m 0755 $(DESTDIR)$(prefix)/lib/systemd/system-generators/
12+
ln -f $(DESTDIR)$(prefix)/bin/bootc $(DESTDIR)$(prefix)/lib/systemd/system-generators/bootc-systemd-generator
1113
install -d $(DESTDIR)$(prefix)/lib/bootc/install
1214
# Support installing pre-generated man pages shipped in source tarball, to avoid
1315
# a dependency on pandoc downstream. But in local builds these end up in target/man,

contrib/packaging/bootc.spec

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ BuildRequires: systemd-devel
2828
%license LICENSE-APACHE LICENSE-MIT
2929
%doc README.md
3030
%{_bindir}/bootc
31+
%{_prefix}/lib/systemd/system-generators/*
3132
%{_prefix}/lib/bootc
3233
%{_unitdir}/*
3334
%{_mandir}/man*/bootc*

lib/src/cli.rs

+74-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use anyhow::{Context, Result};
66
use camino::Utf8PathBuf;
77
use cap_std_ext::cap_std;
8+
use cap_std_ext::cap_std::fs::Dir;
89
use clap::Parser;
910
use fn_error_context::context;
1011
use ostree::gio;
@@ -136,6 +137,24 @@ pub(crate) struct ManOpts {
136137
pub(crate) directory: Utf8PathBuf,
137138
}
138139

140+
/// Hidden, internal only options
141+
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
142+
pub(crate) enum InternalsOpts {
143+
SystemdGenerator {
144+
normal_dir: Utf8PathBuf,
145+
#[allow(dead_code)]
146+
early_dir: Option<Utf8PathBuf>,
147+
#[allow(dead_code)]
148+
late_dir: Option<Utf8PathBuf>,
149+
},
150+
FixupEtcFstab,
151+
}
152+
153+
impl InternalsOpts {
154+
/// The name of the binary we inject into /usr/lib/systemd/system-generators
155+
const GENERATOR_BIN: &'static str = "bootc-systemd-generator";
156+
}
157+
139158
/// Options for internal testing
140159
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
141160
pub(crate) enum TestingOpts {
@@ -226,6 +245,9 @@ pub(crate) enum Opt {
226245
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
227246
args: Vec<OsString>,
228247
},
248+
#[clap(subcommand)]
249+
#[clap(hide = true)]
250+
Internals(InternalsOpts),
229251
/// Internal integration testing helpers.
230252
#[clap(hide(true), subcommand)]
231253
#[cfg(feature = "internal-testing-api")]
@@ -525,11 +547,39 @@ where
525547
I: IntoIterator,
526548
I::Item: Into<OsString> + Clone,
527549
{
528-
run_from_opt(Opt::parse_from(args)).await
550+
run_from_opt(Opt::parse_including_static(args)).await
551+
}
552+
553+
impl Opt {
554+
/// In some cases (e.g. systemd generator) we dispatch specifically on argv0. This
555+
/// requires some special handling in clap.
556+
fn parse_including_static<I>(args: I) -> Self
557+
where
558+
I: IntoIterator,
559+
I::Item: Into<OsString> + Clone,
560+
{
561+
let mut args = args.into_iter();
562+
let first = if let Some(first) = args.next() {
563+
let first: OsString = first.into();
564+
let argv0 = first.to_str().and_then(|s| s.rsplit_once('/')).map(|s| s.1);
565+
tracing::debug!("argv0={argv0:?}");
566+
if matches!(argv0, Some(InternalsOpts::GENERATOR_BIN)) {
567+
let base_args = ["bootc", "internals", "systemd-generator"]
568+
.into_iter()
569+
.map(OsString::from);
570+
return Opt::parse_from(base_args.chain(args.map(|i| i.into())));
571+
}
572+
Some(first)
573+
} else {
574+
None
575+
};
576+
Opt::parse_from(first.into_iter().chain(args.map(|i| i.into())))
577+
}
529578
}
530579

531580
/// Internal (non-generic/monomorphized) primary CLI entrypoint
532581
async fn run_from_opt(opt: Opt) -> Result<()> {
582+
let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
533583
match opt {
534584
Opt::Upgrade(opts) => upgrade(opts).await,
535585
Opt::Switch(opts) => switch(opts).await,
@@ -551,6 +601,17 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
551601
crate::install::exec_in_host_mountns(args.as_slice())
552602
}
553603
Opt::Status(opts) => super::status::status(opts).await,
604+
Opt::Internals(opts) => match opts {
605+
InternalsOpts::SystemdGenerator {
606+
normal_dir,
607+
early_dir: _,
608+
late_dir: _,
609+
} => {
610+
let unit_dir = &Dir::open_ambient_dir(normal_dir, cap_std::ambient_authority())?;
611+
crate::generator::generator(root, unit_dir)
612+
}
613+
InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root),
614+
},
554615
#[cfg(feature = "internal-testing-api")]
555616
Opt::InternalTests(opts) => crate::privtests::run(opts).await,
556617
#[cfg(feature = "docgen")]
@@ -580,10 +641,21 @@ fn test_parse_install_args() {
580641
#[test]
581642
fn test_parse_opts() {
582643
assert!(matches!(
583-
Opt::parse_from(["bootc", "status"]),
644+
Opt::parse_including_static(["bootc", "status"]),
584645
Opt::Status(StatusOpts {
585646
json: false,
586647
booted: false
587648
})
588649
));
589650
}
651+
652+
#[test]
653+
fn test_parse_generator() {
654+
assert!(matches!(
655+
Opt::parse_including_static([
656+
"/usr/lib/systemd/system/bootc-systemd-generator",
657+
"/run/systemd/system"
658+
]),
659+
Opt::Internals(InternalsOpts::SystemdGenerator { .. })
660+
));
661+
}

lib/src/deploy.rs

+126
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
//!
33
//! Create a merged filesystem tree with the image and mounted configmaps.
44
5+
use std::io::{BufRead, Write};
6+
57
use anyhow::Ok;
68
use anyhow::{Context, Result};
79

@@ -377,3 +379,127 @@ fn test_switch_inplace() -> Result<()> {
377379
assert_eq!(replaced, target_deployment);
378380
Ok(())
379381
}
382+
383+
/// A workaround for https://github.com/ostreedev/ostree/issues/3193
384+
/// as generated by anaconda.
385+
#[context("Updating /etc/fstab for anaconda+composefs")]
386+
pub(crate) fn fixup_etc_fstab(root: &Dir) -> Result<()> {
387+
let fstab_path = "etc/fstab";
388+
// Read the old file
389+
let fd = root
390+
.open(fstab_path)
391+
.with_context(|| format!("Opening {fstab_path}"))
392+
.map(std::io::BufReader::new)?;
393+
394+
// Helper function to possibly change a line from /etc/fstab.
395+
// Returns Ok(true) if we made a change (and we wrote the modified line)
396+
// otherwise returns Ok(false) and the caller should write the original line.
397+
fn edit_fstab_line(line: &str, mut w: impl Write) -> Result<bool> {
398+
if line.starts_with("#") {
399+
return Ok(false);
400+
}
401+
let parts = line.split_ascii_whitespace().collect::<Vec<_>>();
402+
403+
let path = parts.get(1);
404+
let options = parts.get(3);
405+
if let (Some(&path), Some(&options)) = (path, options) {
406+
let options = options.split(',').collect::<Vec<_>>();
407+
if options.iter().any(|&s| s == "ro") {
408+
return Ok(false);
409+
}
410+
if path != "/" {
411+
return Ok(false);
412+
}
413+
} else {
414+
tracing::debug!("No path in entry: {line}");
415+
return Ok(false);
416+
};
417+
418+
writeln!(w, "# {}", crate::generator::BOOTC_EDITED_STAMP)?;
419+
420+
// SAFETY: we unpacked the options before.
421+
// This adds `ro` to the option list
422+
let options = format!("{},ro", options.unwrap());
423+
for (i, part) in parts.into_iter().enumerate() {
424+
if i > 0 {
425+
write!(w, " ")?;
426+
}
427+
if i == 3 {
428+
write!(w, "{options}")?;
429+
} else {
430+
write!(w, "{part}")?
431+
}
432+
}
433+
writeln!(w)?;
434+
Ok(true)
435+
}
436+
437+
// Read the input, and atomically write a modified version
438+
root.atomic_replace_with(fstab_path, move |mut w| {
439+
for line in fd.lines() {
440+
let line = line?;
441+
if !edit_fstab_line(&line, &mut w)? {
442+
writeln!(w, "{line}")?;
443+
}
444+
}
445+
Ok(())
446+
})
447+
.context("Replacing /etc/fstab")?;
448+
449+
println!("Updated /etc/fstab to add `ro` for `/`");
450+
Ok(())
451+
}
452+
453+
#[test]
454+
fn test_fixup_etc_fstab_default() -> Result<()> {
455+
let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
456+
let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n";
457+
tempdir.create_dir_all("etc")?;
458+
tempdir.atomic_write("etc/fstab", default)?;
459+
fixup_etc_fstab(&tempdir).unwrap();
460+
assert_eq!(tempdir.read_to_string("etc/fstab")?, default);
461+
Ok(())
462+
}
463+
464+
#[test]
465+
fn test_fixup_etc_fstab_multi() -> Result<()> {
466+
let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
467+
let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
468+
UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
469+
tempdir.create_dir_all("etc")?;
470+
tempdir.atomic_write("etc/fstab", default)?;
471+
fixup_etc_fstab(&tempdir).unwrap();
472+
assert_eq!(tempdir.read_to_string("etc/fstab")?, default);
473+
Ok(())
474+
}
475+
476+
#[test]
477+
fn test_fixup_etc_fstab_ro() -> Result<()> {
478+
let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
479+
let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
480+
UUID=1eef9f42-40e3-4bd8-ae20-e9f2325f8b52 / xfs ro 0 0\n\
481+
UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
482+
tempdir.create_dir_all("etc")?;
483+
tempdir.atomic_write("etc/fstab", default)?;
484+
fixup_etc_fstab(&tempdir).unwrap();
485+
assert_eq!(tempdir.read_to_string("etc/fstab")?, default);
486+
Ok(())
487+
}
488+
489+
#[test]
490+
fn test_fixup_etc_fstab_rw() -> Result<()> {
491+
let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
492+
// This case uses `defaults`
493+
let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
494+
UUID=1eef9f42-40e3-4bd8-ae20-e9f2325f8b52 / xfs defaults 0 0\n\
495+
UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
496+
let modified = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
497+
# Updated by bootc-fstab-edit.service\n\
498+
UUID=1eef9f42-40e3-4bd8-ae20-e9f2325f8b52 / xfs defaults,ro 0 0\n\
499+
UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
500+
tempdir.create_dir_all("etc")?;
501+
tempdir.atomic_write("etc/fstab", default)?;
502+
fixup_etc_fstab(&tempdir).unwrap();
503+
assert_eq!(tempdir.read_to_string("etc/fstab")?, modified);
504+
Ok(())
505+
}

0 commit comments

Comments
 (0)