1use anyhow::{Context, Result};
6use clap::ValueEnum;
7use fn_error_context::context;
8use serde::{Deserialize, Serialize};
9
10#[cfg(feature = "install-to-disk")]
11use super::baseline::BlockSetup;
12
13pub(crate) struct EnvProperties {
16 pub(crate) sys_arch: String,
17}
18
19#[derive(clap::ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "kebab-case")]
22pub(crate) enum Filesystem {
23 Xfs,
24 Ext4,
25 Btrfs,
26}
27
28impl std::fmt::Display for Filesystem {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 self.to_possible_value().unwrap().get_name().fmt(f)
31 }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, Default)]
37#[serde(deny_unknown_fields)]
38pub(crate) struct InstallConfigurationToplevel {
39 pub(crate) install: Option<InstallConfiguration>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, Default)]
44#[serde(deny_unknown_fields)]
45pub(crate) struct RootFS {
46 #[serde(rename = "type")]
47 pub(crate) fstype: Option<Filesystem>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, Default)]
53#[serde(deny_unknown_fields)]
54pub(crate) struct BasicFilesystems {
55 pub(crate) root: Option<RootFS>,
56 }
60
61pub(crate) type OstreeRepoOpts = ostree_ext::repo_options::RepoOptions;
63
64#[derive(Debug, Clone, Serialize, Deserialize, Default)]
66#[serde(rename = "install", rename_all = "kebab-case", deny_unknown_fields)]
67pub(crate) struct InstallConfiguration {
68 pub(crate) root_fs_type: Option<Filesystem>,
70 #[cfg(feature = "install-to-disk")]
72 pub(crate) block: Option<Vec<BlockSetup>>,
73 pub(crate) filesystem: Option<BasicFilesystems>,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub(crate) kargs: Option<Vec<String>>,
77 pub(crate) match_architectures: Option<Vec<String>>,
79 pub(crate) ostree: Option<OstreeRepoOpts>,
81 pub(crate) stateroot: Option<String>,
83 pub(crate) root_mount_spec: Option<String>,
86 pub(crate) boot_mount_spec: Option<String>,
88}
89
90fn merge_basic<T>(s: &mut Option<T>, o: Option<T>, _env: &EnvProperties) {
91 if let Some(o) = o {
92 *s = Some(o);
93 }
94}
95
96trait Mergeable {
97 fn merge(&mut self, other: Self, env: &EnvProperties)
98 where
99 Self: Sized;
100}
101
102impl<T> Mergeable for Option<T>
103where
104 T: Mergeable,
105{
106 fn merge(&mut self, other: Self, env: &EnvProperties)
107 where
108 Self: Sized,
109 {
110 if let Some(other) = other {
111 if let Some(s) = self.as_mut() {
112 s.merge(other, env)
113 } else {
114 *self = Some(other);
115 }
116 }
117 }
118}
119
120impl Mergeable for RootFS {
121 fn merge(&mut self, other: Self, env: &EnvProperties) {
123 merge_basic(&mut self.fstype, other.fstype, env)
124 }
125}
126
127impl Mergeable for BasicFilesystems {
128 fn merge(&mut self, other: Self, env: &EnvProperties) {
130 self.root.merge(other.root, env)
131 }
132}
133
134impl Mergeable for OstreeRepoOpts {
135 fn merge(&mut self, other: Self, env: &EnvProperties) {
137 merge_basic(
138 &mut self.bls_append_except_default,
139 other.bls_append_except_default,
140 env,
141 )
142 }
143}
144
145impl Mergeable for InstallConfiguration {
146 fn merge(&mut self, other: Self, env: &EnvProperties) {
148 if other
151 .match_architectures
152 .map(|a| a.contains(&env.sys_arch))
153 .unwrap_or(true)
154 {
155 merge_basic(&mut self.root_fs_type, other.root_fs_type, env);
156 #[cfg(feature = "install-to-disk")]
157 merge_basic(&mut self.block, other.block, env);
158 self.filesystem.merge(other.filesystem, env);
159 self.ostree.merge(other.ostree, env);
160 merge_basic(&mut self.stateroot, other.stateroot, env);
161 merge_basic(&mut self.root_mount_spec, other.root_mount_spec, env);
162 merge_basic(&mut self.boot_mount_spec, other.boot_mount_spec, env);
163 if let Some(other_kargs) = other.kargs {
164 self.kargs
165 .get_or_insert_with(Default::default)
166 .extend(other_kargs)
167 }
168 }
169 }
170}
171
172impl InstallConfiguration {
173 pub(crate) fn canonicalize(&mut self) {
179 if let Some(rootfs_type) = self.filesystem_root().and_then(|f| f.fstype.as_ref()) {
181 self.root_fs_type = Some(*rootfs_type)
182 } else if let Some(rootfs) = self.root_fs_type.as_ref() {
183 let fs = self.filesystem.get_or_insert_with(Default::default);
184 let root = fs.root.get_or_insert_with(Default::default);
185 root.fstype = Some(*rootfs);
186 }
187
188 #[cfg(feature = "install-to-disk")]
189 if self.block.is_none() {
190 self.block = Some(vec![BlockSetup::Direct]);
191 }
192 }
193
194 pub(crate) fn filesystem_root(&self) -> Option<&RootFS> {
196 self.filesystem.as_ref().and_then(|fs| fs.root.as_ref())
197 }
198
199 pub(crate) fn filter_to_external(&mut self) {
201 self.kargs.take();
202 }
203
204 #[cfg(feature = "install-to-disk")]
205 pub(crate) fn get_block_setup(&self, default: Option<BlockSetup>) -> Result<BlockSetup> {
206 let valid_block_setups = self.block.as_deref().unwrap_or_default();
207 let default_block = valid_block_setups.iter().next().ok_or_else(|| {
208 anyhow::anyhow!("Empty block storage configuration in install configuration")
209 })?;
210 let block_setup = default.as_ref().unwrap_or(default_block);
211 if !valid_block_setups.contains(block_setup) {
212 anyhow::bail!("Block setup {block_setup:?} is not enabled in installation config");
213 }
214 Ok(*block_setup)
215 }
216}
217
218#[context("Loading configuration")]
219pub(crate) fn load_config() -> Result<Option<InstallConfiguration>> {
221 let env = EnvProperties {
222 sys_arch: std::env::consts::ARCH.to_string(),
223 };
224 const SYSTEMD_CONVENTIONAL_BASES: &[&str] = &["/usr/lib", "/usr/local/lib", "/etc", "/run"];
225 let fragments = liboverdrop::scan(SYSTEMD_CONVENTIONAL_BASES, "bootc/install", &["toml"], true);
226 let mut config: Option<InstallConfiguration> = None;
227 for (_name, path) in fragments {
228 let buf = std::fs::read_to_string(&path)?;
229 let mut unused = std::collections::HashSet::new();
230 let de = toml::Deserializer::parse(&buf).with_context(|| format!("Parsing {path:?}"))?;
231 let mut c: InstallConfigurationToplevel = serde_ignored::deserialize(de, |path| {
232 unused.insert(path.to_string());
233 })
234 .with_context(|| format!("Parsing {path:?}"))?;
235 for key in unused {
236 eprintln!("warning: {path:?}: Unknown key {key}");
237 }
238 if let Some(config) = config.as_mut() {
239 if let Some(install) = c.install {
240 tracing::debug!("Merging install config: {install:?}");
241 config.merge(install, &env);
242 }
243 } else {
244 if let Some(ref mut install) = c.install {
247 if install
248 .match_architectures
249 .as_ref()
250 .map(|a| a.contains(&env.sys_arch))
251 .unwrap_or(true)
252 {
253 config = c.install;
254 }
255 }
256 }
257 }
258 if let Some(config) = config.as_mut() {
259 config.canonicalize();
260 }
261 Ok(config)
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_parse_config() {
271 let env = EnvProperties {
272 sys_arch: "x86_64".to_string(),
273 };
274 let c: InstallConfigurationToplevel = toml::from_str(
275 r##"[install]
276root-fs-type = "xfs"
277"##,
278 )
279 .unwrap();
280 let mut install = c.install.unwrap();
281 assert_eq!(install.root_fs_type.unwrap(), Filesystem::Xfs);
282 let other = InstallConfigurationToplevel {
283 install: Some(InstallConfiguration {
284 root_fs_type: Some(Filesystem::Ext4),
285 ..Default::default()
286 }),
287 };
288 install.merge(other.install.unwrap(), &env);
289 assert_eq!(
290 install.root_fs_type.as_ref().copied().unwrap(),
291 Filesystem::Ext4
292 );
293 assert!(install.filesystem_root().is_none());
295 install.canonicalize();
296 assert_eq!(install.root_fs_type.as_ref().unwrap(), &Filesystem::Ext4);
297 assert_eq!(
298 install.filesystem_root().unwrap().fstype.unwrap(),
299 Filesystem::Ext4
300 );
301
302 let c: InstallConfigurationToplevel = toml::from_str(
303 r##"[install]
304root-fs-type = "ext4"
305kargs = ["console=ttyS0", "foo=bar"]
306"##,
307 )
308 .unwrap();
309 let mut install = c.install.unwrap();
310 assert_eq!(install.root_fs_type.unwrap(), Filesystem::Ext4);
311 let other = InstallConfigurationToplevel {
312 install: Some(InstallConfiguration {
313 kargs: Some(
314 ["console=tty0", "nosmt"]
315 .into_iter()
316 .map(ToOwned::to_owned)
317 .collect(),
318 ),
319 ..Default::default()
320 }),
321 };
322 install.merge(other.install.unwrap(), &env);
323 assert_eq!(install.root_fs_type.unwrap(), Filesystem::Ext4);
324 assert_eq!(
325 install.kargs,
326 Some(
327 ["console=ttyS0", "foo=bar", "console=tty0", "nosmt"]
328 .into_iter()
329 .map(ToOwned::to_owned)
330 .collect()
331 )
332 )
333 }
334
335 #[test]
336 fn test_parse_filesystems() {
337 let env = EnvProperties {
338 sys_arch: "x86_64".to_string(),
339 };
340 let c: InstallConfigurationToplevel = toml::from_str(
341 r##"[install.filesystem.root]
342type = "xfs"
343"##,
344 )
345 .unwrap();
346 let mut install = c.install.unwrap();
347 assert_eq!(
348 install.filesystem_root().unwrap().fstype.unwrap(),
349 Filesystem::Xfs
350 );
351 let other = InstallConfigurationToplevel {
352 install: Some(InstallConfiguration {
353 filesystem: Some(BasicFilesystems {
354 root: Some(RootFS {
355 fstype: Some(Filesystem::Ext4),
356 }),
357 }),
358 ..Default::default()
359 }),
360 };
361 install.merge(other.install.unwrap(), &env);
362 assert_eq!(
363 install.filesystem_root().unwrap().fstype.unwrap(),
364 Filesystem::Ext4
365 );
366 }
367
368 #[test]
369 fn test_parse_block() {
370 let env = EnvProperties {
371 sys_arch: "x86_64".to_string(),
372 };
373 let c: InstallConfigurationToplevel = toml::from_str(
374 r##"[install.filesystem.root]
375type = "xfs"
376"##,
377 )
378 .unwrap();
379 let mut install = c.install.unwrap();
380 {
382 let mut install = install.clone();
383 install.canonicalize();
384 assert_eq!(install.get_block_setup(None).unwrap(), BlockSetup::Direct);
385 }
386 let other = InstallConfigurationToplevel {
387 install: Some(InstallConfiguration {
388 block: Some(vec![]),
389 ..Default::default()
390 }),
391 };
392 install.merge(other.install.unwrap(), &env);
393 assert_eq!(install.block.as_ref().unwrap().len(), 0);
395 assert!(install.get_block_setup(None).is_err());
396
397 let c: InstallConfigurationToplevel = toml::from_str(
398 r##"[install]
399block = ["tpm2-luks"]"##,
400 )
401 .unwrap();
402 let mut install = c.install.unwrap();
403 install.canonicalize();
404 assert_eq!(install.block.as_ref().unwrap().len(), 1);
405 assert_eq!(install.get_block_setup(None).unwrap(), BlockSetup::Tpm2Luks);
406
407 assert!(install.get_block_setup(Some(BlockSetup::Direct)).is_err());
409 }
410
411 #[test]
412 fn test_arch() {
414 let env = EnvProperties {
416 sys_arch: "x86_64".to_string(),
417 };
418 let c: InstallConfigurationToplevel = toml::from_str(
419 r##"[install]
420root-fs-type = "xfs"
421"##,
422 )
423 .unwrap();
424 let mut install = c.install.unwrap();
425 let other = InstallConfigurationToplevel {
426 install: Some(InstallConfiguration {
427 kargs: Some(
428 ["console=tty0", "nosmt"]
429 .into_iter()
430 .map(ToOwned::to_owned)
431 .collect(),
432 ),
433 ..Default::default()
434 }),
435 };
436 install.merge(other.install.unwrap(), &env);
437 assert_eq!(
438 install.kargs,
439 Some(
440 ["console=tty0", "nosmt"]
441 .into_iter()
442 .map(ToOwned::to_owned)
443 .collect()
444 )
445 );
446 let env = EnvProperties {
447 sys_arch: "aarch64".to_string(),
448 };
449 let c: InstallConfigurationToplevel = toml::from_str(
450 r##"[install]
451root-fs-type = "xfs"
452"##,
453 )
454 .unwrap();
455 let mut install = c.install.unwrap();
456 let other = InstallConfigurationToplevel {
457 install: Some(InstallConfiguration {
458 kargs: Some(
459 ["console=tty0", "nosmt"]
460 .into_iter()
461 .map(ToOwned::to_owned)
462 .collect(),
463 ),
464 ..Default::default()
465 }),
466 };
467 install.merge(other.install.unwrap(), &env);
468 assert_eq!(
469 install.kargs,
470 Some(
471 ["console=tty0", "nosmt"]
472 .into_iter()
473 .map(ToOwned::to_owned)
474 .collect()
475 )
476 );
477
478 let env = EnvProperties {
480 sys_arch: "aarch64".to_string(),
481 };
482 let c: InstallConfigurationToplevel = toml::from_str(
483 r##"[install]
484root-fs-type = "xfs"
485"##,
486 )
487 .unwrap();
488 let mut install = c.install.unwrap();
489 let other = InstallConfigurationToplevel {
490 install: Some(InstallConfiguration {
491 kargs: Some(
492 ["console=ttyS0", "foo=bar"]
493 .into_iter()
494 .map(ToOwned::to_owned)
495 .collect(),
496 ),
497 match_architectures: Some(["x86_64"].into_iter().map(ToOwned::to_owned).collect()),
498 ..Default::default()
499 }),
500 };
501 install.merge(other.install.unwrap(), &env);
502 assert_eq!(install.kargs, None);
503 let other = InstallConfigurationToplevel {
504 install: Some(InstallConfiguration {
505 kargs: Some(
506 ["console=tty0", "nosmt"]
507 .into_iter()
508 .map(ToOwned::to_owned)
509 .collect(),
510 ),
511 match_architectures: Some(["aarch64"].into_iter().map(ToOwned::to_owned).collect()),
512 ..Default::default()
513 }),
514 };
515 install.merge(other.install.unwrap(), &env);
516 assert_eq!(
517 install.kargs,
518 Some(
519 ["console=tty0", "nosmt"]
520 .into_iter()
521 .map(ToOwned::to_owned)
522 .collect()
523 )
524 );
525
526 let env = EnvProperties {
528 sys_arch: "x86_64".to_string(),
529 };
530 let c: InstallConfigurationToplevel = toml::from_str(
531 r##"[install]
532root-fs-type = "xfs"
533"##,
534 )
535 .unwrap();
536 let mut install = c.install.unwrap();
537 let other = InstallConfigurationToplevel {
538 install: Some(InstallConfiguration {
539 kargs: Some(
540 ["console=tty0", "nosmt"]
541 .into_iter()
542 .map(ToOwned::to_owned)
543 .collect(),
544 ),
545 match_architectures: Some(
546 ["x86_64", "aarch64"]
547 .into_iter()
548 .map(ToOwned::to_owned)
549 .collect(),
550 ),
551 ..Default::default()
552 }),
553 };
554 install.merge(other.install.unwrap(), &env);
555 assert_eq!(
556 install.kargs,
557 Some(
558 ["console=tty0", "nosmt"]
559 .into_iter()
560 .map(ToOwned::to_owned)
561 .collect()
562 )
563 );
564 let env = EnvProperties {
565 sys_arch: "aarch64".to_string(),
566 };
567 let c: InstallConfigurationToplevel = toml::from_str(
568 r##"[install]
569root-fs-type = "xfs"
570"##,
571 )
572 .unwrap();
573 let mut install = c.install.unwrap();
574 let other = InstallConfigurationToplevel {
575 install: Some(InstallConfiguration {
576 kargs: Some(
577 ["console=tty0", "nosmt"]
578 .into_iter()
579 .map(ToOwned::to_owned)
580 .collect(),
581 ),
582 match_architectures: Some(
583 ["x86_64", "aarch64"]
584 .into_iter()
585 .map(ToOwned::to_owned)
586 .collect(),
587 ),
588 ..Default::default()
589 }),
590 };
591 install.merge(other.install.unwrap(), &env);
592 assert_eq!(
593 install.kargs,
594 Some(
595 ["console=tty0", "nosmt"]
596 .into_iter()
597 .map(ToOwned::to_owned)
598 .collect()
599 )
600 );
601 }
602
603 #[test]
604 fn test_parse_ostree() {
605 let env = EnvProperties {
606 sys_arch: "x86_64".to_string(),
607 };
608
609 let parse_cases = [
611 ("console=ttyS0", "console=ttyS0"),
612 ("console=ttyS0,115200n8", "console=ttyS0,115200n8"),
613 ("rd.lvm.lv=vg/root", "rd.lvm.lv=vg/root"),
614 ];
615 for (input, expected) in parse_cases {
616 let toml_str = format!(
617 r#"[install.ostree]
618bls-append-except-default = "{input}"
619"#
620 );
621 let c: InstallConfigurationToplevel = toml::from_str(&toml_str).unwrap();
622 assert_eq!(
623 c.install
624 .unwrap()
625 .ostree
626 .unwrap()
627 .bls_append_except_default
628 .unwrap(),
629 expected
630 );
631 }
632
633 let mut install: InstallConfiguration = toml::from_str(
635 r#"[ostree]
636bls-append-except-default = "console=ttyS0"
637"#,
638 )
639 .unwrap();
640 let other = InstallConfiguration {
641 ostree: Some(OstreeRepoOpts {
642 bls_append_except_default: Some("console=tty0".to_string()),
643 ..Default::default()
644 }),
645 ..Default::default()
646 };
647 install.merge(other, &env);
648 assert_eq!(
649 install.ostree.unwrap().bls_append_except_default.unwrap(),
650 "console=tty0"
651 );
652 }
653
654 #[test]
655 fn test_parse_stateroot() {
656 let c: InstallConfigurationToplevel = toml::from_str(
657 r#"[install]
658stateroot = "custom"
659"#,
660 )
661 .unwrap();
662 assert_eq!(c.install.unwrap().stateroot.unwrap(), "custom");
663 }
664
665 #[test]
666 fn test_merge_stateroot() {
667 let env = EnvProperties {
668 sys_arch: "x86_64".to_string(),
669 };
670 let mut install: InstallConfiguration = toml::from_str(
671 r#"stateroot = "original"
672"#,
673 )
674 .unwrap();
675 let other = InstallConfiguration {
676 stateroot: Some("newroot".to_string()),
677 ..Default::default()
678 };
679 install.merge(other, &env);
680 assert_eq!(install.stateroot.unwrap(), "newroot");
681 }
682
683 #[test]
684 fn test_parse_mount_specs() {
685 let c: InstallConfigurationToplevel = toml::from_str(
686 r#"[install]
687root-mount-spec = "LABEL=rootfs"
688boot-mount-spec = "UUID=abcd-1234"
689"#,
690 )
691 .unwrap();
692 let install = c.install.unwrap();
693 assert_eq!(install.root_mount_spec.unwrap(), "LABEL=rootfs");
694 assert_eq!(install.boot_mount_spec.unwrap(), "UUID=abcd-1234");
695 }
696
697 #[test]
698 fn test_merge_mount_specs() {
699 let env = EnvProperties {
700 sys_arch: "x86_64".to_string(),
701 };
702 let mut install: InstallConfiguration = toml::from_str(
703 r#"root-mount-spec = "UUID=old"
704boot-mount-spec = "UUID=oldboot"
705"#,
706 )
707 .unwrap();
708 let other = InstallConfiguration {
709 root_mount_spec: Some("LABEL=newroot".to_string()),
710 ..Default::default()
711 };
712 install.merge(other, &env);
713 assert_eq!(install.root_mount_spec.as_deref().unwrap(), "LABEL=newroot");
715 assert_eq!(install.boot_mount_spec.as_deref().unwrap(), "UUID=oldboot");
717 }
718
719 #[test]
722 fn test_parse_empty_mount_specs() {
723 let c: InstallConfigurationToplevel = toml::from_str(
724 r#"[install]
725root-mount-spec = ""
726boot-mount-spec = ""
727"#,
728 )
729 .unwrap();
730 let install = c.install.unwrap();
731 assert_eq!(install.root_mount_spec.as_deref().unwrap(), "");
732 assert_eq!(install.boot_mount_spec.as_deref().unwrap(), "");
733 }
734}