Skip to content

Commit 498fd84

Browse files
authored
Merge pull request #67 from cgwalters/add-config-files
install: Add systemd-style drop-in config files
2 parents da3a533 + 5c18ade commit 498fd84

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)