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
37pub(crate) const BOOTPN_SIZE_MB: u32 = 510;
39pub(crate) const EFIPN_SIZE_MB: u32 = 512;
40pub(crate) const CFS_EFIPN_SIZE_MB: u32 = 1024;
44#[cfg(feature = "install-to-disk")]
45pub(crate) const PREPBOOT_GUID: &str = "9E1A2D38-C612-4316-AA26-8B49521E5A8B";
46#[cfg(feature = "install-to-disk")]
47pub(crate) const PREPBOOT_LABEL: &str = "PowerPC-PReP-boot";
48
49#[derive(clap::ValueEnum, Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "kebab-case")]
51pub(crate) enum BlockSetup {
52 #[default]
53 Direct,
54 Tpm2Luks,
55}
56
57impl Display for BlockSetup {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 self.to_possible_value().unwrap().get_name().fmt(f)
60 }
61}
62
63#[derive(Debug, Clone, clap::Args, Serialize, Deserialize, PartialEq, Eq)]
65#[serde(rename_all = "kebab-case")]
66pub(crate) struct InstallBlockDeviceOpts {
67 pub(crate) device: Utf8PathBuf,
69
70 #[clap(long)]
72 #[serde(default)]
73 pub(crate) wipe: bool,
74
75 #[clap(long, value_enum)]
80 pub(crate) block_setup: Option<BlockSetup>,
81
82 #[clap(long, value_enum)]
84 pub(crate) filesystem: Option<Filesystem>,
85
86 #[clap(long)]
90 pub(crate) root_size: Option<String>,
91}
92
93impl BlockSetup {
94 pub(crate) fn requires_bootpart(&self) -> bool {
96 match self {
97 BlockSetup::Direct => false,
98 BlockSetup::Tpm2Luks => true,
99 }
100 }
101}
102
103#[cfg(feature = "install-to-disk")]
104fn mkfs<'a>(
105 dev: &str,
106 fs: Filesystem,
107 label: &str,
108 wipe: bool,
109 opts: impl IntoIterator<Item = &'a str>,
110) -> Result<uuid::Uuid> {
111 let devinfo = bootc_blockdev::list_dev(dev.into())?;
112 let size = ostree_ext::glib::format_size(devinfo.size);
113
114 let u = uuid::Uuid::new_v4();
116
117 let mut t = Task::new(
118 &format!("Creating {label} filesystem ({fs}) on device {dev} (size={size})"),
119 format!("mkfs.{fs}"),
120 );
121 match fs {
122 Filesystem::Xfs => {
123 if wipe {
124 t.cmd.arg("-f");
125 }
126 t.cmd.arg("-m");
127 t.cmd.arg(format!("uuid={u}"));
128 }
129 Filesystem::Btrfs | Filesystem::Ext4 => {
130 t.cmd.arg("-U");
131 t.cmd.arg(u.to_string());
132 }
133 };
134 t.cmd.args(["-L", label]);
136 t.cmd.args(opts);
137 t.cmd.arg(dev);
138 t.cmd.stdout(Stdio::null());
140 t.verbose().run()?;
142 Ok(u)
143}
144
145pub(crate) fn wipefs(dev: &Utf8Path) -> Result<()> {
146 println!("Wiping device {dev}");
147 Command::new("wipefs")
148 .args(["-a", dev.as_str()])
149 .run_inherited_with_cmd_context()
150}
151
152pub(crate) fn udev_settle() -> Result<()> {
153 std::thread::sleep(std::time::Duration::from_millis(200));
158
159 let st = super::run_in_host_mountns("udevadm")?
160 .arg("settle")
161 .status()?;
162 if !st.success() {
163 anyhow::bail!("Failed to run udevadm settle: {st:?}");
164 }
165 Ok(())
166}
167
168#[context("Creating rootfs")]
169#[cfg(feature = "install-to-disk")]
170pub(crate) fn install_create_rootfs(
171 state: &State,
172 opts: InstallBlockDeviceOpts,
173) -> Result<RootSetup> {
174 let install_config = state.install_config.as_ref();
175 let luks_name = "root";
176 let root_filesystem = opts
178 .filesystem
179 .or(install_config
180 .and_then(|c| c.filesystem_root())
181 .and_then(|r| r.fstype))
182 .ok_or_else(|| anyhow::anyhow!("No root filesystem specified"))?;
183 let device = bootc_blockdev::list_dev(&opts.device)?;
186 let devpath: Utf8PathBuf = device.path().into();
188
189 if is_mounted_in_pid1_mountns(&device.path())? {
191 anyhow::bail!("Device {} is mounted", device.path())
192 }
193
194 if opts.wipe {
196 let dev = &opts.device;
197 for child in device.children.iter().flatten() {
198 let child = child.path();
199 println!("Wiping {child}");
200 wipefs(Utf8Path::new(&child))?;
201 }
202 println!("Wiping {dev}");
203 wipefs(dev)?;
204 } else if device.has_children() {
205 anyhow::bail!(
206 "Detected existing partitions on {}; use e.g. `wipefs` or --wipe if you intend to overwrite",
207 opts.device
208 );
209 }
210
211 let run_bootc = Utf8Path::new(RUN_BOOTC);
212 let mntdir = run_bootc.join("mounts");
213 if mntdir.exists() {
214 std::fs::remove_dir_all(&mntdir)?;
215 }
216
217 let block_setup = if let Some(config) = install_config {
219 config.get_block_setup(opts.block_setup.as_ref().copied())?
220 } else if opts.filesystem.is_some() {
221 opts.block_setup.unwrap_or_default()
224 } else {
225 anyhow::bail!("No install configuration found, and no filesystem specified")
228 };
229 let serial = device.serial.as_deref().unwrap_or("<unknown>");
230 let model = device.model.as_deref().unwrap_or("<unknown>");
231 println!("Block setup: {block_setup}");
232 println!(" Size: {}", device.size);
233 println!(" Serial: {serial}");
234 println!(" Model: {model}");
235
236 let root_size = opts
237 .root_size
238 .as_deref()
239 .map(bootc_blockdev::parse_size_mib)
240 .transpose()
241 .context("Parsing root size")?;
242
243 let sepolicy = state.load_policy()?;
245 let sepolicy = sepolicy.as_ref();
246
247 let physical_root_path = mntdir.join("rootfs");
250 std::fs::create_dir_all(&physical_root_path)?;
251 let bootfs = mntdir.join("boot");
252 std::fs::create_dir_all(bootfs)?;
253
254 let mut partno = 0;
256 let mut partitioning_buf = String::new();
257 writeln!(partitioning_buf, "label: gpt")?;
258 let random_label = uuid::Uuid::new_v4();
259 writeln!(&mut partitioning_buf, "label-id: {random_label}")?;
260 if cfg!(target_arch = "x86_64") {
261 partno += 1;
262 writeln!(
263 &mut partitioning_buf,
264 r#"size=1MiB, bootable, type=21686148-6449-6E6F-744E-656564454649, name="BIOS-BOOT""#
265 )?;
266 } else if cfg!(target_arch = "powerpc64") {
267 partno += 1;
269 let label = PREPBOOT_LABEL;
270 let uuid = PREPBOOT_GUID;
271 writeln!(
272 &mut partitioning_buf,
273 r#"size=4MiB, bootable, type={uuid}, name="{label}""#
274 )?;
275 } else if cfg!(any(target_arch = "aarch64", target_arch = "s390x")) {
276 } else {
278 anyhow::bail!("Unsupported architecture: {}", std::env::consts::ARCH);
279 }
280
281 let esp_partno = if super::ARCH_USES_EFI {
282 let esp_guid = crate::discoverable_partition_specification::ESP;
283 partno += 1;
284
285 let esp_size = if state.composefs_options.composefs_backend {
286 CFS_EFIPN_SIZE_MB
287 } else {
288 EFIPN_SIZE_MB
289 };
290
291 writeln!(
292 &mut partitioning_buf,
293 r#"size={esp_size}MiB, type={esp_guid}, name="EFI-SYSTEM""#
294 )?;
295 Some(partno)
296 } else {
297 None
298 };
299
300 let boot_partno = if block_setup.requires_bootpart() {
304 partno += 1;
305 writeln!(
306 &mut partitioning_buf,
307 r#"size={BOOTPN_SIZE_MB}MiB, name="boot""#
308 )?;
309 Some(partno)
310 } else {
311 None
312 };
313 let rootpn = partno + 1;
314 let root_size = root_size
315 .map(|v| Cow::Owned(format!("size={v}MiB, ")))
316 .unwrap_or_else(|| Cow::Borrowed(""));
317 let rootpart_uuid =
318 uuid::Uuid::parse_str(crate::discoverable_partition_specification::this_arch_root())?;
319 writeln!(
320 &mut partitioning_buf,
321 r#"{root_size}type={rootpart_uuid}, name="root""#
322 )?;
323 tracing::debug!("Partitioning: {partitioning_buf}");
324 Task::new("Initializing partitions", "sfdisk")
325 .arg("--wipe=always")
326 .arg(device.path())
327 .quiet()
328 .run_with_stdin_buf(Some(partitioning_buf.as_bytes()))
329 .context("Failed to run sfdisk")?;
330 tracing::debug!("Created partition table");
331
332 udev_settle()?;
335
336 let base_partitions = &bootc_blockdev::partitions_of(&devpath)?;
338
339 let root_partition = base_partitions.find_partno(rootpn)?;
340 let expected_parttype = crate::discoverable_partition_specification::this_arch_root();
342 if !root_partition
343 .parttype
344 .eq_ignore_ascii_case(expected_parttype)
345 {
346 anyhow::bail!(
347 "root partition {rootpn} has type {}; expected {expected_parttype}",
348 root_partition.parttype.as_str()
349 );
350 }
351 let (rootdev, root_blockdev_kargs) = match block_setup {
352 BlockSetup::Direct => (root_partition.node.to_owned(), None),
353 BlockSetup::Tpm2Luks => {
354 let uuid = uuid::Uuid::new_v4().to_string();
355 let dummy_passphrase = uuid::Uuid::new_v4().to_string();
357 let mut tmp_keyfile = tempfile::NamedTempFile::new()?;
358 tmp_keyfile.write_all(dummy_passphrase.as_bytes())?;
359 tmp_keyfile.flush()?;
360 let tmp_keyfile = tmp_keyfile.path();
361 let dummy_passphrase_input = Some(dummy_passphrase.as_bytes());
362
363 let root_devpath = root_partition.path();
364
365 Task::new("Initializing LUKS for root", "cryptsetup")
366 .args(["luksFormat", "--uuid", uuid.as_str(), "--key-file"])
367 .args([tmp_keyfile])
368 .args([root_devpath])
369 .run()?;
370 Task::new("Enrolling root device with TPM", "systemd-cryptenroll")
373 .args(["--wipe-slot=all", "--tpm2-device=auto", "--unlock-key-file"])
374 .args([tmp_keyfile])
375 .args([root_devpath])
376 .verbose()
377 .run_with_stdin_buf(dummy_passphrase_input)?;
378 Task::new("Opening root LUKS device", "cryptsetup")
379 .args(["luksOpen", root_devpath.as_str(), luks_name])
380 .run()?;
381 let rootdev = format!("/dev/mapper/{luks_name}");
382 let kargs = vec![
383 format!("luks.uuid={uuid}"),
384 format!("luks.options=tpm2-device=auto,headless=true"),
385 ];
386 (rootdev, Some(kargs))
387 }
388 };
389
390 let bootdev = if let Some(bootpn) = boot_partno {
392 Some(base_partitions.find_partno(bootpn)?)
393 } else {
394 None
395 };
396 let boot_uuid = if let Some(bootdev) = bootdev {
397 Some(
398 mkfs(
399 bootdev.node.as_str(),
400 root_filesystem,
401 "boot",
402 opts.wipe,
403 [],
404 )
405 .context("Initializing /boot")?,
406 )
407 } else {
408 None
409 };
410
411 let mkfs_options = match root_filesystem {
413 Filesystem::Ext4 => ["-O", "verity"].as_slice(),
414 _ => [].as_slice(),
415 };
416
417 let root_uuid = mkfs(
419 &rootdev,
420 root_filesystem,
421 "root",
422 opts.wipe,
423 mkfs_options.iter().copied(),
424 )?;
425 let rootarg = format!("root=UUID={root_uuid}");
426 let bootsrc = boot_uuid.as_ref().map(|uuid| format!("UUID={uuid}"));
427 let bootarg = bootsrc.as_deref().map(|bootsrc| format!("boot={bootsrc}"));
428 let boot = bootsrc.map(|bootsrc| MountSpec {
429 source: bootsrc,
430 target: "/boot".into(),
431 fstype: MountSpec::AUTO.into(),
432 options: Some("ro".into()),
433 });
434
435 let mut kargs = Cmdline::new();
436
437 if let Some(root_blockdev_kargs) = root_blockdev_kargs {
439 for karg in root_blockdev_kargs {
440 kargs.extend(&Cmdline::from(karg.as_str()));
441 }
442 }
443
444 kargs.extend(&Cmdline::from(format!("{rootarg} {RW_KARG}")));
446
447 if let Some(bootarg) = bootarg {
449 kargs.extend(&Cmdline::from(bootarg.as_str()));
450 }
451
452 if let Some(cli_kargs) = state.config_opts.karg.as_ref() {
454 for karg in cli_kargs {
455 kargs.extend(karg);
456 }
457 }
458
459 bootc_mount::mount(&rootdev, &physical_root_path)?;
460 let target_rootfs = Dir::open_ambient_dir(&physical_root_path, cap_std::ambient_authority())?;
461 crate::lsm::ensure_dir_labeled(&target_rootfs, "", Some("/".into()), 0o755.into(), sepolicy)?;
462 let physical_root = Dir::open_ambient_dir(&physical_root_path, cap_std::ambient_authority())?;
463 let bootfs = physical_root_path.join("boot");
464 crate::lsm::ensure_dir_labeled(&target_rootfs, "boot", None, 0o755.into(), sepolicy)?;
466 if let Some(bootdev) = bootdev {
467 bootc_mount::mount(bootdev.node.as_str(), &bootfs)?;
468 }
469 crate::lsm::ensure_dir_labeled(&target_rootfs, "boot", None, 0o755.into(), sepolicy)?;
471
472 if let Some(esp_partno) = esp_partno {
474 let espdev = base_partitions.find_partno(esp_partno)?;
475 Task::new("Creating ESP filesystem", "mkfs.fat")
476 .args([espdev.node.as_str(), "-n", "EFI-SYSTEM"])
477 .verbose()
478 .quiet_output()
479 .run()?;
480 let efifs_path = bootfs.join(crate::bootloader::EFI_DIR);
481 std::fs::create_dir(&efifs_path).context("Creating efi dir")?;
482 }
483
484 let luks_device = match block_setup {
485 BlockSetup::Direct => None,
486 BlockSetup::Tpm2Luks => Some(luks_name.to_string()),
487 };
488 let device_info = bootc_blockdev::partitions_of(&devpath)?;
489 Ok(RootSetup {
490 luks_device,
491 device_info,
492 physical_root_path,
493 physical_root,
494 target_root_path: None,
495 rootfs_uuid: Some(root_uuid.to_string()),
496 boot,
497 kargs,
498 skip_finalize: false,
499 })
500}