bootc_lib/bootc_composefs/
state.rs1use std::io::Write;
2use std::os::unix::fs::symlink;
3use std::path::Path;
4use std::{fs::create_dir_all, process::Command};
5
6use anyhow::{Context, Result};
7use bootc_initramfs_setup::overlay_transient;
8use bootc_kernel_cmdline::utf8::Cmdline;
9use bootc_mount::tempmount::TempMount;
10use bootc_utils::CommandRunExt;
11use camino::Utf8PathBuf;
12use cap_std_ext::cap_std::ambient_authority;
13use cap_std_ext::cap_std::fs::{Dir, Permissions, PermissionsExt};
14use cap_std_ext::dirext::CapStdExtDirExt;
15use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
16use fn_error_context::context;
17
18use ostree_ext::container::deploy::ORIGIN_CONTAINER;
19use rustix::{
20 fs::{open, Mode, OFlags},
21 path::Arg,
22};
23
24use crate::bootc_composefs::boot::BootType;
25use crate::bootc_composefs::repo::get_imgref;
26use crate::bootc_composefs::status::{get_sorted_type1_boot_entries, ImgConfigManifest};
27use crate::parsers::bls_config::BLSConfigType;
28use crate::store::{BootedComposefs, Storage};
29use crate::{
30 composefs_consts::{
31 COMPOSEFS_CMDLINE, COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR,
32 ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_BOOT_TYPE, SHARED_VAR_PATH,
33 STATE_DIR_RELATIVE,
34 },
35 parsers::bls_config::BLSConfig,
36 spec::ImageReference,
37 utils::path_relative_to,
38};
39
40pub(crate) fn get_booted_bls(boot_dir: &Dir) -> Result<BLSConfig> {
41 let cmdline = Cmdline::from_proc()?;
42 let booted = cmdline
43 .find(COMPOSEFS_CMDLINE)
44 .ok_or_else(|| anyhow::anyhow!("Failed to find composefs parameter in kernel cmdline"))?;
45
46 let sorted_entries = get_sorted_type1_boot_entries(boot_dir, true)?;
47
48 for entry in sorted_entries {
49 match &entry.cfg_type {
50 BLSConfigType::EFI { efi } => {
51 let composefs_param_value = booted.value().ok_or_else(|| {
52 anyhow::anyhow!("Failed to get composefs kernel cmdline value")
53 })?;
54
55 if efi.as_str().contains(composefs_param_value) {
56 return Ok(entry);
57 }
58 }
59
60 BLSConfigType::NonEFI { options, .. } => {
61 let Some(opts) = options else {
62 anyhow::bail!("options not found in bls config")
63 };
64
65 let opts = Cmdline::from(opts);
66
67 if opts.iter().any(|v| v == booted) {
68 return Ok(entry);
69 }
70 }
71
72 BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config type"),
73 };
74 }
75
76 Err(anyhow::anyhow!("Booted BLS not found"))
77}
78
79#[context("Initializing /etc and /var for state")]
82pub(crate) fn initialize_state(
83 sysroot_path: &Utf8PathBuf,
84 erofs_id: &String,
85 state_path: &Utf8PathBuf,
86 initialize_var: bool,
87) -> Result<()> {
88 let sysroot_fd = open(
89 sysroot_path.as_std_path(),
90 OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
91 Mode::empty(),
92 )
93 .context("Opening sysroot")?;
94
95 let composefs_fd = bootc_initramfs_setup::mount_composefs_image(&sysroot_fd, &erofs_id, false)?;
96
97 let tempdir = TempMount::mount_fd(composefs_fd)?;
98
99 if initialize_var {
101 Command::new("cp")
102 .args([
103 "-a",
104 "--remove-destination",
105 &format!("{}/var/.", tempdir.dir.path().as_str()?),
106 &format!("{state_path}/var/."),
107 ])
108 .run_capture_stderr()?;
109 }
110
111 let cp_ret = Command::new("cp")
112 .args([
113 "-a",
114 "--remove-destination",
115 &format!("{}/etc/.", tempdir.dir.path().as_str()?),
116 &format!("{state_path}/etc/."),
117 ])
118 .run_capture_stderr();
119
120 cp_ret
121}
122
123fn add_update_in_origin(
126 storage: &Storage,
127 deployment_id: &str,
128 section: &str,
129 kv_pairs: &[(&str, &str)],
130) -> Result<()> {
131 let path = Path::new(STATE_DIR_RELATIVE).join(deployment_id);
132
133 let state_dir = storage
134 .physical_root
135 .open_dir(path)
136 .context("Opening state dir")?;
137
138 let origin_filename = format!("{deployment_id}.origin");
139
140 let origin_file = state_dir
141 .read_to_string(&origin_filename)
142 .context("Reading origin file")?;
143
144 let mut ini =
145 tini::Ini::from_string(&origin_file).context("Failed to parse file origin file as ini")?;
146
147 for (key, value) in kv_pairs {
148 ini = ini.section(section).item(*key, *value);
149 }
150
151 state_dir
152 .atomic_replace_with(origin_filename, move |f| -> std::io::Result<_> {
153 f.write_all(ini.to_string().as_bytes())?;
154 f.flush()?;
155
156 let perms = Permissions::from_mode(0o644);
157 f.get_mut().as_file_mut().set_permissions(perms)?;
158
159 Ok(())
160 })
161 .context("Writing to origin file")?;
162
163 Ok(())
164}
165
166pub(crate) fn update_target_imgref_in_origin(
168 storage: &Storage,
169 booted_cfs: &BootedComposefs,
170 imgref: &ImageReference,
171) -> Result<()> {
172 add_update_in_origin(
173 storage,
174 booted_cfs.cmdline.digest.as_ref(),
175 "origin",
176 &[(
177 ORIGIN_CONTAINER,
178 &format!(
179 "ostree-unverified-image:{}",
180 get_imgref(&imgref.transport, &imgref.image)
181 ),
182 )],
183 )
184}
185
186pub(crate) fn update_boot_digest_in_origin(
187 storage: &Storage,
188 digest: &str,
189 boot_digest: &str,
190) -> Result<()> {
191 add_update_in_origin(
192 storage,
193 digest,
194 ORIGIN_KEY_BOOT,
195 &[(ORIGIN_KEY_BOOT_DIGEST, boot_digest)],
196 )
197}
198
199#[context("Writing composefs state")]
226pub(crate) async fn write_composefs_state(
227 root_path: &Utf8PathBuf,
228 deployment_id: &Sha512HashValue,
229 target_imgref: &ImageReference,
230 staged: bool,
231 boot_type: BootType,
232 boot_digest: String,
233 container_details: &ImgConfigManifest,
234) -> Result<()> {
235 let state_path = root_path
236 .join(STATE_DIR_RELATIVE)
237 .join(deployment_id.to_hex());
238
239 create_dir_all(state_path.join("etc"))?;
240
241 let actual_var_path = root_path.join(SHARED_VAR_PATH);
242 create_dir_all(&actual_var_path)?;
243
244 symlink(
245 path_relative_to(state_path.as_std_path(), actual_var_path.as_std_path())
246 .context("Getting var symlink path")?,
247 state_path.join("var"),
248 )
249 .context("Failed to create symlink for /var")?;
250
251 initialize_state(&root_path, &deployment_id.to_hex(), &state_path, !staged)?;
252
253 let ImageReference {
254 image: image_name,
255 transport,
256 ..
257 } = &target_imgref;
258
259 let imgref = get_imgref(&transport, &image_name);
260
261 let mut config = tini::Ini::new().section("origin").item(
262 ORIGIN_CONTAINER,
263 format!("ostree-unverified-image:{imgref}"),
265 );
266
267 config = config
268 .section(ORIGIN_KEY_BOOT)
269 .item(ORIGIN_KEY_BOOT_TYPE, boot_type);
270
271 config = config
272 .section(ORIGIN_KEY_BOOT)
273 .item(ORIGIN_KEY_BOOT_DIGEST, boot_digest);
274
275 let state_dir =
276 Dir::open_ambient_dir(&state_path, ambient_authority()).context("Opening state dir")?;
277
278 state_dir
281 .atomic_write(
282 format!("{}.imginfo", deployment_id.to_hex()),
283 serde_json::to_vec(&container_details)?,
284 )
285 .context("Failed to write to .imginfo file")?;
286
287 state_dir
288 .atomic_write(
289 format!("{}.origin", deployment_id.to_hex()),
290 config.to_string().as_bytes(),
291 )
292 .context("Failed to write to .origin file")?;
293
294 if staged {
295 std::fs::create_dir_all(COMPOSEFS_TRANSIENT_STATE_DIR)
296 .with_context(|| format!("Creating {COMPOSEFS_TRANSIENT_STATE_DIR}"))?;
297
298 let staged_depl_dir =
299 Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority())
300 .with_context(|| format!("Opening {COMPOSEFS_TRANSIENT_STATE_DIR}"))?;
301
302 staged_depl_dir
303 .atomic_write(
304 COMPOSEFS_STAGED_DEPLOYMENT_FNAME,
305 deployment_id.to_hex().as_bytes(),
306 )
307 .with_context(|| format!("Writing to {COMPOSEFS_STAGED_DEPLOYMENT_FNAME}"))?;
308 }
309
310 Ok(())
311}
312
313pub(crate) fn composefs_usr_overlay() -> Result<()> {
314 let usr = Dir::open_ambient_dir("/usr", ambient_authority()).context("Opening /usr")?;
315 let is_usr_mounted = usr
316 .is_mountpoint(".")
317 .context("Failed to get mount details for /usr")?;
318
319 let is_usr_mounted =
320 is_usr_mounted.ok_or_else(|| anyhow::anyhow!("Failed to get mountinfo"))?;
321
322 if is_usr_mounted {
323 println!("A writeable overlayfs is already mounted on /usr");
324 return Ok(());
325 }
326
327 let usr_metadata = usr.metadata(".").context("Getting /usr metadata")?;
329 let usr_mode = Mode::from_raw_mode(usr_metadata.permissions().mode());
330
331 overlay_transient(usr, Some(usr_mode))?;
332
333 println!("A writeable overlayfs is now mounted on /usr");
334 println!("All changes there will be discarded on reboot.");
335
336 Ok(())
337}