11use std:: io:: BufRead ;
22
33use anyhow:: { Context , Result } ;
4+ use camino:: Utf8PathBuf ;
45use cap_std:: fs:: Dir ;
56use cap_std_ext:: { cap_std, dirext:: CapStdExtDirExt } ;
67use fn_error_context:: context;
7- use ostree_ext:: container_utils:: is_ostree_booted_in;
8+ use ostree_ext:: container_utils:: { is_ostree_booted_in, OSTREE_BOOTED } ;
89use rustix:: { fd:: AsFd , fs:: StatVfsMountFlags } ;
910
11+ use crate :: install:: DESTRUCTIVE_CLEANUP ;
12+
13+ const STATUS_ONBOOT_UNIT : & str = "bootc-status-updated-onboot.target" ;
14+ const STATUS_PATH_UNIT : & str = "bootc-status-updated.path" ;
15+ const CLEANUP_UNIT : & str = "bootc-destructive-cleanup.service" ;
16+ const MULTI_USER_TARGET : & str = "multi-user.target" ;
1017const EDIT_UNIT : & str = "bootc-fstab-edit.service" ;
1118const FSTAB_ANACONDA_STAMP : & str = "Created by anaconda" ;
1219pub ( crate ) const BOOTC_EDITED_STAMP : & str = "Updated by bootc-fstab-edit.service" ;
@@ -47,9 +54,50 @@ pub(crate) fn fstab_generator_impl(root: &Dir, unit_dir: &Dir) -> Result<bool> {
4754 Ok ( false )
4855}
4956
57+ pub ( 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+
71+ /// Enable our units
72+ pub ( 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+
5087/// Main entrypoint for the generator
5188pub ( crate ) fn generator ( root : & Dir , unit_dir : & Dir ) -> Result < ( ) > {
52- // Right now we only do something if the root is a read-only overlayfs (a composefs really)
89+ // Only run on ostree systems
90+ 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+ // Also only run if the root is a read-only overlayfs (a composefs really)
53101 let st = rustix:: fs:: fstatfs ( root. as_fd ( ) ) ?;
54102 if st. f_type != libc:: OVERLAYFS_SUPER_MAGIC {
55103 tracing:: trace!( "Root is not overlayfs" ) ;
@@ -62,6 +110,7 @@ pub(crate) fn generator(root: &Dir, unit_dir: &Dir) -> Result<()> {
62110 }
63111 let updated = fstab_generator_impl ( root, unit_dir) ?;
64112 tracing:: trace!( "Generated fstab: {updated}" ) ;
113+
65114 Ok ( ( ) )
66115}
67116
@@ -89,12 +138,16 @@ ExecStart=bootc internals fixup-etc-fstab\n\
89138
90139#[ cfg( test) ]
91140mod tests {
141+ use camino:: Utf8Path ;
142+ use cap_std_ext:: cmdext:: CapStdExtCommandExt as _;
143+
92144 use super :: * ;
93145
94146 fn fixture ( ) -> Result < cap_std_ext:: cap_tempfile:: TempDir > {
95147 let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( cap_std:: ambient_authority ( ) ) ?;
96148 tempdir. create_dir ( "etc" ) ?;
97149 tempdir. create_dir ( "run" ) ?;
150+ tempdir. create_dir ( "sysroot" ) ?;
98151 tempdir. create_dir_all ( "run/systemd/system" ) ?;
99152 Ok ( tempdir)
100153 }
@@ -109,6 +162,53 @@ mod tests {
109162 Ok ( ( ) )
110163 }
111164
165+ #[ test]
166+ fn test_units ( ) -> Result < ( ) > {
167+ let tempdir = & fixture ( ) ?;
168+ let sysroot = & tempdir. open_dir ( "sysroot" ) . unwrap ( ) ;
169+ let unit_dir = & tempdir. open_dir ( "run/systemd/system" ) ?;
170+
171+ let verify = |wantsdir : & Dir , n : u32 | -> Result < ( ) > {
172+ assert_eq ! ( unit_dir. entries( ) ?. count( ) , 1 ) ;
173+ let r = wantsdir. read_link_contents ( STATUS_ONBOOT_UNIT ) ?;
174+ let r: Utf8PathBuf = r. try_into ( ) . unwrap ( ) ;
175+ assert_eq ! ( r, format!( "/usr/lib/systemd/system/{STATUS_ONBOOT_UNIT}" ) ) ;
176+ assert_eq ! ( wantsdir. entries( ) ?. count( ) , n as usize ) ;
177+ anyhow:: Ok ( ( ) )
178+ } ;
179+
180+ // Explicitly run this twice to test idempotency
181+
182+ unit_enablement_impl ( sysroot, & unit_dir) . unwrap ( ) ;
183+ unit_enablement_impl ( sysroot, & unit_dir) . unwrap ( ) ;
184+ let wantsdir = & unit_dir. open_dir ( "multi-user.target.wants" ) ?;
185+ verify ( wantsdir, 2 ) ?;
186+ assert ! ( wantsdir
187+ . symlink_metadata_optional( CLEANUP_UNIT )
188+ . unwrap( )
189+ . is_none( ) ) ;
190+
191+ // Now create sysroot and rerun the generator
192+ unit_enablement_impl ( sysroot, & unit_dir) . unwrap ( ) ;
193+ verify ( wantsdir, 2 ) ?;
194+
195+ // Create the destructive stamp
196+ sysroot
197+ . create_dir_all ( Utf8Path :: new ( DESTRUCTIVE_CLEANUP ) . parent ( ) . unwrap ( ) )
198+ . unwrap ( ) ;
199+ sysroot. atomic_write ( DESTRUCTIVE_CLEANUP , b"" ) . unwrap ( ) ;
200+ unit_enablement_impl ( sysroot, unit_dir) . unwrap ( ) ;
201+ verify ( wantsdir, 3 ) ?;
202+
203+ // And now the unit should be enabled
204+ assert ! ( wantsdir
205+ . symlink_metadata( CLEANUP_UNIT )
206+ . unwrap( )
207+ . is_symlink( ) ) ;
208+
209+ Ok ( ( ) )
210+ }
211+
112212 #[ cfg( test) ]
113213 mod test {
114214 use super :: * ;
0 commit comments