1use std::io::BufRead;
2
3use anyhow::{Context, Result};
4use camino::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 rustix::{fd::AsFd, fs::StatVfsMountFlags};
10
11use crate::install::DESTRUCTIVE_CLEANUP;
12
13const STATUS_ONBOOT_UNIT: &str = "bootc-status-updated-onboot.target";
14const STATUS_PATH_UNIT: &str = "bootc-status-updated.path";
15const CLEANUP_UNIT: &str = "bootc-destructive-cleanup.service";
16const MULTI_USER_TARGET: &str = "multi-user.target";
17const EDIT_UNIT: &str = "bootc-fstab-edit.service";
18const FSTAB_ANACONDA_STAMP: &str = "Created by anaconda";
19pub(crate) const BOOTC_EDITED_STAMP: &str = "Updated by bootc-fstab-edit.service";
20
21#[context("bootc generator")]
23pub(crate) fn fstab_generator_impl(root: &Dir, unit_dir: &Dir) -> Result<bool> {
24 if !is_ostree_booted_in(root)? {
26 return Ok(false);
27 }
28
29 if let Some(fd) = root
30 .open_optional("etc/fstab")
31 .context("Opening /etc/fstab")?
32 .map(std::io::BufReader::new)
33 {
34 let mut from_anaconda = false;
35 for line in fd.lines() {
36 let line = line.context("Reading /etc/fstab")?;
37 if line.contains(BOOTC_EDITED_STAMP) {
38 return Ok(false);
40 }
41 if line.contains(FSTAB_ANACONDA_STAMP) {
42 from_anaconda = true;
43 }
44 }
45 if !from_anaconda {
46 return Ok(false);
47 }
48 tracing::debug!("/etc/fstab from anaconda: {from_anaconda}");
49 if from_anaconda {
50 generate_fstab_editor(unit_dir)?;
51 return Ok(true);
52 }
53 }
54 Ok(false)
55}
56
57pub(crate) fn enable_unit(unitdir: &Dir, name: &str, target: &str) -> Result<()> {
58 let wants = Utf8PathBuf::from(format!("{target}.wants"));
59 unitdir
60 .create_dir_all(&wants)
61 .with_context(|| format!("Creating {wants}"))?;
62 let source = format!("/usr/lib/systemd/system/{name}");
63 let target = wants.join(name);
64 unitdir.remove_file_optional(&target)?;
65 unitdir
66 .symlink_contents(&source, &target)
67 .with_context(|| format!("Writing {name}"))?;
68 Ok(())
69}
70
71pub(crate) fn unit_enablement_impl(sysroot: &Dir, unit_dir: &Dir) -> Result<()> {
73 for unit in [STATUS_ONBOOT_UNIT, STATUS_PATH_UNIT] {
74 enable_unit(unit_dir, unit, MULTI_USER_TARGET)?;
75 }
76
77 if sysroot.try_exists(DESTRUCTIVE_CLEANUP)? {
78 tracing::debug!("Found {DESTRUCTIVE_CLEANUP}");
79 enable_unit(unit_dir, CLEANUP_UNIT, MULTI_USER_TARGET)?;
80 } else {
81 tracing::debug!("Didn't find {DESTRUCTIVE_CLEANUP}");
82 }
83
84 Ok(())
85}
86
87pub(crate) fn generator(root: &Dir, unit_dir: &Dir) -> Result<()> {
89 if !root.try_exists(OSTREE_BOOTED)? {
91 return Ok(());
92 }
93
94 let Some(ref sysroot) = root.open_dir_optional("sysroot")? else {
95 return Ok(());
96 };
97
98 unit_enablement_impl(sysroot, unit_dir)?;
99
100 let st = rustix::fs::fstatfs(root.as_fd())?;
102 if st.f_type != libc::OVERLAYFS_SUPER_MAGIC {
103 tracing::trace!("Root is not overlayfs");
104 return Ok(());
105 }
106 let st = rustix::fs::fstatvfs(root.as_fd())?;
107 if !st.f_flag.contains(StatVfsMountFlags::RDONLY) {
108 tracing::trace!("Root is writable");
109 return Ok(());
110 }
111 let updated = fstab_generator_impl(root, unit_dir)?;
112 tracing::trace!("Generated fstab: {updated}");
113
114 Ok(())
115}
116
117fn generate_fstab_editor(unit_dir: &Dir) -> Result<()> {
120 unit_dir.atomic_write(
121 EDIT_UNIT,
122 "[Unit]\n\
123DefaultDependencies=no\n\
124After=systemd-fsck-root.service\n\
125Before=local-fs-pre.target local-fs.target shutdown.target systemd-remount-fs.service\n\
126\n\
127[Service]\n\
128Type=oneshot\n\
129RemainAfterExit=yes\n\
130ExecStart=bootc internals fixup-etc-fstab\n\
131",
132 )?;
133 let target = "local-fs-pre.target.wants";
134 unit_dir.create_dir_all(target)?;
135 unit_dir.symlink(&format!("../{EDIT_UNIT}"), &format!("{target}/{EDIT_UNIT}"))?;
136 Ok(())
137}
138
139#[cfg(test)]
140mod tests {
141 use camino::Utf8Path;
142
143 use super::*;
144
145 fn fixture() -> Result<cap_std_ext::cap_tempfile::TempDir> {
146 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
147 tempdir.create_dir("etc")?;
148 tempdir.create_dir("run")?;
149 tempdir.create_dir("sysroot")?;
150 tempdir.create_dir_all("run/systemd/system")?;
151 Ok(tempdir)
152 }
153
154 #[test]
155 fn test_generator_no_fstab() -> Result<()> {
156 let tempdir = fixture()?;
157 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
158 fstab_generator_impl(&tempdir, &unit_dir).unwrap();
159
160 assert_eq!(unit_dir.entries()?.count(), 0);
161 Ok(())
162 }
163
164 #[test]
165 fn test_units() -> Result<()> {
166 let tempdir = &fixture()?;
167 let sysroot = &tempdir.open_dir("sysroot").unwrap();
168 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
169
170 let verify = |wantsdir: &Dir, n: u32| -> Result<()> {
171 assert_eq!(unit_dir.entries()?.count(), 1);
172 let r = wantsdir.read_link_contents(STATUS_ONBOOT_UNIT)?;
173 let r: Utf8PathBuf = r.try_into().unwrap();
174 assert_eq!(r, format!("/usr/lib/systemd/system/{STATUS_ONBOOT_UNIT}"));
175 assert_eq!(wantsdir.entries()?.count(), n as usize);
176 anyhow::Ok(())
177 };
178
179 unit_enablement_impl(sysroot, &unit_dir).unwrap();
182 unit_enablement_impl(sysroot, &unit_dir).unwrap();
183 let wantsdir = &unit_dir.open_dir("multi-user.target.wants")?;
184 verify(wantsdir, 2)?;
185 assert!(
186 wantsdir
187 .symlink_metadata_optional(CLEANUP_UNIT)
188 .unwrap()
189 .is_none()
190 );
191
192 unit_enablement_impl(sysroot, &unit_dir).unwrap();
194 verify(wantsdir, 2)?;
195
196 sysroot
198 .create_dir_all(Utf8Path::new(DESTRUCTIVE_CLEANUP).parent().unwrap())
199 .unwrap();
200 sysroot.atomic_write(DESTRUCTIVE_CLEANUP, b"").unwrap();
201 unit_enablement_impl(sysroot, unit_dir).unwrap();
202 verify(wantsdir, 3)?;
203
204 assert!(
206 wantsdir
207 .symlink_metadata(CLEANUP_UNIT)
208 .unwrap()
209 .is_symlink()
210 );
211
212 Ok(())
213 }
214
215 #[cfg(test)]
216 mod test {
217 use super::*;
218
219 use ostree_ext::container_utils::OSTREE_BOOTED;
220
221 #[test]
222 fn test_generator_fstab() -> Result<()> {
223 let tempdir = fixture()?;
224 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
225 tempdir.atomic_write("etc/fstab", "# Some dummy fstab")?;
227 fstab_generator_impl(&tempdir, &unit_dir).unwrap();
228 assert_eq!(unit_dir.entries()?.count(), 0);
229
230 tempdir.atomic_write("etc/fstab", &format!("# {FSTAB_ANACONDA_STAMP}"))?;
232 fstab_generator_impl(&tempdir, &unit_dir).unwrap();
233 assert_eq!(unit_dir.entries()?.count(), 0);
234
235 tempdir.atomic_write(OSTREE_BOOTED, "ostree booted")?;
237 fstab_generator_impl(&tempdir, &unit_dir).unwrap();
238 assert_eq!(unit_dir.entries()?.count(), 2);
239
240 Ok(())
241 }
242
243 #[test]
244 fn test_generator_fstab_idempotent() -> Result<()> {
245 let anaconda_fstab = indoc::indoc! { "
246#
247# /etc/fstab
248# Created by anaconda on Tue Mar 19 12:24:29 2024
249#
250# Accessible filesystems, by reference, are maintained under '/dev/disk/'.
251# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info.
252#
253# After editing this file, run 'systemctl daemon-reload' to update systemd
254# units generated from this file.
255#
256# Updated by bootc-fstab-edit.service
257UUID=715be2b7-c458-49f2-acec-b2fdb53d9089 / xfs ro 0 0
258UUID=341c4712-54e8-4839-8020-d94073b1dc8b /boot xfs defaults 0 0
259" };
260 let tempdir = fixture()?;
261 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
262
263 tempdir.atomic_write("etc/fstab", anaconda_fstab)?;
264 tempdir.atomic_write(OSTREE_BOOTED, "ostree booted")?;
265 let updated = fstab_generator_impl(&tempdir, &unit_dir).unwrap();
266 assert!(!updated);
267 assert_eq!(unit_dir.entries()?.count(), 0);
268
269 Ok(())
270 }
271 }
272}