bootc_lib/bootc_composefs/
update.rs1use anyhow::{Context, Result};
2use camino::Utf8PathBuf;
3use cap_std_ext::cap_std::fs::Dir;
4use composefs::{
5 fsverity::{FsVerityHashValue, Sha512HashValue},
6 util::{parse_sha256, Sha256Digest},
7};
8use composefs_boot::BootOps;
9use composefs_oci::image::create_filesystem;
10use fn_error_context::context;
11use ostree_ext::container::ManifestDiff;
12
13use crate::{
14 bootc_composefs::{
15 boot::{setup_composefs_bls_boot, setup_composefs_uki_boot, BootSetupType, BootType},
16 repo::{get_imgref, pull_composefs_repo},
17 service::start_finalize_stated_svc,
18 soft_reboot::prepare_soft_reboot_composefs,
19 state::write_composefs_state,
20 status::{
21 get_bootloader, get_composefs_status, get_container_manifest_and_config, get_imginfo,
22 ImgConfigManifest,
23 },
24 },
25 cli::{SoftRebootMode, UpgradeOpts},
26 composefs_consts::{STATE_DIR_RELATIVE, TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED},
27 spec::{Bootloader, Host, ImageReference},
28 store::{BootedComposefs, ComposefsRepository, Storage},
29};
30
31#[context("Getting SHA256 Digest for {id}")]
32pub fn str_to_sha256digest(id: &str) -> Result<Sha256Digest> {
33 let id = id.strip_prefix("sha256:").unwrap_or(id);
34 Ok(parse_sha256(&id)?)
35}
36
37#[context("Checking if image {} is pulled", imgref.image)]
56pub(crate) async fn is_image_pulled(
57 repo: &ComposefsRepository,
58 imgref: &ImageReference,
59) -> Result<(Option<Sha512HashValue>, ImgConfigManifest)> {
60 let imgref_repr = get_imgref(&imgref.transport, &imgref.image);
61 let img_config_manifest = get_container_manifest_and_config(&imgref_repr).await?;
62
63 let img_digest = img_config_manifest.manifest.config().digest().digest();
64 let img_sha256 = str_to_sha256digest(&img_digest)?;
65
66 let container_pulled = repo.check_stream(&img_sha256).context("Checking stream")?;
68
69 Ok((container_pulled, img_config_manifest))
70}
71
72fn rm_staged_type1_ent(boot_dir: &Dir) -> Result<()> {
73 if boot_dir.exists(TYPE1_ENT_PATH_STAGED) {
74 boot_dir
75 .remove_dir_all(TYPE1_ENT_PATH_STAGED)
76 .context("Removing staged bootloader entry")?;
77 }
78
79 Ok(())
80}
81
82#[derive(Debug)]
83pub(crate) enum UpdateAction {
84 Skip,
86 Proceed,
88 UpdateOrigin,
91}
92
93pub(crate) fn validate_update(
131 storage: &Storage,
132 booted_cfs: &BootedComposefs,
133 host: &Host,
134 img_digest: &str,
135 config_verity: &Sha512HashValue,
136 is_switch: bool,
137) -> Result<UpdateAction> {
138 let repo = &*booted_cfs.repo;
139
140 let mut fs = create_filesystem(repo, img_digest, Some(config_verity))?;
141 fs.transform_for_boot(&repo)?;
142
143 let image_id = fs.compute_image_id();
144
145 if image_id.to_hex() == *booted_cfs.cmdline.digest {
155 let ret = if is_switch {
156 UpdateAction::UpdateOrigin
157 } else {
158 UpdateAction::Skip
159 };
160
161 return Ok(ret);
162 }
163
164 let all_deployments = host.all_composefs_deployments()?;
165
166 let found_depl = all_deployments
167 .iter()
168 .find(|d| d.deployment.verity == image_id.to_hex());
169
170 if found_depl.is_some() {
172 return Ok(UpdateAction::Skip);
173 }
174
175 let booted = host.require_composefs_booted()?;
176 let boot_dir = storage.require_boot_dir()?;
177
178 match get_bootloader()? {
181 Bootloader::Grub => match booted.boot_type {
182 BootType::Bls => rm_staged_type1_ent(boot_dir)?,
183
184 BootType::Uki => {
185 let grub = boot_dir.open_dir("grub2").context("Opening grub dir")?;
186
187 if grub.exists(USER_CFG_STAGED) {
188 grub.remove_file(USER_CFG_STAGED)
189 .context("Removing staged grub user config")?;
190 }
191 }
192 },
193
194 Bootloader::Systemd => rm_staged_type1_ent(boot_dir)?,
195 }
196
197 let state_dir = storage
199 .physical_root
200 .open_dir(STATE_DIR_RELATIVE)
201 .context("Opening state dir")?;
202
203 if state_dir.exists(image_id.to_hex()) {
204 state_dir
205 .remove_dir_all(image_id.to_hex())
206 .context("Removing state")?;
207 }
208
209 Ok(UpdateAction::Proceed)
210}
211
212pub(crate) struct DoUpgradeOpts {
214 pub(crate) apply: bool,
215 pub(crate) soft_reboot: Option<SoftRebootMode>,
216}
217
218#[context("Performing Upgrade Operation")]
220pub(crate) async fn do_upgrade(
221 storage: &Storage,
222 booted_cfs: &BootedComposefs,
223 host: &Host,
224 imgref: &ImageReference,
225 img_manifest_config: &ImgConfigManifest,
226 opts: &DoUpgradeOpts,
227) -> Result<()> {
228 start_finalize_stated_svc()?;
229
230 let (repo, entries, id, fs) = pull_composefs_repo(&imgref.transport, &imgref.image).await?;
231
232 let Some(entry) = entries.iter().next() else {
233 anyhow::bail!("No boot entries!");
234 };
235
236 let mounted_fs = Dir::reopen_dir(
237 &repo
238 .mount(&id.to_hex())
239 .context("Failed to mount composefs image")?,
240 )?;
241
242 let boot_type = BootType::from(entry);
243
244 let boot_digest = match boot_type {
245 BootType::Bls => setup_composefs_bls_boot(
246 BootSetupType::Upgrade((storage, &fs, &host)),
247 repo,
248 &id,
249 entry,
250 &mounted_fs,
251 )?,
252
253 BootType::Uki => setup_composefs_uki_boot(
254 BootSetupType::Upgrade((storage, &fs, &host)),
255 repo,
256 &id,
257 entries,
258 )?,
259 };
260
261 write_composefs_state(
262 &Utf8PathBuf::from("/sysroot"),
263 &id,
264 imgref,
265 true,
266 boot_type,
267 boot_digest,
268 img_manifest_config,
269 )
270 .await?;
271
272 if opts.apply {
273 return crate::reboot::reboot();
274 }
275
276 if opts.soft_reboot.is_some() {
277 prepare_soft_reboot_composefs(storage, booted_cfs, &id.to_hex(), true).await?;
278 }
279
280 Ok(())
281}
282
283#[context("Upgrading composefs")]
284pub(crate) async fn upgrade_composefs(
285 opts: UpgradeOpts,
286 storage: &Storage,
287 composefs: &BootedComposefs,
288) -> Result<()> {
289 if opts.download_only {
291 anyhow::bail!("--download-only is not yet supported for composefs backend");
292 }
293 if opts.from_downloaded {
294 anyhow::bail!("--from-downloaded is not yet supported for composefs backend");
295 }
296
297 let host = get_composefs_status(storage, composefs)
298 .await
299 .context("Getting composefs deployment status")?;
300
301 let mut booted_imgref = host
302 .spec
303 .image
304 .as_ref()
305 .ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
306
307 let repo = &*composefs.repo;
308
309 let (img_pulled, mut img_config) = is_image_pulled(&repo, booted_imgref).await?;
310 let booted_img_digest = img_config.manifest.config().digest().digest().to_owned();
311
312 let staged_image = host.status.staged.as_ref().and_then(|i| i.image.as_ref());
315
316 let do_upgrade_opts = DoUpgradeOpts {
317 soft_reboot: opts.soft_reboot,
318 apply: opts.apply,
319 };
320
321 if let Some(staged_image) = staged_image {
322 if staged_image.image_digest == booted_img_digest {
325 if opts.apply {
326 return crate::reboot::reboot();
327 }
328
329 println!("Update already staged. To apply update run `bootc update --apply`");
330
331 return Ok(());
332 }
333
334 booted_imgref = &staged_image.image;
338
339 let (img_pulled, staged_img_config) = is_image_pulled(&repo, booted_imgref).await?;
340 img_config = staged_img_config;
341
342 if let Some(cfg_verity) = img_pulled {
343 let action = validate_update(
344 storage,
345 composefs,
346 &host,
347 img_config.manifest.config().digest().digest(),
348 &cfg_verity,
349 false,
350 )?;
351
352 match action {
353 UpdateAction::Skip => {
354 println!("No changes in staged image: {booted_imgref:#}");
355 return Ok(());
356 }
357
358 UpdateAction::Proceed => {
359 return do_upgrade(
360 storage,
361 composefs,
362 &host,
363 booted_imgref,
364 &img_config,
365 &do_upgrade_opts,
366 )
367 .await;
368 }
369
370 UpdateAction::UpdateOrigin => {
371 anyhow::bail!("Updating origin not supported for update operation")
372 }
373 }
374 }
375 }
376
377 if let Some(cfg_verity) = img_pulled {
379 let action = validate_update(
380 storage,
381 composefs,
382 &host,
383 &booted_img_digest,
384 &cfg_verity,
385 false,
386 )?;
387
388 match action {
389 UpdateAction::Skip => {
390 println!("No changes in: {booted_imgref:#}");
391 return Ok(());
392 }
393
394 UpdateAction::Proceed => {
395 return do_upgrade(
396 storage,
397 composefs,
398 &host,
399 booted_imgref,
400 &img_config,
401 &do_upgrade_opts,
402 )
403 .await;
404 }
405
406 UpdateAction::UpdateOrigin => {
407 anyhow::bail!("Updating origin not supported for update operation")
408 }
409 }
410 }
411
412 if opts.check {
413 let current_manifest =
414 get_imginfo(storage, &*composefs.cmdline.digest, Some(booted_imgref)).await?;
415 let diff = ManifestDiff::new(¤t_manifest.manifest, &img_config.manifest);
416 diff.print();
417 return Ok(());
418 }
419
420 do_upgrade(
421 storage,
422 composefs,
423 &host,
424 booted_imgref,
425 &img_config,
426 &do_upgrade_opts,
427 )
428 .await?;
429
430 Ok(())
431}