Skip to content

Commit 5c18ade

Browse files
committed
install: Add systemd-style drop-in config files
Add support for `/usr/lib/bootc/install/*.toml` and `/etc/bootc/install/*.toml` and `/run/bootc/install/*.toml` etc. which define config files for the install process. Use this to configure the root filesystem type instead of hardcoding it. We still default to xfs but it's now trivial for an OS/distribution to drop-in an override (or just edit the default toml). Additionally derived builds can do so just as easily. We then drop the `impl Default for Filesystem` so that the default isn't hardcoded in the binary anymore. Signed-off-by: Colin Walters <[email protected]>
1 parent da3a533 commit 5c18ade

File tree

7 files changed

+98
-8
lines changed

7 files changed

+98
-8
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ all-test:
88

99
install:
1010
install -D -t $(DESTDIR)$(prefix)/bin target/release/bootc
11+
install -D -t $(DESTDIR)$(prefix)/lib/bootc/install lib/src/install/*.toml
1112

1213
bin-archive: all
1314
$(MAKE) install DESTDIR=tmp-install && tar --zstd -C tmp-install -cf bootc.tar.zst . && rm tmp-install -rf

lib/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ fn-error-context = "0.2.0"
2020
gvariant = "0.4.0"
2121
indicatif = "0.17.0"
2222
libc = "^0.2"
23+
liboverdrop = "0.1.0"
2324
once_cell = "1.9"
2425
openssl = "^0.10"
2526
nix = ">= 0.24, < 0.26"
@@ -32,6 +33,7 @@ tokio = { features = ["io-std", "time", "process", "rt", "net"], version = ">= 1
3233
tokio-util = { features = ["io-util"], version = "0.7" }
3334
tracing = "0.1"
3435
tempfile = "3.3.0"
36+
toml = "0.7.2"
3537
xshell = { version = "0.2", optional = true }
3638
uuid = { version = "1.2.2", features = ["v4"] }
3739

lib/src/install.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ pub(crate) struct State {
168168
override_disable_selinux: bool,
169169
config_opts: InstallConfigOpts,
170170
target_opts: InstallTargetOpts,
171+
install_config: config::InstallConfiguration,
171172
}
172173

173174
/// Path to initially deployed version information
@@ -260,6 +261,76 @@ impl FromStr for MountSpec {
260261
}
261262
}
262263

264+
mod config {
265+
use super::*;
266+
267+
/// The toplevel config entry for installation configs stored
268+
/// in bootc/install (e.g. /etc/bootc/install/05-custom.toml)
269+
#[derive(Debug, Deserialize, Default)]
270+
#[serde(deny_unknown_fields)]
271+
pub(crate) struct InstallConfigurationToplevel {
272+
pub(crate) install: Option<InstallConfiguration>,
273+
}
274+
275+
/// The serialized [install] section
276+
#[derive(Debug, Deserialize, Default)]
277+
#[serde(rename = "install", rename_all = "kebab-case", deny_unknown_fields)]
278+
pub(crate) struct InstallConfiguration {
279+
pub(crate) root_fs_type: Option<super::baseline::Filesystem>,
280+
}
281+
282+
impl InstallConfiguration {
283+
/// Apply any values in other, overriding any existing values in `self`.
284+
fn merge(&mut self, other: Self) {
285+
fn mergeopt<T>(s: &mut Option<T>, o: Option<T>) {
286+
if let Some(o) = o {
287+
*s = Some(o);
288+
}
289+
}
290+
mergeopt(&mut self.root_fs_type, other.root_fs_type)
291+
}
292+
}
293+
294+
#[context("Loading configuration")]
295+
/// Load the install configuration, merging all found configuration files.
296+
pub(crate) fn load_config() -> Result<InstallConfiguration> {
297+
const SYSTEMD_CONVENTIONAL_BASES: &[&str] = &["/usr/lib", "/usr/local/lib", "/etc", "/run"];
298+
let fragments =
299+
liboverdrop::scan(SYSTEMD_CONVENTIONAL_BASES, "bootc/install", &["toml"], true);
300+
let mut config: Option<InstallConfiguration> = None;
301+
for (_name, path) in fragments {
302+
let buf = std::fs::read_to_string(&path)?;
303+
let c: InstallConfigurationToplevel =
304+
toml::from_str(&buf).with_context(|| format!("Parsing {path:?}"))?;
305+
if let Some(config) = config.as_mut() {
306+
if let Some(install) = c.install {
307+
config.merge(install);
308+
}
309+
} else {
310+
config = c.install;
311+
}
312+
}
313+
config.ok_or_else(|| anyhow::anyhow!("Failed to find any installation config files"))
314+
}
315+
316+
#[test]
317+
/// Verify that we can parse our default config file
318+
fn test_parse_config() {
319+
use super::baseline::Filesystem;
320+
let buf = include_str!("install/00-defaults.toml");
321+
let c: InstallConfigurationToplevel = toml::from_str(buf).unwrap();
322+
let mut install = c.install.unwrap();
323+
assert_eq!(install.root_fs_type.unwrap(), Filesystem::Xfs);
324+
let other = InstallConfigurationToplevel {
325+
install: Some(InstallConfiguration {
326+
root_fs_type: Some(Filesystem::Ext4),
327+
}),
328+
};
329+
install.merge(other.install.unwrap());
330+
assert_eq!(install.root_fs_type.unwrap(), Filesystem::Ext4);
331+
}
332+
}
333+
263334
fn bind_mount_from_host(src: impl AsRef<Utf8Path>, dest: impl AsRef<Utf8Path>) -> Result<()> {
264335
let src = src.as_ref();
265336
let dest = dest.as_ref();
@@ -626,6 +697,8 @@ async fn prepare_install(
626697
let override_disable_selinux =
627698
reexecute_self_for_selinux_if_needed(&srcdata, config_opts.disable_selinux)?;
628699

700+
let install_config = config::load_config()?;
701+
629702
// Create our global (read-only) state which gets wrapped in an Arc
630703
// so we can pass it to worker threads too. Right now this just
631704
// combines our command line options along with some bind mounts from the host.
@@ -637,6 +710,7 @@ async fn prepare_install(
637710
source_digest,
638711
config_opts,
639712
target_opts,
713+
install_config,
640714
});
641715

642716
Ok(state)
@@ -709,7 +783,9 @@ pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
709783

710784
// This is all blocking stuff
711785
let mut rootfs = {
712-
tokio::task::spawn_blocking(move || baseline::install_create_rootfs(block_opts)).await??
786+
let state = state.clone();
787+
tokio::task::spawn_blocking(move || baseline::install_create_rootfs(&state, block_opts))
788+
.await??
713789
};
714790

715791
install_to_filesystem_impl(&state, &mut rootfs).await?;

lib/src/install/00-defaults.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# The default configuration for installations.
2+
[install]
3+
root-fs-type = "xfs"

lib/src/install/baseline.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use serde::{Deserialize, Serialize};
2222

2323
use super::MountSpec;
2424
use super::RootSetup;
25+
use super::State;
2526
use super::RUN_BOOTC;
2627
use super::RW_KARG;
2728
use crate::lsm::lsm_label;
@@ -44,6 +45,7 @@ pub(crate) const PREPPN: u32 = 1;
4445
pub(crate) const RESERVEDPN: u32 = 1;
4546

4647
#[derive(clap::ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
48+
#[serde(rename_all = "kebab-case")]
4749
pub(crate) enum Filesystem {
4850
Xfs,
4951
Ext4,
@@ -97,9 +99,8 @@ pub(crate) struct InstallBlockDeviceOpts {
9799
pub(crate) block_setup: BlockSetup,
98100

99101
/// Target root filesystem type.
100-
#[clap(long, value_enum, default_value_t)]
101-
#[serde(default)]
102-
pub(crate) filesystem: Filesystem,
102+
#[clap(long, value_enum)]
103+
pub(crate) filesystem: Option<Filesystem>,
103104

104105
/// Size of the root partition (default specifier: M). Allowed specifiers: M (mebibytes), G (gibibytes), T (tebibytes).
105106
///
@@ -156,7 +157,10 @@ fn mkfs<'a>(
156157
}
157158

158159
#[context("Creating rootfs")]
159-
pub(crate) fn install_create_rootfs(opts: InstallBlockDeviceOpts) -> Result<RootSetup> {
160+
pub(crate) fn install_create_rootfs(
161+
state: &State,
162+
opts: InstallBlockDeviceOpts,
163+
) -> Result<RootSetup> {
160164
// Verify that the target is empty (if not already wiped in particular, but it's
161165
// also good to verify that the wipe worked)
162166
let device = crate::blockdev::list_dev(&opts.device)?;
@@ -302,7 +306,11 @@ pub(crate) fn install_create_rootfs(opts: InstallBlockDeviceOpts) -> Result<Root
302306

303307
// Initialize rootfs
304308
let rootdev = &format!("{device}{ROOTPN}");
305-
let root_uuid = mkfs(rootdev, opts.filesystem, Some("root"), [])?;
309+
let root_filesystem = opts
310+
.filesystem
311+
.or(state.install_config.root_fs_type)
312+
.ok_or_else(|| anyhow::anyhow!("No root filesystem specified"))?;
313+
let root_uuid = mkfs(rootdev, root_filesystem, Some("root"), [])?;
306314
let rootarg = format!("root=UUID={root_uuid}");
307315
let bootsrc = format!("UUID={boot_uuid}");
308316
let bootarg = format!("boot={bootsrc}");

lib/src/privtests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ fn test_install_filesystem(image: &str, blockdev: &Utf8Path) -> Result<()> {
151151
let mountpoint: &Utf8Path = mountpoint_dir.path().try_into().unwrap();
152152

153153
// And run the install
154-
cmd!(sh, "podman run --rm --privileged --pid=host --net=none --env=RUST_LOG -v /usr/bin/bootc:/usr/bin/bootc -v {mountpoint}:/target-root {image} bootc install-to-filesystem /target-root").run()?;
154+
cmd!(sh, "podman run --rm --privileged --pid=host --net=none --env=RUST_LOG -v /usr/bin/bootc:/usr/bin/bootc -v /usr/lib/bootc:/usr/lib/bootc -v {mountpoint}:/target-root {image} bootc install-to-filesystem /target-root").run()?;
155155

156156
cmd!(sh, "umount -R {mountpoint}").run()?;
157157

tests/kolainst/install

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ cd $(mktemp -d)
2020

2121
case "${AUTOPKGTEST_REBOOT_MARK:-}" in
2222
"")
23-
podman run --rm -ti --privileged --pid=host --net=none -v /usr/bin/bootc:/usr/bin/bootc ${IMAGE} bootc install --karg=foo=bar ${DEV}
23+
podman run --rm -ti --privileged --pid=host --net=none -v /usr/bin/bootc:/usr/bin/bootc -v /usr/lib/bootc:/usr/lib/bootc ${IMAGE} bootc install --karg=foo=bar ${DEV}
2424
# In theory we could e.g. wipe the bootloader setup on the primary disk, then reboot;
2525
# but for now let's just sanity test that the install command executes.
2626
lsblk ${DEV}

0 commit comments

Comments
 (0)