Skip to content

Commit 3bbf7ee

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 3bbf7ee

File tree

6 files changed

+312
-2
lines changed

6 files changed

+312
-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,

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

+120
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,121 @@ 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+
// SAFETY: we unpacked the options before.
419+
// This adds `ro` to the option list
420+
let options = format!("{},ro", options.unwrap());
421+
for (i, part) in parts.into_iter().enumerate() {
422+
if i > 0 {
423+
write!(w, " ")?;
424+
}
425+
if i == 3 {
426+
write!(w, "{options}")?;
427+
} else {
428+
write!(w, "{part}")?
429+
}
430+
}
431+
writeln!(w)?;
432+
Ok(true)
433+
}
434+
435+
// Read the input, and atomically write a modified version
436+
root.atomic_replace_with(fstab_path, move |mut w| {
437+
for line in fd.lines() {
438+
let line = line?;
439+
if !edit_fstab_line(&line, &mut w)? {
440+
writeln!(w, "{line}")?;
441+
}
442+
}
443+
Ok(())
444+
})
445+
.context("Replacing /etc/fstab")
446+
}
447+
448+
#[test]
449+
fn test_fixup_etc_fstab_default() -> Result<()> {
450+
let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
451+
let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n";
452+
tempdir.create_dir_all("etc")?;
453+
tempdir.atomic_write("etc/fstab", default)?;
454+
fixup_etc_fstab(&tempdir).unwrap();
455+
assert_eq!(tempdir.read_to_string("etc/fstab")?, default);
456+
Ok(())
457+
}
458+
459+
#[test]
460+
fn test_fixup_etc_fstab_multi() -> Result<()> {
461+
let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
462+
let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
463+
UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
464+
tempdir.create_dir_all("etc")?;
465+
tempdir.atomic_write("etc/fstab", default)?;
466+
fixup_etc_fstab(&tempdir).unwrap();
467+
assert_eq!(tempdir.read_to_string("etc/fstab")?, default);
468+
Ok(())
469+
}
470+
471+
#[test]
472+
fn test_fixup_etc_fstab_ro() -> Result<()> {
473+
let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
474+
let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
475+
UUID=1eef9f42-40e3-4bd8-ae20-e9f2325f8b52 / xfs ro 0 0\n\
476+
UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
477+
tempdir.create_dir_all("etc")?;
478+
tempdir.atomic_write("etc/fstab", default)?;
479+
fixup_etc_fstab(&tempdir).unwrap();
480+
assert_eq!(tempdir.read_to_string("etc/fstab")?, default);
481+
Ok(())
482+
}
483+
484+
#[test]
485+
fn test_fixup_etc_fstab_rw() -> Result<()> {
486+
let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
487+
// This case uses `defaults`
488+
let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
489+
UUID=1eef9f42-40e3-4bd8-ae20-e9f2325f8b52 / xfs defaults 0 0\n\
490+
UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
491+
let modified = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
492+
UUID=1eef9f42-40e3-4bd8-ae20-e9f2325f8b52 / xfs defaults,ro 0 0\n\
493+
UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
494+
tempdir.create_dir_all("etc")?;
495+
tempdir.atomic_write("etc/fstab", default)?;
496+
fixup_etc_fstab(&tempdir).unwrap();
497+
assert_eq!(tempdir.read_to_string("etc/fstab")?, modified);
498+
Ok(())
499+
}

lib/src/generator.rs

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use std::io::BufRead;
2+
3+
use anyhow::{Context, Result};
4+
use cap_std::fs::Dir;
5+
use cap_std_ext::{cap_std, dirext::CapStdExtDirExt};
6+
use fn_error_context::context;
7+
use rustix::{fd::AsFd, fs::StatVfsMountFlags};
8+
9+
const FSTAB_ANACONDA_STAMP: &str = "Created by anaconda";
10+
11+
/// Called when the root is read-only composefs to reconcile /etc/fstab
12+
#[context("bootc generator")]
13+
pub(crate) fn fstab_generator_impl(root: &Dir, unit_dir: &Dir) -> Result<()> {
14+
// Do nothing if not ostree-booted
15+
if !root.try_exists("run/ostree-booted")? {
16+
return Ok(());
17+
}
18+
19+
if let Some(fd) = root
20+
.open_optional("etc/fstab")
21+
.context("Opening /etc/fstab")?
22+
.map(std::io::BufReader::new)
23+
{
24+
let mut from_anaconda = false;
25+
// Only read 10 lines just because it should be near the start
26+
for line in fd.lines().take(10) {
27+
let line = line.context("Reading /etc/fstab")?;
28+
if line.contains(FSTAB_ANACONDA_STAMP) {
29+
from_anaconda = true;
30+
break;
31+
}
32+
}
33+
tracing::debug!("/etc/fstab from anaconda: {from_anaconda}");
34+
if from_anaconda {
35+
generate_fstab_editor(unit_dir)?;
36+
}
37+
}
38+
Ok(())
39+
}
40+
41+
/// Main entrypoint for the generator
42+
pub(crate) fn generator(root: &Dir, unit_dir: &Dir) -> Result<()> {
43+
// Right now we only do something if the root is a read-only overlayfs (a composefs really)
44+
let st = rustix::fs::fstatfs(root.as_fd())?;
45+
if st.f_type != libc::OVERLAYFS_SUPER_MAGIC {
46+
tracing::trace!("Root is not overlayfs");
47+
return Ok(());
48+
}
49+
let st = rustix::fs::fstatvfs(root.as_fd())?;
50+
if !st.f_flag.contains(StatVfsMountFlags::RDONLY) {
51+
tracing::trace!("Root is writable");
52+
return Ok(());
53+
}
54+
fstab_generator_impl(root, unit_dir)
55+
}
56+
57+
/// Parse /etc/fstab and check if the root mount is out of sync with the composefs
58+
/// state, and if so, fix it.
59+
fn generate_fstab_editor(unit_dir: &Dir) -> Result<()> {
60+
let unit = "bootc-fstab-edit.service";
61+
unit_dir.atomic_write(
62+
unit,
63+
"[Unit]\n\
64+
Before=local-fs-pre.target\n\
65+
[Service]\n\
66+
ExecStart=bootc internals fixup-etc-fstab
67+
",
68+
)?;
69+
let target = "local-fs-pre.target.wants";
70+
unit_dir.create_dir_all(target)?;
71+
unit_dir.symlink(&format!("../{unit}"), &format!("{target}/{unit}"))?;
72+
Ok(())
73+
}
74+
75+
#[cfg(test)]
76+
fn fixture() -> Result<cap_std_ext::cap_tempfile::TempDir> {
77+
let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
78+
tempdir.create_dir("etc")?;
79+
tempdir.create_dir("run")?;
80+
tempdir.create_dir_all("run/systemd/system")?;
81+
Ok(tempdir)
82+
}
83+
84+
#[test]
85+
fn test_generator_no_fstab() -> Result<()> {
86+
let tempdir = fixture()?;
87+
let unit_dir = &tempdir.open_dir("run/systemd/system")?;
88+
fstab_generator_impl(&tempdir, &unit_dir).unwrap();
89+
90+
assert_eq!(unit_dir.entries()?.count(), 0);
91+
Ok(())
92+
}
93+
94+
#[test]
95+
fn test_generator_fstab() -> Result<()> {
96+
let tempdir = fixture()?;
97+
let unit_dir = &tempdir.open_dir("run/systemd/system")?;
98+
// Should still be a no-op
99+
tempdir.atomic_write("etc/fstab", "# Some dummy fstab")?;
100+
fstab_generator_impl(&tempdir, &unit_dir).unwrap();
101+
assert_eq!(unit_dir.entries()?.count(), 0);
102+
103+
// Also a no-op, not booted via ostree
104+
tempdir.atomic_write("etc/fstab", &format!("# {FSTAB_ANACONDA_STAMP}"))?;
105+
fstab_generator_impl(&tempdir, &unit_dir).unwrap();
106+
assert_eq!(unit_dir.entries()?.count(), 0);
107+
108+
// Now it should generate
109+
tempdir.atomic_write("run/ostree-booted", "ostree booted")?;
110+
fstab_generator_impl(&tempdir, &unit_dir).unwrap();
111+
assert_eq!(unit_dir.entries()?.count(), 1);
112+
113+
Ok(())
114+
}

lib/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
pub mod cli;
2121
pub(crate) mod deploy;
22+
pub(crate) mod generator;
2223
pub(crate) mod journal;
2324
mod lsm;
2425
pub(crate) mod metadata;

lib/src/systemglue/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub(crate) mod generator;

0 commit comments

Comments
 (0)