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_all = "kebab-case", deny_unknown_fields)]
67pub(crate) struct Bootupd {
68 pub(crate) skip_boot_uuid: Option<bool>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, Default)]
76#[serde(rename = "install", rename_all = "kebab-case", deny_unknown_fields)]
77pub(crate) struct InstallConfiguration {
78 pub(crate) root_fs_type: Option<Filesystem>,
80 #[cfg(feature = "install-to-disk")]
82 pub(crate) block: Option<Vec<BlockSetup>>,
83 pub(crate) filesystem: Option<BasicFilesystems>,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub(crate) kargs: Option<Vec<String>>,
87 pub(crate) match_architectures: Option<Vec<String>>,
89 pub(crate) ostree: Option<OstreeRepoOpts>,
91 pub(crate) stateroot: Option<String>,
93 pub(crate) root_mount_spec: Option<String>,
96 pub(crate) boot_mount_spec: Option<String>,
98 pub(crate) bootupd: Option<Bootupd>,
100}
101
102fn merge_basic<T>(s: &mut Option<T>, o: Option<T>, _env: &EnvProperties) {
103 if let Some(o) = o {
104 *s = Some(o);
105 }
106}
107
108trait Mergeable {
109 fn merge(&mut self, other: Self, env: &EnvProperties)
110 where
111 Self: Sized;
112}
113
114impl<T> Mergeable for Option<T>
115where
116 T: Mergeable,
117{
118 fn merge(&mut self, other: Self, env: &EnvProperties)
119 where
120 Self: Sized,
121 {
122 if let Some(other) = other {
123 if let Some(s) = self.as_mut() {
124 s.merge(other, env)
125 } else {
126 *self = Some(other);
127 }
128 }
129 }
130}
131
132impl Mergeable for RootFS {
133 fn merge(&mut self, other: Self, env: &EnvProperties) {
135 merge_basic(&mut self.fstype, other.fstype, env)
136 }
137}
138
139impl Mergeable for BasicFilesystems {
140 fn merge(&mut self, other: Self, env: &EnvProperties) {
142 self.root.merge(other.root, env)
143 }
144}
145
146impl Mergeable for OstreeRepoOpts {
147 fn merge(&mut self, other: Self, env: &EnvProperties) {
149 merge_basic(
150 &mut self.bls_append_except_default,
151 other.bls_append_except_default,
152 env,
153 )
154 }
155}
156
157impl Mergeable for Bootupd {
158 fn merge(&mut self, other: Self, env: &EnvProperties) {
160 merge_basic(&mut self.skip_boot_uuid, other.skip_boot_uuid, env)
161 }
162}
163
164impl Mergeable for InstallConfiguration {
165 fn merge(&mut self, other: Self, env: &EnvProperties) {
167 if other
170 .match_architectures
171 .map(|a| a.contains(&env.sys_arch))
172 .unwrap_or(true)
173 {
174 merge_basic(&mut self.root_fs_type, other.root_fs_type, env);
175 #[cfg(feature = "install-to-disk")]
176 merge_basic(&mut self.block, other.block, env);
177 self.filesystem.merge(other.filesystem, env);
178 self.ostree.merge(other.ostree, env);
179 merge_basic(&mut self.stateroot, other.stateroot, env);
180 merge_basic(&mut self.root_mount_spec, other.root_mount_spec, env);
181 merge_basic(&mut self.boot_mount_spec, other.boot_mount_spec, env);
182 self.bootupd.merge(other.bootupd, env);
183 if let Some(other_kargs) = other.kargs {
184 self.kargs
185 .get_or_insert_with(Default::default)
186 .extend(other_kargs)
187 }
188 }
189 }
190}
191
192impl InstallConfiguration {
193 pub(crate) fn canonicalize(&mut self) {
199 if let Some(rootfs_type) = self.filesystem_root().and_then(|f| f.fstype.as_ref()) {
201 self.root_fs_type = Some(*rootfs_type)
202 } else if let Some(rootfs) = self.root_fs_type.as_ref() {
203 let fs = self.filesystem.get_or_insert_with(Default::default);
204 let root = fs.root.get_or_insert_with(Default::default);
205 root.fstype = Some(*rootfs);
206 }
207
208 #[cfg(feature = "install-to-disk")]
209 if self.block.is_none() {
210 self.block = Some(vec![BlockSetup::Direct]);
211 }
212 }
213
214 pub(crate) fn filesystem_root(&self) -> Option<&RootFS> {
216 self.filesystem.as_ref().and_then(|fs| fs.root.as_ref())
217 }
218
219 pub(crate) fn filter_to_external(&mut self) {
221 self.kargs.take();
222 }
223
224 #[cfg(feature = "install-to-disk")]
225 pub(crate) fn get_block_setup(&self, default: Option<BlockSetup>) -> Result<BlockSetup> {
226 let valid_block_setups = self.block.as_deref().unwrap_or_default();
227 let default_block = valid_block_setups.iter().next().ok_or_else(|| {
228 anyhow::anyhow!("Empty block storage configuration in install configuration")
229 })?;
230 let block_setup = default.as_ref().unwrap_or(default_block);
231 if !valid_block_setups.contains(block_setup) {
232 anyhow::bail!("Block setup {block_setup:?} is not enabled in installation config");
233 }
234 Ok(*block_setup)
235 }
236}
237
238#[context("Loading configuration")]
239pub(crate) fn load_config() -> Result<Option<InstallConfiguration>> {
241 let env = EnvProperties {
242 sys_arch: std::env::consts::ARCH.to_string(),
243 };
244 const SYSTEMD_CONVENTIONAL_BASES: &[&str] = &["/usr/lib", "/usr/local/lib", "/etc", "/run"];
245 let fragments = liboverdrop::scan(SYSTEMD_CONVENTIONAL_BASES, "bootc/install", &["toml"], true);
246 let mut config: Option<InstallConfiguration> = None;
247 for (_name, path) in fragments {
248 let buf = std::fs::read_to_string(&path)?;
249 let mut unused = std::collections::HashSet::new();
250 let de = toml::Deserializer::parse(&buf).with_context(|| format!("Parsing {path:?}"))?;
251 let mut c: InstallConfigurationToplevel = serde_ignored::deserialize(de, |path| {
252 unused.insert(path.to_string());
253 })
254 .with_context(|| format!("Parsing {path:?}"))?;
255 for key in unused {
256 eprintln!("warning: {path:?}: Unknown key {key}");
257 }
258 if let Some(config) = config.as_mut() {
259 if let Some(install) = c.install {
260 tracing::debug!("Merging install config: {install:?}");
261 config.merge(install, &env);
262 }
263 } else {
264 if let Some(ref mut install) = c.install {
267 if install
268 .match_architectures
269 .as_ref()
270 .map(|a| a.contains(&env.sys_arch))
271 .unwrap_or(true)
272 {
273 config = c.install;
274 }
275 }
276 }
277 }
278 if let Some(config) = config.as_mut() {
279 config.canonicalize();
280 }
281 Ok(config)
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_parse_config() {
291 let env = EnvProperties {
292 sys_arch: "x86_64".to_string(),
293 };
294 let c: InstallConfigurationToplevel = toml::from_str(
295 r##"[install]
296root-fs-type = "xfs"
297"##,
298 )
299 .unwrap();
300 let mut install = c.install.unwrap();
301 assert_eq!(install.root_fs_type.unwrap(), Filesystem::Xfs);
302 let other = InstallConfigurationToplevel {
303 install: Some(InstallConfiguration {
304 root_fs_type: Some(Filesystem::Ext4),
305 ..Default::default()
306 }),
307 };
308 install.merge(other.install.unwrap(), &env);
309 assert_eq!(
310 install.root_fs_type.as_ref().copied().unwrap(),
311 Filesystem::Ext4
312 );
313 assert!(install.filesystem_root().is_none());
315 install.canonicalize();
316 assert_eq!(install.root_fs_type.as_ref().unwrap(), &Filesystem::Ext4);
317 assert_eq!(
318 install.filesystem_root().unwrap().fstype.unwrap(),
319 Filesystem::Ext4
320 );
321
322 let c: InstallConfigurationToplevel = toml::from_str(
323 r##"[install]
324root-fs-type = "ext4"
325kargs = ["console=ttyS0", "foo=bar"]
326"##,
327 )
328 .unwrap();
329 let mut install = c.install.unwrap();
330 assert_eq!(install.root_fs_type.unwrap(), Filesystem::Ext4);
331 let other = InstallConfigurationToplevel {
332 install: Some(InstallConfiguration {
333 kargs: Some(
334 ["console=tty0", "nosmt"]
335 .into_iter()
336 .map(ToOwned::to_owned)
337 .collect(),
338 ),
339 ..Default::default()
340 }),
341 };
342 install.merge(other.install.unwrap(), &env);
343 assert_eq!(install.root_fs_type.unwrap(), Filesystem::Ext4);
344 assert_eq!(
345 install.kargs,
346 Some(
347 ["console=ttyS0", "foo=bar", "console=tty0", "nosmt"]
348 .into_iter()
349 .map(ToOwned::to_owned)
350 .collect()
351 )
352 )
353 }
354
355 #[test]
356 fn test_parse_filesystems() {
357 let env = EnvProperties {
358 sys_arch: "x86_64".to_string(),
359 };
360 let c: InstallConfigurationToplevel = toml::from_str(
361 r##"[install.filesystem.root]
362type = "xfs"
363"##,
364 )
365 .unwrap();
366 let mut install = c.install.unwrap();
367 assert_eq!(
368 install.filesystem_root().unwrap().fstype.unwrap(),
369 Filesystem::Xfs
370 );
371 let other = InstallConfigurationToplevel {
372 install: Some(InstallConfiguration {
373 filesystem: Some(BasicFilesystems {
374 root: Some(RootFS {
375 fstype: Some(Filesystem::Ext4),
376 }),
377 }),
378 ..Default::default()
379 }),
380 };
381 install.merge(other.install.unwrap(), &env);
382 assert_eq!(
383 install.filesystem_root().unwrap().fstype.unwrap(),
384 Filesystem::Ext4
385 );
386 }
387
388 #[test]
389 fn test_parse_block() {
390 let env = EnvProperties {
391 sys_arch: "x86_64".to_string(),
392 };
393 let c: InstallConfigurationToplevel = toml::from_str(
394 r##"[install.filesystem.root]
395type = "xfs"
396"##,
397 )
398 .unwrap();
399 let mut install = c.install.unwrap();
400 {
402 let mut install = install.clone();
403 install.canonicalize();
404 assert_eq!(install.get_block_setup(None).unwrap(), BlockSetup::Direct);
405 }
406 let other = InstallConfigurationToplevel {
407 install: Some(InstallConfiguration {
408 block: Some(vec![]),
409 ..Default::default()
410 }),
411 };
412 install.merge(other.install.unwrap(), &env);
413 assert_eq!(install.block.as_ref().unwrap().len(), 0);
415 assert!(install.get_block_setup(None).is_err());
416
417 let c: InstallConfigurationToplevel = toml::from_str(
418 r##"[install]
419block = ["tpm2-luks"]"##,
420 )
421 .unwrap();
422 let mut install = c.install.unwrap();
423 install.canonicalize();
424 assert_eq!(install.block.as_ref().unwrap().len(), 1);
425 assert_eq!(install.get_block_setup(None).unwrap(), BlockSetup::Tpm2Luks);
426
427 assert!(install.get_block_setup(Some(BlockSetup::Direct)).is_err());
429 }
430
431 #[test]
432 fn test_arch() {
434 let env = EnvProperties {
436 sys_arch: "x86_64".to_string(),
437 };
438 let c: InstallConfigurationToplevel = toml::from_str(
439 r##"[install]
440root-fs-type = "xfs"
441"##,
442 )
443 .unwrap();
444 let mut install = c.install.unwrap();
445 let other = InstallConfigurationToplevel {
446 install: Some(InstallConfiguration {
447 kargs: Some(
448 ["console=tty0", "nosmt"]
449 .into_iter()
450 .map(ToOwned::to_owned)
451 .collect(),
452 ),
453 ..Default::default()
454 }),
455 };
456 install.merge(other.install.unwrap(), &env);
457 assert_eq!(
458 install.kargs,
459 Some(
460 ["console=tty0", "nosmt"]
461 .into_iter()
462 .map(ToOwned::to_owned)
463 .collect()
464 )
465 );
466 let env = EnvProperties {
467 sys_arch: "aarch64".to_string(),
468 };
469 let c: InstallConfigurationToplevel = toml::from_str(
470 r##"[install]
471root-fs-type = "xfs"
472"##,
473 )
474 .unwrap();
475 let mut install = c.install.unwrap();
476 let other = InstallConfigurationToplevel {
477 install: Some(InstallConfiguration {
478 kargs: Some(
479 ["console=tty0", "nosmt"]
480 .into_iter()
481 .map(ToOwned::to_owned)
482 .collect(),
483 ),
484 ..Default::default()
485 }),
486 };
487 install.merge(other.install.unwrap(), &env);
488 assert_eq!(
489 install.kargs,
490 Some(
491 ["console=tty0", "nosmt"]
492 .into_iter()
493 .map(ToOwned::to_owned)
494 .collect()
495 )
496 );
497
498 let env = EnvProperties {
500 sys_arch: "aarch64".to_string(),
501 };
502 let c: InstallConfigurationToplevel = toml::from_str(
503 r##"[install]
504root-fs-type = "xfs"
505"##,
506 )
507 .unwrap();
508 let mut install = c.install.unwrap();
509 let other = InstallConfigurationToplevel {
510 install: Some(InstallConfiguration {
511 kargs: Some(
512 ["console=ttyS0", "foo=bar"]
513 .into_iter()
514 .map(ToOwned::to_owned)
515 .collect(),
516 ),
517 match_architectures: Some(["x86_64"].into_iter().map(ToOwned::to_owned).collect()),
518 ..Default::default()
519 }),
520 };
521 install.merge(other.install.unwrap(), &env);
522 assert_eq!(install.kargs, None);
523 let other = InstallConfigurationToplevel {
524 install: Some(InstallConfiguration {
525 kargs: Some(
526 ["console=tty0", "nosmt"]
527 .into_iter()
528 .map(ToOwned::to_owned)
529 .collect(),
530 ),
531 match_architectures: Some(["aarch64"].into_iter().map(ToOwned::to_owned).collect()),
532 ..Default::default()
533 }),
534 };
535 install.merge(other.install.unwrap(), &env);
536 assert_eq!(
537 install.kargs,
538 Some(
539 ["console=tty0", "nosmt"]
540 .into_iter()
541 .map(ToOwned::to_owned)
542 .collect()
543 )
544 );
545
546 let env = EnvProperties {
548 sys_arch: "x86_64".to_string(),
549 };
550 let c: InstallConfigurationToplevel = toml::from_str(
551 r##"[install]
552root-fs-type = "xfs"
553"##,
554 )
555 .unwrap();
556 let mut install = c.install.unwrap();
557 let other = InstallConfigurationToplevel {
558 install: Some(InstallConfiguration {
559 kargs: Some(
560 ["console=tty0", "nosmt"]
561 .into_iter()
562 .map(ToOwned::to_owned)
563 .collect(),
564 ),
565 match_architectures: Some(
566 ["x86_64", "aarch64"]
567 .into_iter()
568 .map(ToOwned::to_owned)
569 .collect(),
570 ),
571 ..Default::default()
572 }),
573 };
574 install.merge(other.install.unwrap(), &env);
575 assert_eq!(
576 install.kargs,
577 Some(
578 ["console=tty0", "nosmt"]
579 .into_iter()
580 .map(ToOwned::to_owned)
581 .collect()
582 )
583 );
584 let env = EnvProperties {
585 sys_arch: "aarch64".to_string(),
586 };
587 let c: InstallConfigurationToplevel = toml::from_str(
588 r##"[install]
589root-fs-type = "xfs"
590"##,
591 )
592 .unwrap();
593 let mut install = c.install.unwrap();
594 let other = InstallConfigurationToplevel {
595 install: Some(InstallConfiguration {
596 kargs: Some(
597 ["console=tty0", "nosmt"]
598 .into_iter()
599 .map(ToOwned::to_owned)
600 .collect(),
601 ),
602 match_architectures: Some(
603 ["x86_64", "aarch64"]
604 .into_iter()
605 .map(ToOwned::to_owned)
606 .collect(),
607 ),
608 ..Default::default()
609 }),
610 };
611 install.merge(other.install.unwrap(), &env);
612 assert_eq!(
613 install.kargs,
614 Some(
615 ["console=tty0", "nosmt"]
616 .into_iter()
617 .map(ToOwned::to_owned)
618 .collect()
619 )
620 );
621 }
622
623 #[test]
624 fn test_parse_ostree() {
625 let env = EnvProperties {
626 sys_arch: "x86_64".to_string(),
627 };
628
629 let parse_cases = [
631 ("console=ttyS0", "console=ttyS0"),
632 ("console=ttyS0,115200n8", "console=ttyS0,115200n8"),
633 ("rd.lvm.lv=vg/root", "rd.lvm.lv=vg/root"),
634 ];
635 for (input, expected) in parse_cases {
636 let toml_str = format!(
637 r#"[install.ostree]
638bls-append-except-default = "{input}"
639"#
640 );
641 let c: InstallConfigurationToplevel = toml::from_str(&toml_str).unwrap();
642 assert_eq!(
643 c.install
644 .unwrap()
645 .ostree
646 .unwrap()
647 .bls_append_except_default
648 .unwrap(),
649 expected
650 );
651 }
652
653 let mut install: InstallConfiguration = toml::from_str(
655 r#"[ostree]
656bls-append-except-default = "console=ttyS0"
657"#,
658 )
659 .unwrap();
660 let other = InstallConfiguration {
661 ostree: Some(OstreeRepoOpts {
662 bls_append_except_default: Some("console=tty0".to_string()),
663 ..Default::default()
664 }),
665 ..Default::default()
666 };
667 install.merge(other, &env);
668 assert_eq!(
669 install.ostree.unwrap().bls_append_except_default.unwrap(),
670 "console=tty0"
671 );
672 }
673
674 #[test]
675 fn test_parse_stateroot() {
676 let c: InstallConfigurationToplevel = toml::from_str(
677 r#"[install]
678stateroot = "custom"
679"#,
680 )
681 .unwrap();
682 assert_eq!(c.install.unwrap().stateroot.unwrap(), "custom");
683 }
684
685 #[test]
686 fn test_merge_stateroot() {
687 let env = EnvProperties {
688 sys_arch: "x86_64".to_string(),
689 };
690 let mut install: InstallConfiguration = toml::from_str(
691 r#"stateroot = "original"
692"#,
693 )
694 .unwrap();
695 let other = InstallConfiguration {
696 stateroot: Some("newroot".to_string()),
697 ..Default::default()
698 };
699 install.merge(other, &env);
700 assert_eq!(install.stateroot.unwrap(), "newroot");
701 }
702
703 #[test]
704 fn test_parse_mount_specs() {
705 let c: InstallConfigurationToplevel = toml::from_str(
706 r#"[install]
707root-mount-spec = "LABEL=rootfs"
708boot-mount-spec = "UUID=abcd-1234"
709"#,
710 )
711 .unwrap();
712 let install = c.install.unwrap();
713 assert_eq!(install.root_mount_spec.unwrap(), "LABEL=rootfs");
714 assert_eq!(install.boot_mount_spec.unwrap(), "UUID=abcd-1234");
715 }
716
717 #[test]
718 fn test_merge_mount_specs() {
719 let env = EnvProperties {
720 sys_arch: "x86_64".to_string(),
721 };
722 let mut install: InstallConfiguration = toml::from_str(
723 r#"root-mount-spec = "UUID=old"
724boot-mount-spec = "UUID=oldboot"
725"#,
726 )
727 .unwrap();
728 let other = InstallConfiguration {
729 root_mount_spec: Some("LABEL=newroot".to_string()),
730 ..Default::default()
731 };
732 install.merge(other, &env);
733 assert_eq!(install.root_mount_spec.as_deref().unwrap(), "LABEL=newroot");
735 assert_eq!(install.boot_mount_spec.as_deref().unwrap(), "UUID=oldboot");
737 }
738
739 #[test]
742 fn test_parse_empty_mount_specs() {
743 let c: InstallConfigurationToplevel = toml::from_str(
744 r#"[install]
745root-mount-spec = ""
746boot-mount-spec = ""
747"#,
748 )
749 .unwrap();
750 let install = c.install.unwrap();
751 assert_eq!(install.root_mount_spec.as_deref().unwrap(), "");
752 assert_eq!(install.boot_mount_spec.as_deref().unwrap(), "");
753 }
754
755 #[test]
756 fn test_parse_bootupd_skip_boot_uuid() {
757 let c: InstallConfigurationToplevel = toml::from_str(
759 r#"[install.bootupd]
760skip-boot-uuid = true
761"#,
762 )
763 .unwrap();
764 assert_eq!(
765 c.install.unwrap().bootupd.unwrap().skip_boot_uuid.unwrap(),
766 true
767 );
768
769 let c: InstallConfigurationToplevel = toml::from_str(
771 r#"[install.bootupd]
772skip-boot-uuid = false
773"#,
774 )
775 .unwrap();
776 assert_eq!(
777 c.install.unwrap().bootupd.unwrap().skip_boot_uuid.unwrap(),
778 false
779 );
780
781 let c: InstallConfigurationToplevel = toml::from_str(
783 r#"[install]
784root-fs-type = "xfs"
785"#,
786 )
787 .unwrap();
788 assert!(c.install.unwrap().bootupd.is_none());
789 }
790
791 #[test]
792 fn test_merge_bootupd_skip_boot_uuid() {
793 let env = EnvProperties {
794 sys_arch: "x86_64".to_string(),
795 };
796 let mut install: InstallConfiguration = toml::from_str(
797 r#"[bootupd]
798skip-boot-uuid = false
799"#,
800 )
801 .unwrap();
802 let other = InstallConfiguration {
803 bootupd: Some(Bootupd {
804 skip_boot_uuid: Some(true),
805 }),
806 ..Default::default()
807 };
808 install.merge(other, &env);
809 assert_eq!(install.bootupd.unwrap().skip_boot_uuid.unwrap(), true);
811 }
812}