1use std::io::BufRead;
2
3use anyhow::{Context, Result};
4use camino::{Utf8Path, Utf8PathBuf};
5use cap_std::fs::Dir;
6use cap_std_ext::{cap_std, dirext::CapStdExtDirExt};
7use fn_error_context::context;
8use ostree_ext::container_utils::{OSTREE_BOOTED, is_ostree_booted_in};
9use ostree_ext::{gio, ostree};
10use rustix::{fd::AsFd, fs::StatVfsMountFlags};
11
12use crate::bootc_composefs::{boot::BootType, state::read_boot_type, status::ComposefsCmdline};
13use crate::composefs_consts::ABOOT_RECONCILE_SERVICE;
14use crate::install::DESTRUCTIVE_CLEANUP;
15use linux_kernel_cmdline::utf8::Cmdline;
16
17const STATUS_ONBOOT_UNIT: &str = "bootc-status-updated-onboot.target";
18const STATUS_PATH_UNIT: &str = "bootc-status-updated.path";
19const CLEANUP_UNIT: &str = "bootc-destructive-cleanup.service";
20const MULTI_USER_TARGET: &str = "multi-user.target";
21const EDIT_UNIT: &str = "bootc-fstab-edit.service";
22const FSTAB_ANACONDA_STAMP: &str = "Created by anaconda";
23pub(crate) const BOOTC_EDITED_STAMP: &str = "Updated by bootc-fstab-edit.service";
24const TRANSIENT_RELABEL_UNIT: &str = "bootc-early-overlay-relabel.service";
25const SYSINIT_TARGET: &str = "sysinit.target";
26const SHADOW_SYNC_UNIT: &str = "bootc-sysusers-shadow-sync.service";
27const EARLY_TMPFILES_UNIT: &str = "bootc-early-tmpfiles.service";
28
29#[context("bootc generator")]
31pub(crate) fn fstab_generator_impl(root: &Dir, unit_dir: &Dir) -> Result<bool> {
32 if !is_ostree_booted_in(root)? {
34 return Ok(false);
35 }
36
37 if let Some(fd) = root
38 .open_optional("etc/fstab")
39 .context("Opening /etc/fstab")?
40 .map(std::io::BufReader::new)
41 {
42 let mut from_anaconda = false;
43 let mut root_is_readonly = false;
44 for line in fd.lines() {
45 let line = line.context("Reading /etc/fstab")?;
46 if line.contains(BOOTC_EDITED_STAMP) {
47 return Ok(false);
49 }
50 if line.contains(FSTAB_ANACONDA_STAMP) {
51 from_anaconda = true;
52 }
53 let fields = line.split_ascii_whitespace().collect::<Vec<_>>();
54 if !line.trim_start().starts_with('#')
55 && fields.get(1) == Some(&"/")
56 && fields
57 .get(3)
58 .is_some_and(|options| options.split(',').any(|option| option == "ro"))
59 {
60 root_is_readonly = true;
61 }
62 }
63 if !from_anaconda || root_is_readonly {
64 return Ok(false);
65 }
66 tracing::debug!("/etc/fstab from anaconda: {from_anaconda}");
67 if from_anaconda {
68 generate_fstab_editor(unit_dir)?;
69 return Ok(true);
70 }
71 }
72 Ok(false)
73}
74
75pub(crate) fn enable_unit(unitdir: &Dir, name: &str, target: &str) -> Result<()> {
76 let wants = Utf8PathBuf::from(format!("{target}.wants"));
77 unitdir
78 .create_dir_all(&wants)
79 .with_context(|| format!("Creating {wants}"))?;
80 let source = format!("/usr/lib/systemd/system/{name}");
81 let target = wants.join(name);
82 unitdir.remove_file_optional(&target)?;
83 unitdir
84 .symlink_contents(&source, &target)
85 .with_context(|| format!("Writing {name}"))?;
86 Ok(())
87}
88
89pub(crate) fn unit_enablement_impl(sysroot: &Dir, unit_dir: &Dir) -> Result<()> {
91 for unit in [STATUS_ONBOOT_UNIT, STATUS_PATH_UNIT] {
92 enable_unit(unit_dir, unit, MULTI_USER_TARGET)?;
93 }
94
95 if sysroot.try_exists(DESTRUCTIVE_CLEANUP)? {
96 tracing::debug!("Found {DESTRUCTIVE_CLEANUP}");
97 enable_unit(unit_dir, CLEANUP_UNIT, MULTI_USER_TARGET)?;
98 } else {
99 tracing::debug!("Didn't find {DESTRUCTIVE_CLEANUP}");
100 }
101
102 Ok(())
103}
104
105pub(crate) fn generator(root: &Dir, unit_dir: &Dir) -> Result<()> {
107 let is_overlayfs = rustix::fs::fstatfs(root.as_fd())?.f_type == libc::OVERLAYFS_SUPER_MAGIC;
113 let root_source = if is_overlayfs {
114 match bootc_mount::inspect_filesystem(camino::Utf8Path::new("/")) {
115 Ok(fs) => Some(fs.source),
116 Err(e) => {
117 tracing::debug!("Could not inspect root filesystem: {e:#}");
118 None
119 }
120 }
121 } else {
122 None
123 };
124 let root_is_transient = root_source
125 .as_deref()
126 .is_some_and(|s| s.starts_with("transient:composefs="));
127 let is_composefs = root_is_transient
128 || root_source
129 .as_deref()
130 .is_some_and(|s| s.starts_with("composefs:"));
131 let is_ostree = root.try_exists(OSTREE_BOOTED)?;
132
133 if is_composefs {
149 let submounts_are_transient = bootc_initramfs_setup::config_has_transient_submounts(
150 std::path::Path::new(bootc_initramfs_setup::SETUP_ROOT_CONF_PATH),
151 );
152 if root_is_transient || submounts_are_transient {
153 tracing::debug!(
154 root_is_transient,
155 submounts_are_transient,
156 "Transient overlay detected; generating relabel unit"
157 );
158 generate_transient_overlay_relabel(unit_dir)?;
159 }
160 }
161
162 if is_composefs || is_ostree {
168 let updated = shadow_sync_generator_impl(root, unit_dir)?;
169 tracing::trace!("Enabled shadow sync: {updated}");
170 }
171
172 if is_composefs && !is_ostree {
191 generate_early_tmpfiles(unit_dir)?;
192 }
193
194 aboot_generator_impl(root, unit_dir)?;
195
196 if !root.try_exists(OSTREE_BOOTED)? {
199 return Ok(());
200 }
201
202 let Some(ref sysroot) = root.open_dir_optional("sysroot")? else {
203 return Ok(());
204 };
205
206 unit_enablement_impl(sysroot, unit_dir)?;
207
208 let st = rustix::fs::fstatfs(root.as_fd())?;
210 if st.f_type != libc::OVERLAYFS_SUPER_MAGIC {
211 tracing::trace!("Root is not overlayfs");
212 return Ok(());
213 }
214
215 let st = rustix::fs::fstatvfs(root.as_fd())?;
217 if !st.f_flag.contains(StatVfsMountFlags::RDONLY) {
218 tracing::trace!("Root is writable, skipping fstab generator");
219 return Ok(());
220 }
221
222 let updated = fstab_generator_impl(root, unit_dir)?;
223 tracing::trace!("Generated fstab: {updated}");
224
225 Ok(())
226}
227
228#[context("shadow sync generator")]
242pub(crate) fn shadow_sync_generator_impl(root: &Dir, unit_dir: &Dir) -> Result<bool> {
243 if !root.try_exists("etc/shadow")? {
244 tracing::trace!("/etc/shadow not found, skipping shadow sync");
245 return Ok(false);
246 }
247
248 tracing::debug!("/etc/shadow found, enabling {SHADOW_SYNC_UNIT}");
249 enable_unit(unit_dir, SHADOW_SYNC_UNIT, "sysinit.target")?;
250 Ok(true)
251}
252
253fn aboot_generator_impl(root: &Dir, unit_dir: &Dir) -> Result<bool> {
254 let Some(cmdline) = root.read_to_string_optional("proc/cmdline")? else {
255 return Ok(false);
256 };
257 let Some(cmdline) = ComposefsCmdline::find_in_cmdline(&Cmdline::from(cmdline.as_str()))? else {
258 return Ok(false);
259 };
260 let Some(sysroot) = root.open_dir_optional("sysroot")? else {
261 return Ok(false);
262 };
263 if read_boot_type(&sysroot, &cmdline.digest)? != Some(BootType::Aboot) {
264 return Ok(false);
265 }
266 enable_unit(unit_dir, ABOOT_RECONCILE_SERVICE, MULTI_USER_TARGET)?;
267 Ok(true)
268}
269
270fn generate_fstab_editor(unit_dir: &Dir) -> Result<()> {
273 unit_dir.atomic_write(
274 EDIT_UNIT,
275 "[Unit]\n\
276DefaultDependencies=no\n\
277After=systemd-fsck-root.service\n\
278Before=local-fs-pre.target local-fs.target shutdown.target systemd-remount-fs.service\n\
279\n\
280[Service]\n\
281Type=oneshot\n\
282RemainAfterExit=yes\n\
283ExecStart=bootc internals fixup-etc-fstab\n\
284",
285 )?;
286 let target = "local-fs-pre.target.wants";
287 unit_dir.create_dir_all(target)?;
288 unit_dir.symlink(&format!("../{EDIT_UNIT}"), &format!("{target}/{EDIT_UNIT}"))?;
289 Ok(())
290}
291
292fn generate_early_tmpfiles(unit_dir: &Dir) -> Result<()> {
309 unit_dir.atomic_write(
310 EARLY_TMPFILES_UNIT,
311 include_str!("units/bootc-early-tmpfiles.service"),
312 )?;
313 let wants = format!("{SYSINIT_TARGET}.wants");
314 unit_dir.create_dir_all(&wants)?;
315 let link = format!("{wants}/{EARLY_TMPFILES_UNIT}");
316 unit_dir.remove_file_optional(&link)?;
317 unit_dir.symlink(&format!("../{EARLY_TMPFILES_UNIT}"), &link)?;
318 tracing::debug!("Generated {EARLY_TMPFILES_UNIT} for composefs boot");
319 Ok(())
320}
321
322fn generate_transient_overlay_relabel(unit_dir: &Dir) -> Result<()> {
326 unit_dir.atomic_write(
327 TRANSIENT_RELABEL_UNIT,
328 include_str!("units/bootc-early-overlay-relabel.service"),
329 )?;
330 let wants = format!("{SYSINIT_TARGET}.wants");
331 unit_dir.create_dir_all(&wants)?;
332 unit_dir.symlink(
333 &format!("../{TRANSIENT_RELABEL_UNIT}"),
334 &format!("{wants}/{TRANSIENT_RELABEL_UNIT}"),
335 )?;
336 Ok(())
337}
338
339pub(crate) fn relabel_overlay_mountpoints() -> Result<()> {
345 let policy = ostree::SePolicy::new(&gio::File::for_path("/"), gio::Cancellable::NONE)
346 .context("Loading SELinux policy")?;
347 for path in ["/", "/etc", "/var"] {
348 let dir = Dir::open_ambient_dir(path, cap_std::ambient_authority())
349 .with_context(|| format!("Opening {path}"))?;
350 let st = rustix::fs::fstatfs(dir.as_fd())?;
351 if st.f_type != libc::OVERLAYFS_SUPER_MAGIC {
352 tracing::trace!("{path} is not an overlayfs mount, skipping relabel");
353 continue;
354 }
355 let stv = rustix::fs::fstatvfs(dir.as_fd())?;
356 if stv.f_flag.contains(StatVfsMountFlags::RDONLY) {
357 tracing::trace!("{path} is a read-only overlayfs (composefs), skipping relabel");
358 continue;
359 }
360 let metadata = dir.metadata(".").with_context(|| format!("stat {path}"))?;
361 crate::lsm::relabel(
362 &dir,
363 &metadata,
364 Utf8Path::new("."),
365 Some(Utf8Path::new(path)),
366 &policy,
367 )
368 .with_context(|| format!("Relabelling {path}"))?;
369 tracing::debug!("Relabelled {path}");
370 }
371 Ok(())
372}
373
374#[cfg(test)]
375mod tests {
376 use camino::Utf8Path;
377
378 use super::*;
379
380 fn fixture() -> Result<cap_std_ext::cap_tempfile::TempDir> {
381 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
382 tempdir.create_dir("etc")?;
383 tempdir.create_dir("run")?;
384 tempdir.create_dir("sysroot")?;
385 tempdir.create_dir_all("run/systemd/system")?;
386 Ok(tempdir)
387 }
388
389 #[test]
390 fn aboot_reconcile_enablement() -> Result<()> {
391 let digest = "ab".repeat(64);
392 for boot_type in [None, Some("bls"), Some("uki"), Some("aboot")] {
393 let root = fixture()?;
394 let units = root.open_dir("run/systemd/system")?;
395 assert!(!aboot_generator_impl(&root, &units)?);
396 root.create_dir("proc")?;
397 root.atomic_write(
398 "proc/cmdline",
399 format!("composefs=?{digest} androidboot.slot_suffix=_a"),
400 )?;
401 if let Some(boot_type) = boot_type {
402 let path = format!(
403 "sysroot/{}/{digest}",
404 crate::composefs_consts::STATE_DIR_RELATIVE
405 );
406 root.create_dir_all(&path)?;
407 root.atomic_write(
408 format!("{path}/{digest}.origin"),
409 format!("[boot]\nboot_type={boot_type}\n"),
410 )?;
411 }
412 let expected = boot_type == Some("aboot");
413 for _ in 0..2 {
414 assert_eq!(aboot_generator_impl(&root, &units)?, expected);
415 }
416 let link = format!("{MULTI_USER_TARGET}.wants/{ABOOT_RECONCILE_SERVICE}");
417 if expected {
418 assert_eq!(
419 units.read_link_contents(link)?,
420 std::path::Path::new("/usr/lib/systemd/system").join(ABOOT_RECONCILE_SERVICE)
421 );
422 } else {
423 assert_eq!(units.entries()?.count(), 0);
424 }
425 }
426 Ok(())
427 }
428
429 #[test]
430 fn aboot_reconcile_with_dual_format_cmdline() -> Result<()> {
431 use composefs_ctl::composefs::erofs::format::FormatVersion;
432 use composefs_ctl::composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
433
434 let root = fixture()?;
435 let units = root.open_dir("run/systemd/system")?;
436 root.create_dir("proc")?;
437 let v1 = Sha512HashValue::from_hex("ab".repeat(64))?;
438 let v2 = Sha512HashValue::from_hex("cd".repeat(64))?;
439 root.atomic_write(
440 "proc/cmdline",
441 format!(
442 "{} {} androidboot.slot_suffix=_a",
443 crate::bootc_composefs::status::build_composefs_karg(
444 v1.clone(),
445 FormatVersion::V1,
446 false,
447 ),
448 crate::bootc_composefs::status::build_composefs_karg(v2, FormatVersion::V2, false,),
449 ),
450 )?;
451 let digest = v1.to_hex();
452 let path = format!(
453 "sysroot/{}/{digest}",
454 crate::composefs_consts::STATE_DIR_RELATIVE
455 );
456 root.create_dir_all(&path)?;
457 root.atomic_write(
458 format!("{path}/{digest}.origin"),
459 "[boot]\nboot_type=aboot\n",
460 )?;
461
462 assert!(aboot_generator_impl(&root, &units)?);
463 Ok(())
464 }
465
466 #[test]
467 fn test_generator_no_fstab() -> Result<()> {
468 let tempdir = fixture()?;
469 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
470 fstab_generator_impl(&tempdir, &unit_dir).unwrap();
471
472 assert_eq!(unit_dir.entries()?.count(), 0);
473 Ok(())
474 }
475
476 #[test]
477 fn test_units() -> Result<()> {
478 let tempdir = &fixture()?;
479 let sysroot = &tempdir.open_dir("sysroot").unwrap();
480 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
481
482 let verify = |wantsdir: &Dir, n: u32| -> Result<()> {
483 assert_eq!(unit_dir.entries()?.count(), 1);
484 let r = wantsdir.read_link_contents(STATUS_ONBOOT_UNIT)?;
485 let r: Utf8PathBuf = r.try_into().unwrap();
486 assert_eq!(r, format!("/usr/lib/systemd/system/{STATUS_ONBOOT_UNIT}"));
487 assert_eq!(wantsdir.entries()?.count(), n as usize);
488 anyhow::Ok(())
489 };
490
491 unit_enablement_impl(sysroot, &unit_dir).unwrap();
494 unit_enablement_impl(sysroot, &unit_dir).unwrap();
495 let wantsdir = &unit_dir.open_dir("multi-user.target.wants")?;
496 verify(wantsdir, 2)?;
497 assert!(
498 wantsdir
499 .symlink_metadata_optional(CLEANUP_UNIT)
500 .unwrap()
501 .is_none()
502 );
503
504 unit_enablement_impl(sysroot, &unit_dir).unwrap();
506 verify(wantsdir, 2)?;
507
508 sysroot
510 .create_dir_all(Utf8Path::new(DESTRUCTIVE_CLEANUP).parent().unwrap())
511 .unwrap();
512 sysroot.atomic_write(DESTRUCTIVE_CLEANUP, b"").unwrap();
513 unit_enablement_impl(sysroot, unit_dir).unwrap();
514 verify(wantsdir, 3)?;
515
516 assert!(
518 wantsdir
519 .symlink_metadata(CLEANUP_UNIT)
520 .unwrap()
521 .is_symlink()
522 );
523
524 Ok(())
525 }
526
527 #[cfg(test)]
528 mod test {
529 use super::*;
530
531 use ostree_ext::container_utils::OSTREE_BOOTED;
532
533 #[test]
534 fn test_generator_fstab() -> Result<()> {
535 let tempdir = fixture()?;
536 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
537 tempdir.atomic_write("etc/fstab", "# Some dummy fstab")?;
539 fstab_generator_impl(&tempdir, &unit_dir).unwrap();
540 assert_eq!(unit_dir.entries()?.count(), 0);
541
542 tempdir.atomic_write("etc/fstab", &format!("# {FSTAB_ANACONDA_STAMP}"))?;
544 fstab_generator_impl(&tempdir, &unit_dir).unwrap();
545 assert_eq!(unit_dir.entries()?.count(), 0);
546
547 tempdir.atomic_write(OSTREE_BOOTED, "ostree booted")?;
549 fstab_generator_impl(&tempdir, &unit_dir).unwrap();
550 assert_eq!(unit_dir.entries()?.count(), 2);
551
552 Ok(())
553 }
554
555 #[test]
556 fn test_transient_overlay_relabel_generated() -> Result<()> {
557 let tempdir = fixture()?;
558 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
559
560 generate_transient_overlay_relabel(unit_dir)?;
562
563 assert!(unit_dir.try_exists(TRANSIENT_RELABEL_UNIT)?);
565 let wants = format!("{SYSINIT_TARGET}.wants");
567 let link = unit_dir.read_link_contents(format!("{wants}/{TRANSIENT_RELABEL_UNIT}"))?;
568 let link: camino::Utf8PathBuf = link.try_into().unwrap();
569 assert_eq!(link, format!("../{TRANSIENT_RELABEL_UNIT}"));
570 let content = unit_dir.read_to_string(TRANSIENT_RELABEL_UNIT)?;
572 assert!(
573 content.contains("ExecStart=bootc internals relabel-overlay-mountpoints"),
574 "unexpected unit content: {content}"
575 );
576
577 Ok(())
578 }
579
580 #[test]
581 fn test_transient_overlay_relabel_idempotent() -> Result<()> {
582 let tempdir = fixture()?;
583 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
584
585 generate_transient_overlay_relabel(unit_dir)?;
587 let wants = format!("{SYSINIT_TARGET}.wants");
592 unit_dir.remove_file_optional(format!("{wants}/{TRANSIENT_RELABEL_UNIT}"))?;
593 generate_transient_overlay_relabel(unit_dir)?;
594
595 assert!(unit_dir.try_exists(TRANSIENT_RELABEL_UNIT)?);
596
597 Ok(())
598 }
599
600 #[test]
601 fn test_generator_fstab_idempotent() -> Result<()> {
602 let anaconda_fstab = indoc::indoc! { "
603#
604# /etc/fstab
605# Created by anaconda on Tue Mar 19 12:24:29 2024
606#
607# Accessible filesystems, by reference, are maintained under '/dev/disk/'.
608# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info.
609#
610# After editing this file, run 'systemctl daemon-reload' to update systemd
611# units generated from this file.
612#
613# Updated by bootc-fstab-edit.service
614UUID=715be2b7-c458-49f2-acec-b2fdb53d9089 / xfs ro 0 0
615UUID=341c4712-54e8-4839-8020-d94073b1dc8b /boot xfs defaults 0 0
616" };
617 let tempdir = fixture()?;
618 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
619
620 tempdir.atomic_write("etc/fstab", anaconda_fstab)?;
621 tempdir.atomic_write(OSTREE_BOOTED, "ostree booted")?;
622 let updated = fstab_generator_impl(&tempdir, &unit_dir).unwrap();
623 assert!(!updated);
624 assert_eq!(unit_dir.entries()?.count(), 0);
625
626 Ok(())
627 }
628
629 #[test]
630 fn test_generator_fstab_readonly_root() -> Result<()> {
631 let anaconda_fstab = indoc::indoc! { "
632# Created by anaconda
633UUID=715be2b7-c458-49f2-acec-b2fdb53d9089 / xfs defaults,ro 0 0
634" };
635 let tempdir = fixture()?;
636 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
637
638 tempdir.atomic_write("etc/fstab", anaconda_fstab)?;
639 tempdir.atomic_write(OSTREE_BOOTED, "ostree booted")?;
640 let updated = fstab_generator_impl(&tempdir, unit_dir)?;
641 assert!(!updated);
642 assert_eq!(unit_dir.entries()?.count(), 0);
643
644 Ok(())
645 }
646
647 fn unit_values<'a>(unit: &'a str, key: &str) -> Vec<&'a str> {
649 unit.lines()
650 .filter_map(|l| l.strip_prefix(key)?.strip_prefix('='))
651 .flat_map(str::split_ascii_whitespace)
652 .collect()
653 }
654
655 #[test]
656 fn test_early_tmpfiles_unit() -> Result<()> {
657 let tempdir = fixture()?;
658 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
659
660 for _ in 0..2 {
662 generate_early_tmpfiles(unit_dir)?;
663 }
664
665 let unit = unit_dir.read_to_string(EARLY_TMPFILES_UNIT)?;
666 assert_eq!(
667 unit_values(&unit, "ExecStart"),
668 ["systemd-tmpfiles", "--create", "--prefix=/var/lib/systemd"]
669 );
670 assert_eq!(
671 unit_values(&unit, "Before"),
672 [
673 "systemd-random-seed.service",
674 "systemd-tpm2-setup.service",
675 "sysinit.target",
676 "shutdown.target"
677 ]
678 );
679 assert_eq!(unit_values(&unit, "DefaultDependencies"), ["no"]);
683 assert_eq!(unit_values(&unit, "After"), ["systemd-remount-fs.service"]);
684 assert_eq!(
685 unit_values(&unit, "RequiresMountsFor"),
686 ["/var/lib/systemd"]
687 );
688
689 let link = unit_dir
690 .read_link_contents(format!("{SYSINIT_TARGET}.wants/{EARLY_TMPFILES_UNIT}"))?;
691 assert_eq!(link, std::path::Path::new("..").join(EARLY_TMPFILES_UNIT));
692
693 Ok(())
694 }
695
696 #[test]
697 fn test_shadow_sync_no_shadow() -> Result<()> {
698 let tempdir = fixture()?;
700 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
701 let generated = shadow_sync_generator_impl(&tempdir, unit_dir)?;
702 assert!(!generated);
703 assert_eq!(unit_dir.entries()?.count(), 0);
704 Ok(())
705 }
706
707 #[test]
708 fn test_shadow_sync_enables_when_shadow_present() -> Result<()> {
709 let tempdir = fixture()?;
711 tempdir.atomic_write("etc/shadow", "root:*:18912:0:99999:7:::\n")?;
712 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
713 let generated = shadow_sync_generator_impl(&tempdir, unit_dir)?;
714 assert!(generated);
715 let wants = unit_dir.open_dir("sysinit.target.wants")?;
719 let meta = wants.symlink_metadata(SHADOW_SYNC_UNIT)?;
720 assert!(meta.is_symlink(), "expected symlink for {SHADOW_SYNC_UNIT}");
721 Ok(())
722 }
723
724 #[test]
727 fn test_generator_shadow_sync_on_non_composefs() -> Result<()> {
728 let tempdir = fixture()?;
729 tempdir.atomic_write(OSTREE_BOOTED, "")?;
730 tempdir.atomic_write("etc/shadow", "root:*:18912:0:99999:7:::\n")?;
731 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
732 generator(&tempdir, unit_dir)?;
733 let wants = unit_dir.open_dir("sysinit.target.wants")?;
734 let meta = wants.symlink_metadata(SHADOW_SYNC_UNIT)?;
735 assert!(
736 meta.is_symlink(),
737 "shadow sync unit must be enabled on non-composefs ostree systems"
738 );
739 Ok(())
740 }
741 }
742}