bootc_lib/install/
baseline.rs

1//! # The baseline installer
2//!
3//! This module handles creation of simple root filesystem setups.  At the current time
4//! it's very simple - just a direct filesystem (e.g. xfs, ext4, btrfs etc.).  It is
5//! intended to add opinionated handling of TPM2-bound LUKS too.  But that's about it;
6//! other more complex flows should set things up externally and use `bootc install to-filesystem`.
7
8use 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// This ensures we end up under 512 to be small-sized.
38pub(crate) const BOOTPN_SIZE_MB: u32 = 510;
39pub(crate) const EFIPN_SIZE_MB: u32 = 512;
40/// EFI Partition size for composefs installations
41/// We need more space than ostree as we have UKIs and UKI addons
42/// We might also need to store UKIs for pinned deployments
43pub(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/// Options for installing to a block device
64#[derive(Debug, Clone, clap::Args, Serialize, Deserialize, PartialEq, Eq)]
65#[serde(rename_all = "kebab-case")]
66pub(crate) struct InstallBlockDeviceOpts {
67    /// Target block device for installation.  The entire device will be wiped.
68    pub(crate) device: Utf8PathBuf,
69
70    /// Automatically wipe all existing data on device
71    #[clap(long)]
72    #[serde(default)]
73    pub(crate) wipe: bool,
74
75    /// Target root block device setup.
76    ///
77    /// direct: Filesystem written directly to block device
78    /// tpm2-luks: Bind unlock of filesystem to presence of the default tpm2 device.
79    #[clap(long, value_enum)]
80    pub(crate) block_setup: Option<BlockSetup>,
81
82    /// Target root filesystem type.
83    #[clap(long, value_enum)]
84    pub(crate) filesystem: Option<Filesystem>,
85
86    /// Size of the root partition (default specifier: M).  Allowed specifiers: M (mebibytes), G (gibibytes), T (tebibytes).
87    ///
88    /// By default, all remaining space on the disk will be used.
89    #[clap(long)]
90    pub(crate) root_size: Option<String>,
91}
92
93impl BlockSetup {
94    /// Returns true if the block setup requires a separate /boot aka XBOOTLDR partition.
95    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    // Generate a random UUID for the filesystem
115    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    // Today all the above mkfs commands take -L
135    t.cmd.args(["-L", label]);
136    t.cmd.args(opts);
137    t.cmd.arg(dev);
138    // All the mkfs commands are unnecessarily noisy by default
139    t.cmd.stdout(Stdio::null());
140    // But this one is notable so let's print the whole thing with verbose()
141    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    // There's a potential window after rereading the partition table where
154    // udevd hasn't yet received updates from the kernel, settle will return
155    // immediately, and lsblk won't pick up partition labels.  Try to sleep
156    // our way out of this.
157    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    // Ensure we have a root filesystem upfront
177    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    // Verify that the target is empty (if not already wiped in particular, but it's
184    // also good to verify that the wipe worked)
185    let device = bootc_blockdev::list_dev(&opts.device)?;
186    // Canonicalize devpath
187    let devpath: Utf8PathBuf = device.path().into();
188
189    // Always disallow writing to mounted device
190    if is_mounted_in_pid1_mountns(&device.path())? {
191        anyhow::bail!("Device {} is mounted", device.path())
192    }
193
194    // Handle wiping any existing data
195    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    // Use the install configuration to find the block setup, if we have one
218    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        // Otherwise, if a filesystem is specified then we default to whatever was
222        // specified via --block-setup, or the default
223        opts.block_setup.unwrap_or_default()
224    } else {
225        // If there was no default filesystem, then there's no default block setup,
226        // and we need to error out.
227        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    // Load the policy from the container root, which also must be our install root
244    let sepolicy = state.load_policy()?;
245    let sepolicy = sepolicy.as_ref();
246
247    // Create a temporary directory to use for mount points.  Note that we're
248    // in a mount namespace, so these should not be visible on the host.
249    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    // Generate partitioning spec as input to sfdisk
255    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        // PowerPC-PReP-boot
268        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        // No bootloader partition is necessary
277    } 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    // Initialize the /boot filesystem.  Note that in the future, we may match
301    // what systemd/uapi-group encourages and make /boot be FAT32 as well, as
302    // it would aid systemd-boot.
303    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    // Full udev sync; it'd obviously be better to await just the devices
333    // we're targeting, but this is a simple coarse hammer.
334    udev_settle()?;
335
336    // Re-read what we wrote into structured information
337    let base_partitions = &bootc_blockdev::partitions_of(&devpath)?;
338
339    let root_partition = base_partitions.find_partno(rootpn)?;
340    // Verify the partition type matches the DPS root partition type for this architecture
341    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            // This will be replaced via --wipe-slot=all when binding to tpm below
356            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            // The --wipe-slot=all removes our temporary passphrase, and binds to the local TPM device.
371            // We also use .verbose() here as the details are important/notable.
372            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    // Initialize the /boot filesystem
391    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    // Unconditionally enable fsverity for ext4
412    let mkfs_options = match root_filesystem {
413        Filesystem::Ext4 => ["-O", "verity"].as_slice(),
414        _ => [].as_slice(),
415    };
416
417    // Initialize rootfs
418    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    // Add root blockdev kargs (e.g., LUKS parameters)
438    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    // Add root= and rw argument
445    kargs.extend(&Cmdline::from(format!("{rootarg} {RW_KARG}")));
446
447    // Add boot= argument if present
448    if let Some(bootarg) = bootarg {
449        kargs.extend(&Cmdline::from(bootarg.as_str()));
450    }
451
452    // Add CLI kargs
453    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    // Create the underlying mount point directory, which should be labeled
465    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    // And we want to label the root mount of /boot
470    crate::lsm::ensure_dir_labeled(&target_rootfs, "boot", None, 0o755.into(), sepolicy)?;
471
472    // Create the EFI system partition, if applicable
473    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}