1use std::borrow::Cow;
9use std::fmt::Display;
10use std::fmt::Write as _;
11use std::io::Write;
12use std::process::Command;
13use std::process::Stdio;
14
15use anyhow::Ok;
16use anyhow::{Context, Result};
17use bootc_utils::CommandRunExt;
18use camino::Utf8Path;
19use camino::Utf8PathBuf;
20use cap_std::fs::Dir;
21use cap_std_ext::cap_std;
22use clap::ValueEnum;
23use fn_error_context::context;
24use serde::{Deserialize, Serialize};
25
26use super::MountSpec;
27use super::RUN_BOOTC;
28use super::RW_KARG;
29use super::RootSetup;
30use super::State;
31use super::config::Filesystem;
32use crate::task::Task;
33use bootc_kernel_cmdline::utf8::Cmdline;
34#[cfg(feature = "install-to-disk")]
35use bootc_mount::is_mounted_in_pid1_mountns;
36
37#[cfg(feature = "install-to-disk")]
48fn use_discoverable_partitions(state: &State) -> bool {
49 if let Some(ref config) = state.install_config {
51 if let Some(v) = config.discoverable_partitions {
52 return v;
53 }
54 }
55 matches!(
57 state.config_opts.bootloader,
58 Some(crate::spec::Bootloader::Systemd)
59 )
60}
61
62pub(crate) const BOOTPN_SIZE_MB: u32 = 510;
64pub(crate) const EFIPN_SIZE_MB: u32 = 512;
65pub(crate) const CFS_EFIPN_SIZE_MB: u32 = 1024;
69#[cfg(feature = "install-to-disk")]
70pub(crate) const PREPBOOT_GUID: &str = "9E1A2D38-C612-4316-AA26-8B49521E5A8B";
71#[cfg(feature = "install-to-disk")]
72pub(crate) const PREPBOOT_LABEL: &str = "PowerPC-PReP-boot";
73
74#[derive(clap::ValueEnum, Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "kebab-case")]
76pub(crate) enum BlockSetup {
77 #[default]
78 Direct,
79 Tpm2Luks,
80}
81
82impl Display for BlockSetup {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 self.to_possible_value().unwrap().get_name().fmt(f)
85 }
86}
87
88#[derive(Debug, Clone, clap::Args, Serialize, Deserialize, PartialEq, Eq)]
90#[serde(rename_all = "kebab-case")]
91pub(crate) struct InstallBlockDeviceOpts {
92 pub(crate) device: Utf8PathBuf,
94
95 #[clap(long)]
97 #[serde(default)]
98 pub(crate) wipe: bool,
99
100 #[clap(long, value_enum)]
105 pub(crate) block_setup: Option<BlockSetup>,
106
107 #[clap(long, value_enum)]
109 pub(crate) filesystem: Option<Filesystem>,
110
111 #[clap(long)]
115 pub(crate) root_size: Option<String>,
116}
117
118impl BlockSetup {
119 pub(crate) fn requires_bootpart(&self) -> bool {
121 match self {
122 BlockSetup::Direct => false,
123 BlockSetup::Tpm2Luks => true,
124 }
125 }
126}
127
128#[cfg(feature = "install-to-disk")]
129fn mkfs<'a>(
130 dev: &str,
131 fs: Filesystem,
132 label: &str,
133 wipe: bool,
134 opts: impl IntoIterator<Item = &'a str>,
135) -> Result<uuid::Uuid> {
136 let devinfo = bootc_blockdev::list_dev(dev.into())?;
137 let size = ostree_ext::glib::format_size(devinfo.size);
138
139 let u = uuid::Uuid::new_v4();
141
142 let mut t = Task::new(
143 &format!("Creating {label} filesystem ({fs}) on device {dev} (size={size})"),
144 format!("mkfs.{fs}"),
145 );
146 match fs {
147 Filesystem::Xfs => {
148 if wipe {
149 t.cmd.arg("-f");
150 }
151 t.cmd.arg("-m");
152 t.cmd.arg(format!("uuid={u}"));
153 }
154 Filesystem::Btrfs | Filesystem::Ext4 => {
155 t.cmd.arg("-U");
156 t.cmd.arg(u.to_string());
157 }
158 };
159 t.cmd.args(["-L", label]);
161 t.cmd.args(opts);
162 t.cmd.arg(dev);
163 t.cmd.stdout(Stdio::null());
165 t.verbose().run()?;
167 Ok(u)
168}
169
170pub(crate) fn wipefs(dev: &Utf8Path) -> Result<()> {
171 println!("Wiping device {dev}");
172 Command::new("wipefs")
173 .args(["-a", dev.as_str()])
174 .run_inherited_with_cmd_context()
175}
176
177pub(crate) fn udev_settle() -> Result<()> {
178 std::thread::sleep(std::time::Duration::from_millis(200));
183
184 let st = super::run_in_host_mountns("udevadm")?
185 .arg("settle")
186 .status()?;
187 if !st.success() {
188 anyhow::bail!("Failed to run udevadm settle: {st:?}");
189 }
190 Ok(())
191}
192
193#[context("Creating rootfs")]
194#[cfg(feature = "install-to-disk")]
195pub(crate) fn install_create_rootfs(
196 state: &State,
197 opts: InstallBlockDeviceOpts,
198) -> Result<RootSetup> {
199 let install_config = state.install_config.as_ref();
200 let luks_name = "root";
201 let root_filesystem = opts
203 .filesystem
204 .or(install_config
205 .and_then(|c| c.filesystem_root())
206 .and_then(|r| r.fstype))
207 .ok_or_else(|| anyhow::anyhow!("No root filesystem specified"))?;
208 let mut device = bootc_blockdev::list_dev(&opts.device)?;
211
212 if is_mounted_in_pid1_mountns(&device.path())? {
214 anyhow::bail!("Device {} is mounted", device.path())
215 }
216
217 if opts.wipe {
219 let dev = &opts.device;
220 for child in device.children.iter().flatten() {
221 let child = child.path();
222 println!("Wiping {child}");
223 wipefs(Utf8Path::new(&child))?;
224 }
225 println!("Wiping {dev}");
226 wipefs(dev)?;
227 } else if device.has_children() {
228 anyhow::bail!(
229 "Detected existing partitions on {}; use e.g. `wipefs` or --wipe if you intend to overwrite",
230 opts.device
231 );
232 }
233
234 let run_bootc = Utf8Path::new(RUN_BOOTC);
235 let mntdir = run_bootc.join("mounts");
236 if mntdir.exists() {
237 std::fs::remove_dir_all(&mntdir)?;
238 }
239
240 let block_setup = if let Some(config) = install_config {
242 config.get_block_setup(opts.block_setup.as_ref().copied())?
243 } else if opts.filesystem.is_some() {
244 opts.block_setup.unwrap_or_default()
247 } else {
248 anyhow::bail!("No install configuration found, and no filesystem specified")
251 };
252 let serial = device.serial.as_deref().unwrap_or("<unknown>");
253 let model = device.model.as_deref().unwrap_or("<unknown>");
254 let discoverable = use_discoverable_partitions(state);
255 println!("Block setup: {block_setup}");
256 println!(" Size: {}", device.size);
257 println!(" Serial: {serial}");
258 println!(" Model: {model}");
259 println!(
260 " Partitions: {}",
261 if discoverable { "Discoverable" } else { "UUID" }
262 );
263
264 let root_size = opts
265 .root_size
266 .as_deref()
267 .map(bootc_blockdev::parse_size_mib)
268 .transpose()
269 .context("Parsing root size")?;
270
271 let sepolicy = state.load_policy()?;
273 let sepolicy = sepolicy.as_ref();
274
275 let physical_root_path = mntdir.join("rootfs");
278 std::fs::create_dir_all(&physical_root_path)?;
279 let bootfs = mntdir.join("boot");
280 std::fs::create_dir_all(bootfs)?;
281
282 let mut partno = 0;
284 let mut partitioning_buf = String::new();
285 writeln!(partitioning_buf, "label: gpt")?;
286 let random_label = uuid::Uuid::new_v4();
287 writeln!(&mut partitioning_buf, "label-id: {random_label}")?;
288 if cfg!(target_arch = "x86_64") {
289 partno += 1;
290 writeln!(
291 &mut partitioning_buf,
292 r#"size=1MiB, bootable, type=21686148-6449-6E6F-744E-656564454649, name="BIOS-BOOT""#
293 )?;
294 } else if cfg!(target_arch = "powerpc64") {
295 partno += 1;
297 let label = PREPBOOT_LABEL;
298 let uuid = PREPBOOT_GUID;
299 writeln!(
300 &mut partitioning_buf,
301 r#"size=4MiB, bootable, type={uuid}, name="{label}""#
302 )?;
303 } else if cfg!(any(target_arch = "aarch64", target_arch = "s390x")) {
304 } else {
306 anyhow::bail!("Unsupported architecture: {}", std::env::consts::ARCH);
307 }
308
309 let esp_partno = if super::ARCH_USES_EFI {
310 let esp_guid = crate::discoverable_partition_specification::ESP;
311 partno += 1;
312
313 let esp_size = if state.composefs_options.composefs_backend {
314 CFS_EFIPN_SIZE_MB
315 } else {
316 EFIPN_SIZE_MB
317 };
318
319 writeln!(
320 &mut partitioning_buf,
321 r#"size={esp_size}MiB, type={esp_guid}, name="EFI-SYSTEM""#
322 )?;
323 Some(partno)
324 } else {
325 None
326 };
327
328 let boot_partno = if block_setup.requires_bootpart() {
332 partno += 1;
333 writeln!(
334 &mut partitioning_buf,
335 r#"size={BOOTPN_SIZE_MB}MiB, name="boot""#
336 )?;
337 Some(partno)
338 } else {
339 None
340 };
341 let rootpn = partno + 1;
342 let root_size = root_size
343 .map(|v| Cow::Owned(format!("size={v}MiB, ")))
344 .unwrap_or_else(|| Cow::Borrowed(""));
345 let rootpart_uuid =
346 uuid::Uuid::parse_str(crate::discoverable_partition_specification::this_arch_root())?;
347 writeln!(
348 &mut partitioning_buf,
349 r#"{root_size}type={rootpart_uuid}, name="root""#
350 )?;
351 tracing::debug!("Partitioning: {partitioning_buf}");
352 Task::new("Initializing partitions", "sfdisk")
353 .arg("--wipe=always")
354 .arg(device.path())
355 .quiet()
356 .run_with_stdin_buf(Some(partitioning_buf.as_bytes()))
357 .context("Failed to run sfdisk")?;
358 tracing::debug!("Created partition table");
359
360 udev_settle()?;
363
364 device.refresh()?;
366
367 let root_device = device.find_device_by_partno(rootpn)?;
368 let expected_parttype = crate::discoverable_partition_specification::this_arch_root();
370 if !root_device
371 .parttype
372 .as_ref()
373 .is_some_and(|pt| pt.eq_ignore_ascii_case(expected_parttype))
374 {
375 anyhow::bail!(
376 "root partition {rootpn} has type {}; expected {expected_parttype}",
377 root_device.parttype.as_deref().unwrap_or("<none>")
378 );
379 }
380 let (rootdev_path, root_blockdev_kargs) = match block_setup {
381 BlockSetup::Direct => (root_device.path(), None),
382 BlockSetup::Tpm2Luks => {
383 let uuid = uuid::Uuid::new_v4().to_string();
384 let dummy_passphrase = uuid::Uuid::new_v4().to_string();
386 let mut tmp_keyfile = tempfile::NamedTempFile::new()?;
387 tmp_keyfile.write_all(dummy_passphrase.as_bytes())?;
388 tmp_keyfile.flush()?;
389 let tmp_keyfile = tmp_keyfile.path();
390 let dummy_passphrase_input = Some(dummy_passphrase.as_bytes());
391
392 let root_devpath = root_device.path();
393
394 Task::new("Initializing LUKS for root", "cryptsetup")
395 .args(["luksFormat", "--uuid", uuid.as_str(), "--key-file"])
396 .args([tmp_keyfile])
397 .arg(&root_devpath)
398 .run()?;
399 Task::new("Enrolling root device with TPM", "systemd-cryptenroll")
402 .args(["--wipe-slot=all", "--tpm2-device=auto", "--unlock-key-file"])
403 .args([tmp_keyfile])
404 .arg(&root_devpath)
405 .verbose()
406 .run_with_stdin_buf(dummy_passphrase_input)?;
407 Task::new("Opening root LUKS device", "cryptsetup")
408 .args(["luksOpen", &root_devpath, luks_name])
409 .run()?;
410 let rootdev = format!("/dev/mapper/{luks_name}");
411 let kargs = vec![
412 format!("luks.uuid={uuid}"),
413 format!("luks.options=tpm2-device=auto,headless=true"),
414 ];
415 (rootdev, Some(kargs))
416 }
417 };
418
419 let bootdev = if let Some(bootpn) = boot_partno {
421 Some(device.find_device_by_partno(bootpn)?)
422 } else {
423 None
424 };
425 let boot_uuid = if let Some(bootdev) = bootdev {
426 Some(
427 mkfs(&bootdev.path(), root_filesystem, "boot", opts.wipe, [])
428 .context("Initializing /boot")?,
429 )
430 } else {
431 None
432 };
433
434 let mkfs_options = match root_filesystem {
436 Filesystem::Ext4 => ["-O", "verity"].as_slice(),
437 _ => [].as_slice(),
438 };
439
440 let root_uuid = mkfs(
442 &rootdev_path,
443 root_filesystem,
444 "root",
445 opts.wipe,
446 mkfs_options.iter().copied(),
447 )?;
448 let bootsrc = boot_uuid.as_ref().map(|uuid| format!("UUID={uuid}"));
449 let bootarg = bootsrc.as_deref().map(|bootsrc| format!("boot={bootsrc}"));
450 let boot = bootsrc.map(|bootsrc| MountSpec {
451 source: bootsrc,
452 target: "/boot".into(),
453 fstype: MountSpec::AUTO.into(),
454 options: Some("ro".into()),
455 });
456
457 let mut kargs = Cmdline::new();
458
459 if let Some(root_blockdev_kargs) = root_blockdev_kargs {
461 for karg in root_blockdev_kargs {
462 kargs.extend(&Cmdline::from(karg.as_str()));
463 }
464 }
465
466 if discoverable {
469 kargs.extend(&Cmdline::from(RW_KARG));
470 } else {
471 let rootarg = format!("root=UUID={root_uuid}");
472 kargs.extend(&Cmdline::from(format!("{rootarg} {RW_KARG}")));
473 }
474
475 if let Some(bootarg) = bootarg {
477 kargs.extend(&Cmdline::from(bootarg.as_str()));
478 }
479
480 if let Some(cli_kargs) = state.config_opts.karg.as_ref() {
482 for karg in cli_kargs {
483 kargs.extend(karg);
484 }
485 }
486
487 let fstype = &root_filesystem.to_string();
488 bootc_mount::mount_typed(&rootdev_path, fstype, &physical_root_path)?;
489 let target_rootfs = Dir::open_ambient_dir(&physical_root_path, cap_std::ambient_authority())?;
490 crate::lsm::ensure_dir_labeled(&target_rootfs, "", Some("/".into()), 0o755.into(), sepolicy)?;
491 let physical_root = Dir::open_ambient_dir(&physical_root_path, cap_std::ambient_authority())?;
492 let bootfs = physical_root_path.join("boot");
493 crate::lsm::ensure_dir_labeled(&target_rootfs, "boot", None, 0o755.into(), sepolicy)?;
495 if let Some(bootdev) = bootdev {
496 bootc_mount::mount_typed(&bootdev.path(), fstype, &bootfs)?;
497 }
498 crate::lsm::ensure_dir_labeled(&target_rootfs, "boot", None, 0o755.into(), sepolicy)?;
500
501 if let Some(esp_partno) = esp_partno {
503 let espdev = device.find_device_by_partno(esp_partno)?;
504 Task::new("Creating ESP filesystem", "mkfs.fat")
505 .args([&espdev.path(), "-n", "EFI-SYSTEM"])
506 .verbose()
507 .quiet_output()
508 .run()?;
509 let efifs_path = bootfs.join(crate::bootloader::EFI_DIR);
510 std::fs::create_dir(&efifs_path).context("Creating efi dir")?;
511 }
512
513 let luks_device = match block_setup {
514 BlockSetup::Direct => None,
515 BlockSetup::Tpm2Luks => Some(luks_name.to_string()),
516 };
517 Ok(RootSetup {
518 luks_device,
519 device_info: device,
520 physical_root_path,
521 physical_root,
522 target_root_path: None,
523 rootfs_uuid: Some(root_uuid.to_string()),
524 boot,
525 kargs,
526 skip_finalize: false,
527 })
528}