|
| 1 | +use std::path::PathBuf; |
| 2 | +use std::{fmt::Write, fs::create_dir_all}; |
| 3 | + |
| 4 | +use anyhow::{anyhow, Context, Result}; |
| 5 | +use cap_std_ext::cap_std::fs::Dir; |
| 6 | +use cap_std_ext::{cap_std, dirext::CapStdExtDirExt}; |
| 7 | +use fn_error_context::context; |
| 8 | +use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags}; |
| 9 | + |
| 10 | +use crate::bootc_composefs::boot::BootType; |
| 11 | +use crate::bootc_composefs::status::{composefs_deployment_status, get_sorted_bls_boot_entries}; |
| 12 | +use crate::{ |
| 13 | + bootc_composefs::{boot::get_efi_uuid_source, status::get_sorted_uki_boot_entries}, |
| 14 | + composefs_consts::{ |
| 15 | + BOOT_LOADER_ENTRIES, ROLLBACK_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_ROLLBACK, |
| 16 | + }, |
| 17 | + spec::BootOrder, |
| 18 | +}; |
| 19 | + |
| 20 | +#[context("Rolling back UKI")] |
| 21 | +pub(crate) fn rollback_composefs_uki() -> Result<()> { |
| 22 | + let user_cfg_path = PathBuf::from("/sysroot/boot/grub2"); |
| 23 | + |
| 24 | + let mut str = String::new(); |
| 25 | + let boot_dir = |
| 26 | + cap_std::fs::Dir::open_ambient_dir("/sysroot/boot", cap_std::ambient_authority()) |
| 27 | + .context("Opening boot dir")?; |
| 28 | + let mut menuentries = |
| 29 | + get_sorted_uki_boot_entries(&boot_dir, &mut str).context("Getting UKI boot entries")?; |
| 30 | + |
| 31 | + // TODO(Johan-Liebert): Currently assuming there are only two deployments |
| 32 | + assert!(menuentries.len() == 2); |
| 33 | + |
| 34 | + let (first, second) = menuentries.split_at_mut(1); |
| 35 | + std::mem::swap(&mut first[0], &mut second[0]); |
| 36 | + |
| 37 | + let mut buffer = get_efi_uuid_source(); |
| 38 | + |
| 39 | + for entry in menuentries { |
| 40 | + write!(buffer, "{entry}")?; |
| 41 | + } |
| 42 | + |
| 43 | + let entries_dir = |
| 44 | + cap_std::fs::Dir::open_ambient_dir(&user_cfg_path, cap_std::ambient_authority()) |
| 45 | + .with_context(|| format!("Opening {user_cfg_path:?}"))?; |
| 46 | + |
| 47 | + entries_dir |
| 48 | + .atomic_write(USER_CFG_ROLLBACK, buffer) |
| 49 | + .with_context(|| format!("Writing to {USER_CFG_ROLLBACK}"))?; |
| 50 | + |
| 51 | + tracing::debug!("Atomically exchanging for {USER_CFG_ROLLBACK} and {USER_CFG}"); |
| 52 | + renameat_with( |
| 53 | + &entries_dir, |
| 54 | + USER_CFG_ROLLBACK, |
| 55 | + &entries_dir, |
| 56 | + USER_CFG, |
| 57 | + RenameFlags::EXCHANGE, |
| 58 | + ) |
| 59 | + .context("renameat")?; |
| 60 | + |
| 61 | + tracing::debug!("Removing {USER_CFG_ROLLBACK}"); |
| 62 | + rustix::fs::unlinkat(&entries_dir, USER_CFG_ROLLBACK, AtFlags::empty()).context("unlinkat")?; |
| 63 | + |
| 64 | + tracing::debug!("Syncing to disk"); |
| 65 | + fsync( |
| 66 | + entries_dir |
| 67 | + .reopen_as_ownedfd() |
| 68 | + .with_context(|| format!("Reopening {user_cfg_path:?} as owned fd"))?, |
| 69 | + ) |
| 70 | + .with_context(|| format!("fsync {user_cfg_path:?}"))?; |
| 71 | + |
| 72 | + Ok(()) |
| 73 | +} |
| 74 | + |
| 75 | +#[context("Rolling back BLS")] |
| 76 | +pub(crate) fn rollback_composefs_bls() -> Result<()> { |
| 77 | + let boot_dir = |
| 78 | + cap_std::fs::Dir::open_ambient_dir("/sysroot/boot", cap_std::ambient_authority()) |
| 79 | + .context("Opening boot dir")?; |
| 80 | + |
| 81 | + // Sort in descending order as that's the order they're shown on the boot screen |
| 82 | + // After this: |
| 83 | + // all_configs[0] -> booted depl |
| 84 | + // all_configs[1] -> rollback depl |
| 85 | + let mut all_configs = get_sorted_bls_boot_entries(&boot_dir, false)?; |
| 86 | + |
| 87 | + // Update the indicies so that they're swapped |
| 88 | + for (idx, cfg) in all_configs.iter_mut().enumerate() { |
| 89 | + cfg.sort_key = Some(idx.to_string()); |
| 90 | + } |
| 91 | + |
| 92 | + // TODO(Johan-Liebert): Currently assuming there are only two deployments |
| 93 | + assert!(all_configs.len() == 2); |
| 94 | + |
| 95 | + // Write these |
| 96 | + let dir_path = PathBuf::from(format!( |
| 97 | + "/sysroot/boot/loader/{ROLLBACK_BOOT_LOADER_ENTRIES}", |
| 98 | + )); |
| 99 | + create_dir_all(&dir_path).with_context(|| format!("Failed to create dir: {dir_path:?}"))?; |
| 100 | + |
| 101 | + let rollback_entries_dir = |
| 102 | + cap_std::fs::Dir::open_ambient_dir(&dir_path, cap_std::ambient_authority()) |
| 103 | + .with_context(|| format!("Opening {dir_path:?}"))?; |
| 104 | + |
| 105 | + // Write the BLS configs in there |
| 106 | + for cfg in all_configs { |
| 107 | + // SAFETY: We set sort_key above |
| 108 | + let file_name = format!("bootc-composefs-{}.conf", cfg.sort_key.as_ref().unwrap()); |
| 109 | + |
| 110 | + rollback_entries_dir |
| 111 | + .atomic_write(&file_name, cfg.to_string()) |
| 112 | + .with_context(|| format!("Writing to {file_name}"))?; |
| 113 | + } |
| 114 | + |
| 115 | + // Should we sync after every write? |
| 116 | + fsync( |
| 117 | + rollback_entries_dir |
| 118 | + .reopen_as_ownedfd() |
| 119 | + .with_context(|| format!("Reopening {dir_path:?} as owned fd"))?, |
| 120 | + ) |
| 121 | + .with_context(|| format!("fsync {dir_path:?}"))?; |
| 122 | + |
| 123 | + // Atomically exchange "entries" <-> "entries.rollback" |
| 124 | + let dir = Dir::open_ambient_dir("/sysroot/boot/loader", cap_std::ambient_authority()) |
| 125 | + .context("Opening loader dir")?; |
| 126 | + |
| 127 | + tracing::debug!( |
| 128 | + "Atomically exchanging for {ROLLBACK_BOOT_LOADER_ENTRIES} and {BOOT_LOADER_ENTRIES}" |
| 129 | + ); |
| 130 | + renameat_with( |
| 131 | + &dir, |
| 132 | + ROLLBACK_BOOT_LOADER_ENTRIES, |
| 133 | + &dir, |
| 134 | + BOOT_LOADER_ENTRIES, |
| 135 | + RenameFlags::EXCHANGE, |
| 136 | + ) |
| 137 | + .context("renameat")?; |
| 138 | + |
| 139 | + tracing::debug!("Removing {ROLLBACK_BOOT_LOADER_ENTRIES}"); |
| 140 | + rustix::fs::unlinkat(&dir, ROLLBACK_BOOT_LOADER_ENTRIES, AtFlags::empty()) |
| 141 | + .context("unlinkat")?; |
| 142 | + |
| 143 | + tracing::debug!("Syncing to disk"); |
| 144 | + fsync( |
| 145 | + dir.reopen_as_ownedfd() |
| 146 | + .with_context(|| format!("Reopening /sysroot/boot/loader as owned fd"))?, |
| 147 | + ) |
| 148 | + .context("fsync")?; |
| 149 | + |
| 150 | + Ok(()) |
| 151 | +} |
| 152 | + |
| 153 | +#[context("Rolling back composefs")] |
| 154 | +pub(crate) async fn composefs_rollback() -> Result<()> { |
| 155 | + let host = composefs_deployment_status().await?; |
| 156 | + |
| 157 | + let new_spec = { |
| 158 | + let mut new_spec = host.spec.clone(); |
| 159 | + new_spec.boot_order = new_spec.boot_order.swap(); |
| 160 | + new_spec |
| 161 | + }; |
| 162 | + |
| 163 | + // Just to be sure |
| 164 | + host.spec.verify_transition(&new_spec)?; |
| 165 | + |
| 166 | + let reverting = new_spec.boot_order == BootOrder::Default; |
| 167 | + if reverting { |
| 168 | + println!("notice: Reverting queued rollback state"); |
| 169 | + } |
| 170 | + |
| 171 | + let rollback_status = host |
| 172 | + .status |
| 173 | + .rollback |
| 174 | + .ok_or_else(|| anyhow!("No rollback available"))?; |
| 175 | + |
| 176 | + // TODO: Handle staged deployment |
| 177 | + // Ostree will drop any staged deployment on rollback but will keep it if it is the first item |
| 178 | + // in the new deployment list |
| 179 | + let Some(rollback_composefs_entry) = &rollback_status.composefs else { |
| 180 | + anyhow::bail!("Rollback deployment not a composefs deployment") |
| 181 | + }; |
| 182 | + |
| 183 | + match rollback_composefs_entry.boot_type { |
| 184 | + BootType::Bls => rollback_composefs_bls(), |
| 185 | + BootType::Uki => rollback_composefs_uki(), |
| 186 | + }?; |
| 187 | + |
| 188 | + if reverting { |
| 189 | + println!("Next boot: current deployment"); |
| 190 | + } else { |
| 191 | + println!("Next boot: rollback deployment"); |
| 192 | + } |
| 193 | + |
| 194 | + Ok(()) |
| 195 | +} |
0 commit comments