1mod aleph;
10#[cfg(feature = "install-to-disk")]
11pub(crate) mod baseline;
12pub(crate) mod completion;
13pub(crate) mod config;
14mod osbuild;
15pub(crate) mod osconfig;
16
17use std::collections::HashMap;
18use std::io::Write;
19use std::os::fd::{AsFd, AsRawFd};
20use std::os::unix::process::CommandExt;
21use std::path::Path;
22use std::process;
23use std::process::Command;
24use std::str::FromStr;
25use std::sync::Arc;
26use std::time::Duration;
27
28use aleph::InstallAleph;
29use anyhow::{Context, Result, anyhow, ensure};
30use bootc_kernel_cmdline::utf8::{Cmdline, CmdlineOwned};
31use bootc_utils::CommandRunExt;
32use camino::Utf8Path;
33use camino::Utf8PathBuf;
34use canon_json::CanonJsonSerialize;
35use cap_std::fs::{Dir, MetadataExt};
36use cap_std_ext::cap_std;
37use cap_std_ext::cap_std::fs::FileType;
38use cap_std_ext::cap_std::fs_utf8::DirEntry as DirEntryUtf8;
39use cap_std_ext::cap_tempfile::TempDir;
40use cap_std_ext::cmdext::CapStdExtCommandExt;
41use cap_std_ext::prelude::CapStdExtDirExt;
42use clap::ValueEnum;
43use fn_error_context::context;
44use ostree::gio;
45use ostree_ext::ostree;
46use ostree_ext::ostree_prepareroot::{ComposefsState, Tristate};
47use ostree_ext::prelude::Cast;
48use ostree_ext::sysroot::{SysrootLock, allocate_new_stateroot, list_stateroots};
49use ostree_ext::{container as ostree_container, ostree_prepareroot};
50#[cfg(feature = "install-to-disk")]
51use rustix::fs::FileTypeExt;
52use rustix::fs::MetadataExt as _;
53use serde::{Deserialize, Serialize};
54
55#[cfg(feature = "install-to-disk")]
56use self::baseline::InstallBlockDeviceOpts;
57use crate::bootc_composefs::{boot::setup_composefs_boot, repo::initialize_composefs_repository};
58use crate::boundimage::{BoundImage, ResolvedBoundImage};
59use crate::containerenv::ContainerExecutionInfo;
60use crate::deploy::{
61 MergeState, PreparedImportMeta, PreparedPullResult, prepare_for_pull, pull_from_prepared,
62};
63use crate::lsm;
64use crate::progress_jsonl::ProgressWriter;
65use crate::spec::{Bootloader, ImageReference};
66use crate::store::Storage;
67use crate::task::Task;
68use crate::utils::sigpolicy_from_opt;
69use bootc_kernel_cmdline::{INITRD_ARG_PREFIX, ROOTFLAGS, bytes, utf8};
70use bootc_mount::Filesystem;
71use composefs::fsverity::FsVerityHashValue;
72
73pub(crate) const BOOT: &str = "boot";
75#[cfg(feature = "install-to-disk")]
77const RUN_BOOTC: &str = "/run/bootc";
78const ALONGSIDE_ROOT_MOUNT: &str = "/target";
80pub(crate) const DESTRUCTIVE_CLEANUP: &str = "etc/bootc-destructive-cleanup";
82const LOST_AND_FOUND: &str = "lost+found";
84const OSTREE_COMPOSEFS_SUPER: &str = ".ostree.cfs";
86const SELINUXFS: &str = "/sys/fs/selinux";
88pub(crate) const EFIVARFS: &str = "/sys/firmware/efi/efivars";
90pub(crate) const ARCH_USES_EFI: bool = cfg!(any(target_arch = "x86_64", target_arch = "aarch64"));
91
92pub(crate) const EFI_LOADER_INFO: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
93
94const DEFAULT_REPO_CONFIG: &[(&str, &str)] = &[
95 ("sysroot.bootloader", "none"),
97 ("sysroot.bootprefix", "true"),
100 ("sysroot.readonly", "true"),
101];
102
103pub(crate) const RW_KARG: &str = "rw";
105
106#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107pub(crate) struct InstallTargetOpts {
108 #[clap(long, default_value = "registry")]
112 #[serde(default)]
113 pub(crate) target_transport: String,
114
115 #[clap(long)]
117 pub(crate) target_imgref: Option<String>,
118
119 #[clap(long, hide = true)]
129 #[serde(default)]
130 pub(crate) target_no_signature_verification: bool,
131
132 #[clap(long)]
136 #[serde(default)]
137 pub(crate) enforce_container_sigpolicy: bool,
138
139 #[clap(long)]
142 #[serde(default)]
143 pub(crate) run_fetch_check: bool,
144
145 #[clap(long)]
148 #[serde(default)]
149 pub(crate) skip_fetch_check: bool,
150
151 #[clap(long = "experimental-unified-storage", hide = true)]
157 #[serde(default)]
158 pub(crate) unified_storage_exp: bool,
159}
160
161#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
162pub(crate) struct InstallSourceOpts {
163 #[clap(long)]
170 pub(crate) source_imgref: Option<String>,
171}
172
173#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
174#[serde(rename_all = "kebab-case")]
175pub(crate) enum BoundImagesOpt {
176 #[default]
178 Stored,
179 #[clap(hide = true)]
180 Skip,
182 Pull,
186}
187
188impl std::fmt::Display for BoundImagesOpt {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 self.to_possible_value().unwrap().get_name().fmt(f)
191 }
192}
193
194#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
195pub(crate) struct InstallConfigOpts {
196 #[clap(long)]
201 #[serde(default)]
202 pub(crate) disable_selinux: bool,
203
204 #[clap(long)]
208 pub(crate) karg: Option<Vec<CmdlineOwned>>,
209
210 #[clap(long)]
218 root_ssh_authorized_keys: Option<Utf8PathBuf>,
219
220 #[clap(long)]
226 #[serde(default)]
227 pub(crate) generic_image: bool,
228
229 #[clap(long)]
231 #[serde(default)]
232 #[arg(default_value_t)]
233 pub(crate) bound_images: BoundImagesOpt,
234
235 #[clap(long)]
237 pub(crate) stateroot: Option<String>,
238}
239
240#[derive(Debug, Default, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
241pub(crate) struct InstallComposefsOpts {
242 #[clap(long, default_value_t)]
244 #[serde(default)]
245 pub(crate) composefs_backend: bool,
246
247 #[clap(long, default_value_t)]
249 #[serde(default)]
250 pub(crate) insecure: bool,
251
252 #[clap(long)]
254 #[serde(default)]
255 pub(crate) bootloader: Option<Bootloader>,
256
257 #[clap(long)]
260 #[serde(default)]
261 pub(crate) uki_addon: Option<Vec<String>>,
262}
263
264#[cfg(feature = "install-to-disk")]
265#[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
266pub(crate) struct InstallToDiskOpts {
267 #[clap(flatten)]
268 #[serde(flatten)]
269 pub(crate) block_opts: InstallBlockDeviceOpts,
270
271 #[clap(flatten)]
272 #[serde(flatten)]
273 pub(crate) source_opts: InstallSourceOpts,
274
275 #[clap(flatten)]
276 #[serde(flatten)]
277 pub(crate) target_opts: InstallTargetOpts,
278
279 #[clap(flatten)]
280 #[serde(flatten)]
281 pub(crate) config_opts: InstallConfigOpts,
282
283 #[clap(long)]
285 #[serde(default)]
286 pub(crate) via_loopback: bool,
287
288 #[clap(flatten)]
289 #[serde(flatten)]
290 pub(crate) composefs_opts: InstallComposefsOpts,
291}
292
293#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
294#[serde(rename_all = "kebab-case")]
295pub(crate) enum ReplaceMode {
296 Wipe,
299 Alongside,
307}
308
309impl std::fmt::Display for ReplaceMode {
310 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311 self.to_possible_value().unwrap().get_name().fmt(f)
312 }
313}
314
315#[derive(Debug, Clone, clap::Args, PartialEq, Eq)]
317pub(crate) struct InstallTargetFilesystemOpts {
318 pub(crate) root_path: Utf8PathBuf,
323
324 #[clap(long)]
328 pub(crate) root_mount_spec: Option<String>,
329
330 #[clap(long)]
335 pub(crate) boot_mount_spec: Option<String>,
336
337 #[clap(long)]
340 pub(crate) replace: Option<ReplaceMode>,
341
342 #[clap(long)]
344 pub(crate) acknowledge_destructive: bool,
345
346 #[clap(long)]
350 pub(crate) skip_finalize: bool,
351}
352
353#[derive(Debug, Clone, clap::Parser, PartialEq, Eq)]
354pub(crate) struct InstallToFilesystemOpts {
355 #[clap(flatten)]
356 pub(crate) filesystem_opts: InstallTargetFilesystemOpts,
357
358 #[clap(flatten)]
359 pub(crate) source_opts: InstallSourceOpts,
360
361 #[clap(flatten)]
362 pub(crate) target_opts: InstallTargetOpts,
363
364 #[clap(flatten)]
365 pub(crate) config_opts: InstallConfigOpts,
366
367 #[clap(flatten)]
368 pub(crate) composefs_opts: InstallComposefsOpts,
369}
370
371#[derive(Debug, Clone, clap::Parser, PartialEq, Eq)]
372pub(crate) struct InstallToExistingRootOpts {
373 #[clap(long, default_value = "alongside")]
375 pub(crate) replace: Option<ReplaceMode>,
376
377 #[clap(flatten)]
378 pub(crate) source_opts: InstallSourceOpts,
379
380 #[clap(flatten)]
381 pub(crate) target_opts: InstallTargetOpts,
382
383 #[clap(flatten)]
384 pub(crate) config_opts: InstallConfigOpts,
385
386 #[clap(long)]
388 pub(crate) acknowledge_destructive: bool,
389
390 #[clap(long)]
393 pub(crate) cleanup: bool,
394
395 #[clap(default_value = ALONGSIDE_ROOT_MOUNT)]
399 pub(crate) root_path: Utf8PathBuf,
400
401 #[clap(flatten)]
402 pub(crate) composefs_opts: InstallComposefsOpts,
403}
404
405#[derive(Debug, clap::Parser, PartialEq, Eq)]
406pub(crate) struct InstallResetOpts {
407 #[clap(long)]
409 pub(crate) experimental: bool,
410
411 #[clap(flatten)]
412 pub(crate) source_opts: InstallSourceOpts,
413
414 #[clap(flatten)]
415 pub(crate) target_opts: InstallTargetOpts,
416
417 #[clap(long)]
421 pub(crate) stateroot: Option<String>,
422
423 #[clap(long)]
425 pub(crate) quiet: bool,
426
427 #[clap(flatten)]
428 pub(crate) progress: crate::cli::ProgressOptions,
429
430 #[clap(long)]
436 pub(crate) apply: bool,
437
438 #[clap(long)]
440 no_root_kargs: bool,
441
442 #[clap(long)]
446 karg: Option<Vec<CmdlineOwned>>,
447}
448
449#[derive(Debug, clap::Parser, PartialEq, Eq)]
450pub(crate) struct InstallPrintConfigurationOpts {
451 #[clap(long)]
455 pub(crate) all: bool,
456}
457
458#[derive(Debug, Clone)]
460pub(crate) struct SourceInfo {
461 pub(crate) imageref: ostree_container::ImageReference,
463 pub(crate) digest: Option<String>,
465 pub(crate) selinux: bool,
467 pub(crate) in_host_mountns: bool,
469}
470
471#[derive(Debug)]
473pub(crate) struct State {
474 pub(crate) source: SourceInfo,
475 pub(crate) selinux_state: SELinuxFinalState,
477 #[allow(dead_code)]
478 pub(crate) config_opts: InstallConfigOpts,
479 pub(crate) target_opts: InstallTargetOpts,
480 pub(crate) target_imgref: ostree_container::OstreeImageReference,
481 #[allow(dead_code)]
482 pub(crate) prepareroot_config: HashMap<String, String>,
483 pub(crate) install_config: Option<config::InstallConfiguration>,
484 pub(crate) root_ssh_authorized_keys: Option<String>,
486 #[allow(dead_code)]
487 pub(crate) host_is_container: bool,
488 pub(crate) container_root: Dir,
490 pub(crate) tempdir: TempDir,
491
492 #[allow(dead_code)]
494 pub(crate) composefs_required: bool,
495
496 pub(crate) composefs_options: InstallComposefsOpts,
498}
499
500#[derive(Debug)]
502pub(crate) struct PostFetchState {
503 pub(crate) detected_bootloader: crate::spec::Bootloader,
505}
506
507impl InstallTargetOpts {
508 pub(crate) fn imageref(&self) -> Result<Option<ostree_container::OstreeImageReference>> {
509 let Some(target_imgname) = self.target_imgref.as_deref() else {
510 return Ok(None);
511 };
512 let target_transport =
513 ostree_container::Transport::try_from(self.target_transport.as_str())?;
514 let target_imgref = ostree_container::OstreeImageReference {
515 sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
516 imgref: ostree_container::ImageReference {
517 transport: target_transport,
518 name: target_imgname.to_string(),
519 },
520 };
521 Ok(Some(target_imgref))
522 }
523}
524
525impl State {
526 #[context("Loading SELinux policy")]
527 pub(crate) fn load_policy(&self) -> Result<Option<ostree::SePolicy>> {
528 if !self.selinux_state.enabled() {
529 return Ok(None);
530 }
531 let r = lsm::new_sepolicy_at(&self.container_root)?
533 .ok_or_else(|| anyhow::anyhow!("SELinux enabled, but no policy found in root"))?;
534 tracing::debug!("Loaded SELinux policy: {}", r.csum().unwrap());
536 Ok(Some(r))
537 }
538
539 #[context("Finalizing state")]
540 #[allow(dead_code)]
541 pub(crate) fn consume(self) -> Result<()> {
542 self.tempdir.close()?;
543 if let SELinuxFinalState::Enabled(Some(guard)) = self.selinux_state {
545 guard.consume()?;
546 }
547 Ok(())
548 }
549
550 pub(crate) fn require_no_kargs_for_uki(&self) -> Result<()> {
552 if self
553 .config_opts
554 .karg
555 .as_ref()
556 .map(|v| !v.is_empty())
557 .unwrap_or_default()
558 {
559 anyhow::bail!("Cannot use externally specified kernel arguments with UKI");
560 }
561 Ok(())
562 }
563
564 fn stateroot(&self) -> &str {
565 self.config_opts
567 .stateroot
568 .as_deref()
569 .or_else(|| {
570 self.install_config
571 .as_ref()
572 .and_then(|c| c.stateroot.as_deref())
573 })
574 .unwrap_or(ostree_ext::container::deploy::STATEROOT_DEFAULT)
575 }
576}
577
578#[derive(Debug, Clone)]
589pub(crate) struct MountSpec {
590 pub(crate) source: String,
591 pub(crate) target: String,
592 pub(crate) fstype: String,
593 pub(crate) options: Option<String>,
594}
595
596impl MountSpec {
597 const AUTO: &'static str = "auto";
598
599 pub(crate) fn new(src: &str, target: &str) -> Self {
600 MountSpec {
601 source: src.to_string(),
602 target: target.to_string(),
603 fstype: Self::AUTO.to_string(),
604 options: None,
605 }
606 }
607
608 pub(crate) fn new_uuid_src(uuid: &str, target: &str) -> Self {
610 Self::new(&format!("UUID={uuid}"), target)
611 }
612
613 pub(crate) fn get_source_uuid(&self) -> Option<&str> {
614 if let Some((t, rest)) = self.source.split_once('=') {
615 if t.eq_ignore_ascii_case("uuid") {
616 return Some(rest);
617 }
618 }
619 None
620 }
621
622 pub(crate) fn to_fstab(&self) -> String {
623 let options = self.options.as_deref().unwrap_or("defaults");
624 format!(
625 "{} {} {} {} 0 0",
626 self.source, self.target, self.fstype, options
627 )
628 }
629
630 pub(crate) fn push_option(&mut self, opt: &str) {
632 let options = self.options.get_or_insert_with(Default::default);
633 if !options.is_empty() {
634 options.push(',');
635 }
636 options.push_str(opt);
637 }
638}
639
640impl FromStr for MountSpec {
641 type Err = anyhow::Error;
642
643 fn from_str(s: &str) -> Result<Self> {
644 let mut parts = s.split_ascii_whitespace().fuse();
645 let source = parts.next().unwrap_or_default();
646 if source.is_empty() {
647 tracing::debug!("Empty mount specification");
648 return Ok(Self {
649 source: String::new(),
650 target: String::new(),
651 fstype: Self::AUTO.into(),
652 options: None,
653 });
654 }
655 let target = parts
656 .next()
657 .ok_or_else(|| anyhow!("Missing target in mount specification {s}"))?;
658 let fstype = parts.next().unwrap_or(Self::AUTO);
659 let options = parts.next().map(ToOwned::to_owned);
660 Ok(Self {
661 source: source.to_string(),
662 fstype: fstype.to_string(),
663 target: target.to_string(),
664 options,
665 })
666 }
667}
668
669#[cfg(feature = "install-to-disk")]
670impl InstallToDiskOpts {
671 pub(crate) fn validate(&self) -> Result<()> {
672 if !self.composefs_opts.composefs_backend {
673 if self.composefs_opts.insecure != false {
675 anyhow::bail!("--insecure must not be provided without --composefs-backend");
676 }
677 }
678
679 Ok(())
680 }
681}
682
683impl SourceInfo {
684 #[context("Gathering source info from container env")]
687 pub(crate) fn from_container(
688 root: &Dir,
689 container_info: &ContainerExecutionInfo,
690 ) -> Result<Self> {
691 if !container_info.engine.starts_with("podman") {
692 anyhow::bail!("Currently this command only supports being executed via podman");
693 }
694 if container_info.imageid.is_empty() {
695 anyhow::bail!("Invalid empty imageid");
696 }
697 let imageref = ostree_container::ImageReference {
698 transport: ostree_container::Transport::ContainerStorage,
699 name: container_info.image.clone(),
700 };
701 tracing::debug!("Finding digest for image ID {}", container_info.imageid);
702 let digest = crate::podman::imageid_to_digest(&container_info.imageid)?;
703
704 Self::new(imageref, Some(digest), root, true)
705 }
706
707 #[context("Creating source info from a given imageref")]
708 pub(crate) fn from_imageref(imageref: &str, root: &Dir) -> Result<Self> {
709 let imageref = ostree_container::ImageReference::try_from(imageref)?;
710 Self::new(imageref, None, root, false)
711 }
712
713 fn have_selinux_from_repo(root: &Dir) -> Result<bool> {
714 let cancellable = ostree::gio::Cancellable::NONE;
715
716 let commit = Command::new("ostree")
717 .args(["--repo=/ostree/repo", "rev-parse", "--single"])
718 .run_get_string()?;
719 let repo = ostree::Repo::open_at_dir(root.as_fd(), "ostree/repo")?;
720 let root = repo
721 .read_commit(commit.trim(), cancellable)
722 .context("Reading commit")?
723 .0;
724 let root = root.downcast_ref::<ostree::RepoFile>().unwrap();
725 let xattrs = root.xattrs(cancellable)?;
726 Ok(crate::lsm::xattrs_have_selinux(&xattrs))
727 }
728
729 fn new(
731 imageref: ostree_container::ImageReference,
732 digest: Option<String>,
733 root: &Dir,
734 in_host_mountns: bool,
735 ) -> Result<Self> {
736 let selinux = if Path::new("/ostree/repo").try_exists()? {
737 Self::have_selinux_from_repo(root)?
738 } else {
739 lsm::have_selinux_policy(root)?
740 };
741 Ok(Self {
742 imageref,
743 digest,
744 selinux,
745 in_host_mountns,
746 })
747 }
748}
749
750pub(crate) fn print_configuration(opts: InstallPrintConfigurationOpts) -> Result<()> {
751 let mut install_config = config::load_config()?.unwrap_or_default();
752 if !opts.all {
753 install_config.filter_to_external();
754 }
755 let stdout = std::io::stdout().lock();
756 anyhow::Ok(install_config.to_canon_json_writer(stdout)?)
757}
758
759#[context("Creating ostree deployment")]
760async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result<(Storage, bool)> {
761 let sepolicy = state.load_policy()?;
762 let sepolicy = sepolicy.as_ref();
763 let rootfs_dir = &root_setup.physical_root;
765 let cancellable = gio::Cancellable::NONE;
766
767 let stateroot = state.stateroot();
768
769 let has_ostree = rootfs_dir.try_exists("ostree/repo")?;
770 if !has_ostree {
771 Task::new("Initializing ostree layout", "ostree")
772 .args(["admin", "init-fs", "--modern", "."])
773 .cwd(rootfs_dir)?
774 .run()?;
775 } else {
776 println!("Reusing extant ostree layout");
777
778 let path = ".".into();
779 let _ = crate::utils::open_dir_remount_rw(rootfs_dir, path)
780 .context("remounting target as read-write")?;
781 crate::utils::remove_immutability(rootfs_dir, path)?;
782 }
783
784 crate::lsm::ensure_dir_labeled(rootfs_dir, "", Some("/".into()), 0o755.into(), sepolicy)?;
787
788 if has_ostree && root_setup.boot.is_some() {
791 if let Some(boot) = &root_setup.boot {
792 let source_boot = &boot.source;
793 let target_boot = root_setup.physical_root_path.join(BOOT);
794 tracing::debug!("Mount {source_boot} to {target_boot} on ostree");
795 bootc_mount::mount(source_boot, &target_boot)?;
796 }
797 }
798
799 if rootfs_dir.try_exists("boot")? {
801 crate::lsm::ensure_dir_labeled(rootfs_dir, "boot", None, 0o755.into(), sepolicy)?;
802 }
803
804 let ostree_opts = state
806 .install_config
807 .as_ref()
808 .and_then(|c| c.ostree.as_ref())
809 .into_iter()
810 .flat_map(|o| o.to_config_tuples());
811
812 let repo_config: Vec<_> = DEFAULT_REPO_CONFIG
813 .iter()
814 .copied()
815 .chain(ostree_opts)
816 .collect();
817
818 for (k, v) in repo_config.iter() {
819 Command::new("ostree")
820 .args(["config", "--repo", "ostree/repo", "set", k, v])
821 .cwd_dir(rootfs_dir.try_clone()?)
822 .run_capture_stderr()?;
823 }
824
825 let sysroot = {
826 let path = format!(
827 "/proc/{}/fd/{}",
828 process::id(),
829 rootfs_dir.as_fd().as_raw_fd()
830 );
831 ostree::Sysroot::new(Some(&gio::File::for_path(path)))
832 };
833 sysroot.load(cancellable)?;
834 let repo = &sysroot.repo();
835
836 let repo_verity_state = ostree_ext::fsverity::is_verity_enabled(&repo)?;
837 let prepare_root_composefs = state
838 .prepareroot_config
839 .get("composefs.enabled")
840 .map(|v| ComposefsState::from_str(&v))
841 .transpose()?
842 .unwrap_or(ComposefsState::default());
843 if prepare_root_composefs.requires_fsverity() || repo_verity_state.desired == Tristate::Enabled
844 {
845 ostree_ext::fsverity::ensure_verity(repo).await?;
846 }
847
848 if let Some(booted) = sysroot.booted_deployment() {
849 if stateroot == booted.stateroot() {
850 anyhow::bail!("Cannot redeploy over booted stateroot {stateroot}");
851 }
852 }
853
854 let sysroot_dir = crate::utils::sysroot_dir(&sysroot)?;
855
856 let stateroot_path = format!("ostree/deploy/{stateroot}");
861 if !sysroot_dir.try_exists(stateroot_path)? {
862 sysroot
863 .init_osname(stateroot, cancellable)
864 .context("initializing stateroot")?;
865 }
866
867 state.tempdir.create_dir("temp-run")?;
868 let temp_run = state.tempdir.open_dir("temp-run")?;
869
870 if let Some(policy) = sepolicy {
873 let ostree_dir = rootfs_dir.open_dir("ostree")?;
874 crate::lsm::ensure_dir_labeled(
875 &ostree_dir,
876 ".",
877 Some("/usr".into()),
878 0o755.into(),
879 Some(policy),
880 )?;
881 }
882
883 sysroot.load(cancellable)?;
884 let sysroot = SysrootLock::new_from_sysroot(&sysroot).await?;
885 let storage = Storage::new_ostree(sysroot, &temp_run)?;
886
887 Ok((storage, has_ostree))
888}
889
890fn check_disk_space(
891 repo_fd: impl AsFd,
892 image_meta: &PreparedImportMeta,
893 imgref: &ImageReference,
894) -> Result<()> {
895 let stat = rustix::fs::fstatvfs(repo_fd)?;
896 let bytes_avail: u64 = stat.f_bsize * stat.f_bavail;
897 tracing::trace!("bytes_avail: {bytes_avail}");
898
899 if image_meta.bytes_to_fetch > bytes_avail {
900 anyhow::bail!(
901 "Insufficient free space for {image} (available: {bytes_avail} required: {bytes_to_fetch})",
902 bytes_avail = ostree_ext::glib::format_size(bytes_avail),
903 bytes_to_fetch = ostree_ext::glib::format_size(image_meta.bytes_to_fetch),
904 image = imgref.image,
905 );
906 }
907
908 Ok(())
909}
910
911#[context("Creating ostree deployment")]
912async fn install_container(
913 state: &State,
914 root_setup: &RootSetup,
915 sysroot: &ostree::Sysroot,
916 storage: &Storage,
917 has_ostree: bool,
918) -> Result<(ostree::Deployment, InstallAleph)> {
919 let sepolicy = state.load_policy()?;
920 let sepolicy = sepolicy.as_ref();
921 let stateroot = state.stateroot();
922
923 let (src_imageref, proxy_cfg) = if !state.source.in_host_mountns {
925 (state.source.imageref.clone(), None)
926 } else {
927 let src_imageref = {
928 let digest = state
930 .source
931 .digest
932 .as_ref()
933 .ok_or_else(|| anyhow::anyhow!("Missing container image digest"))?;
934 let spec = crate::utils::digested_pullspec(&state.source.imageref.name, digest);
935 ostree_container::ImageReference {
936 transport: ostree_container::Transport::ContainerStorage,
937 name: spec,
938 }
939 };
940
941 let proxy_cfg = ostree_container::store::ImageProxyConfig::default();
942 (src_imageref, Some(proxy_cfg))
943 };
944 let src_imageref = ostree_container::OstreeImageReference {
945 sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
948 imgref: src_imageref,
949 };
950
951 let spec_imgref = ImageReference::from(src_imageref.clone());
954 let repo = &sysroot.repo();
955 repo.set_disable_fsync(true);
956
957 let use_unified = state.target_opts.unified_storage_exp;
961
962 let prepared = if use_unified {
963 tracing::info!("Using unified storage path for installation");
964 crate::deploy::prepare_for_pull_unified(
965 repo,
966 &spec_imgref,
967 Some(&state.target_imgref),
968 storage,
969 )
970 .await?
971 } else {
972 prepare_for_pull(repo, &spec_imgref, Some(&state.target_imgref)).await?
973 };
974
975 let pulled_image = match prepared {
976 PreparedPullResult::AlreadyPresent(existing) => existing,
977 PreparedPullResult::Ready(image_meta) => {
978 check_disk_space(root_setup.physical_root.as_fd(), &image_meta, &spec_imgref)?;
979 pull_from_prepared(&spec_imgref, false, ProgressWriter::default(), *image_meta).await?
980 }
981 };
982
983 repo.set_disable_fsync(false);
984
985 let merged_ostree_root = sysroot
988 .repo()
989 .read_commit(pulled_image.ostree_commit.as_str(), gio::Cancellable::NONE)?
990 .0;
991 let kargsd = crate::bootc_kargs::get_kargs_from_ostree_root(
992 &sysroot.repo(),
993 merged_ostree_root.downcast_ref().unwrap(),
994 std::env::consts::ARCH,
995 )?;
996
997 if ostree_ext::bootabletree::commit_has_aboot_img(&merged_ostree_root, None)? {
1000 tracing::debug!("Setting bootloader to aboot");
1001 Command::new("ostree")
1002 .args([
1003 "config",
1004 "--repo",
1005 "ostree/repo",
1006 "set",
1007 "sysroot.bootloader",
1008 "aboot",
1009 ])
1010 .cwd_dir(root_setup.physical_root.try_clone()?)
1011 .run_capture_stderr()
1012 .context("Setting bootloader config to aboot")?;
1013 sysroot.repo().reload_config(None::<&gio::Cancellable>)?;
1014 }
1015
1016 let install_config_kargs = state.install_config.as_ref().and_then(|c| c.kargs.as_ref());
1018
1019 let mut kargs = Cmdline::new();
1025
1026 kargs.extend(&root_setup.kargs);
1027
1028 if let Some(install_config_kargs) = install_config_kargs {
1029 for karg in install_config_kargs {
1030 kargs.extend(&Cmdline::from(karg.as_str()));
1031 }
1032 }
1033
1034 kargs.extend(&kargsd);
1035
1036 if let Some(cli_kargs) = state.config_opts.karg.as_ref() {
1037 for karg in cli_kargs {
1038 kargs.extend(karg);
1039 }
1040 }
1041
1042 let kargs_strs: Vec<&str> = kargs.iter_str().collect();
1044
1045 let mut options = ostree_container::deploy::DeployOpts::default();
1046 options.kargs = Some(kargs_strs.as_slice());
1047 options.target_imgref = Some(&state.target_imgref);
1048 options.proxy_cfg = proxy_cfg;
1049 options.skip_completion = true; options.no_clean = has_ostree;
1051 let imgstate = crate::utils::async_task_with_spinner(
1052 "Deploying container image",
1053 ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)),
1054 )
1055 .await?;
1056
1057 let deployment = sysroot
1058 .deployments()
1059 .into_iter()
1060 .next()
1061 .ok_or_else(|| anyhow::anyhow!("Failed to find deployment"))?;
1062 let path = sysroot.deployment_dirpath(&deployment);
1064 let root = root_setup
1065 .physical_root
1066 .open_dir(path.as_str())
1067 .context("Opening deployment dir")?;
1068
1069 if let Some(policy) = sepolicy {
1073 let deployment_root_meta = root.dir_metadata()?;
1074 let deployment_root_devino = (deployment_root_meta.dev(), deployment_root_meta.ino());
1075 for d in ["ostree", "boot"] {
1076 let mut pathbuf = Utf8PathBuf::from(d);
1077 crate::lsm::ensure_dir_labeled_recurse(
1078 &root_setup.physical_root,
1079 &mut pathbuf,
1080 policy,
1081 Some(deployment_root_devino),
1082 )
1083 .with_context(|| format!("Recursive SELinux relabeling of {d}"))?;
1084 }
1085
1086 if let Some(cfs_super) = root.open_optional(OSTREE_COMPOSEFS_SUPER)? {
1087 let label = crate::lsm::require_label(policy, "/usr".into(), 0o644)?;
1088 crate::lsm::set_security_selinux(cfs_super.as_fd(), label.as_bytes())?;
1089 } else {
1090 tracing::warn!("Missing {OSTREE_COMPOSEFS_SUPER}; composefs is not enabled?");
1091 }
1092 }
1093
1094 if let Some(boot) = root_setup.boot.as_ref() {
1098 if !boot.source.is_empty() {
1099 crate::lsm::atomic_replace_labeled(&root, "etc/fstab", 0o644.into(), sepolicy, |w| {
1100 writeln!(w, "{}", boot.to_fstab()).map_err(Into::into)
1101 })?;
1102 }
1103 }
1104
1105 if let Some(contents) = state.root_ssh_authorized_keys.as_deref() {
1106 osconfig::inject_root_ssh_authorized_keys(&root, sepolicy, contents)?;
1107 }
1108
1109 let aleph = InstallAleph::new(&src_imageref, &imgstate, &state.selinux_state)?;
1110 Ok((deployment, aleph))
1111}
1112
1113pub(crate) fn run_in_host_mountns(cmd: &str) -> Result<Command> {
1115 let mut c = Command::new(bootc_utils::reexec::executable_path()?);
1116 c.lifecycle_bind()
1117 .args(["exec-in-host-mount-namespace", cmd]);
1118 Ok(c)
1119}
1120
1121#[context("Re-exec in host mountns")]
1122pub(crate) fn exec_in_host_mountns(args: &[std::ffi::OsString]) -> Result<()> {
1123 let (cmd, args) = args
1124 .split_first()
1125 .ok_or_else(|| anyhow::anyhow!("Missing command"))?;
1126 tracing::trace!("{cmd:?} {args:?}");
1127 let pid1mountns = std::fs::File::open("/proc/1/ns/mnt").context("open pid1 mountns")?;
1128 rustix::thread::move_into_link_name_space(
1129 pid1mountns.as_fd(),
1130 Some(rustix::thread::LinkNameSpaceType::Mount),
1131 )
1132 .context("setns")?;
1133 rustix::process::chdir("/").context("chdir")?;
1134 if !Utf8Path::new("/usr").try_exists().context("/usr")?
1137 && Utf8Path::new("/root/usr")
1138 .try_exists()
1139 .context("/root/usr")?
1140 {
1141 tracing::debug!("Using supermin workaround");
1142 rustix::process::chroot("/root").context("chroot")?;
1143 }
1144 Err(Command::new(cmd).args(args).arg0(bootc_utils::NAME).exec()).context("exec")?
1145}
1146
1147pub(crate) struct RootSetup {
1148 #[cfg(feature = "install-to-disk")]
1149 luks_device: Option<String>,
1150 pub(crate) device_info: bootc_blockdev::PartitionTable,
1151 pub(crate) physical_root_path: Utf8PathBuf,
1154 pub(crate) physical_root: Dir,
1156 pub(crate) target_root_path: Option<Utf8PathBuf>,
1158 pub(crate) rootfs_uuid: Option<String>,
1159 skip_finalize: bool,
1161 boot: Option<MountSpec>,
1162 pub(crate) kargs: CmdlineOwned,
1163}
1164
1165fn require_boot_uuid(spec: &MountSpec) -> Result<&str> {
1166 spec.get_source_uuid()
1167 .ok_or_else(|| anyhow!("/boot is not specified via UUID= (this is currently required)"))
1168}
1169
1170impl RootSetup {
1171 pub(crate) fn get_boot_uuid(&self) -> Result<Option<&str>> {
1174 self.boot.as_ref().map(require_boot_uuid).transpose()
1175 }
1176
1177 #[cfg(feature = "install-to-disk")]
1179 fn into_storage(self) -> (Utf8PathBuf, Option<String>) {
1180 (self.physical_root_path, self.luks_device)
1181 }
1182}
1183
1184#[derive(Debug)]
1185#[allow(dead_code)]
1186pub(crate) enum SELinuxFinalState {
1187 ForceTargetDisabled,
1189 Enabled(Option<crate::lsm::SetEnforceGuard>),
1191 HostDisabled,
1193 Disabled,
1195}
1196
1197impl SELinuxFinalState {
1198 pub(crate) fn enabled(&self) -> bool {
1200 match self {
1201 SELinuxFinalState::ForceTargetDisabled | SELinuxFinalState::Disabled => false,
1202 SELinuxFinalState::Enabled(_) | SELinuxFinalState::HostDisabled => true,
1203 }
1204 }
1205
1206 pub(crate) fn to_aleph(&self) -> &'static str {
1209 match self {
1210 SELinuxFinalState::ForceTargetDisabled => "force-target-disabled",
1211 SELinuxFinalState::Enabled(_) => "enabled",
1212 SELinuxFinalState::HostDisabled => "host-disabled",
1213 SELinuxFinalState::Disabled => "disabled",
1214 }
1215 }
1216}
1217
1218pub(crate) fn reexecute_self_for_selinux_if_needed(
1223 srcdata: &SourceInfo,
1224 override_disable_selinux: bool,
1225) -> Result<SELinuxFinalState> {
1226 if srcdata.selinux {
1228 let host_selinux = crate::lsm::selinux_enabled()?;
1229 tracing::debug!("Target has SELinux, host={host_selinux}");
1230 let r = if override_disable_selinux {
1231 println!("notice: Target has SELinux enabled, overriding to disable");
1232 SELinuxFinalState::ForceTargetDisabled
1233 } else if host_selinux {
1234 setup_sys_mount("selinuxfs", SELINUXFS)?;
1240 let g = crate::lsm::selinux_ensure_install_or_setenforce()?;
1242 SELinuxFinalState::Enabled(g)
1243 } else {
1244 SELinuxFinalState::HostDisabled
1245 };
1246 Ok(r)
1247 } else {
1248 Ok(SELinuxFinalState::Disabled)
1249 }
1250}
1251
1252pub(crate) fn finalize_filesystem(
1255 fsname: &str,
1256 root: &Dir,
1257 path: impl AsRef<Utf8Path>,
1258) -> Result<()> {
1259 let path = path.as_ref();
1260 Task::new(format!("Trimming {fsname}"), "fstrim")
1262 .args(["--quiet-unsupported", "-v", path.as_str()])
1263 .cwd(root)?
1264 .run()?;
1265 Task::new(format!("Finalizing filesystem {fsname}"), "mount")
1268 .cwd(root)?
1269 .args(["-o", "remount,ro", path.as_str()])
1270 .run()?;
1271 for a in ["-f", "-u"] {
1273 Command::new("fsfreeze")
1274 .cwd_dir(root.try_clone()?)
1275 .args([a, path.as_str()])
1276 .run_capture_stderr()?;
1277 }
1278 Ok(())
1279}
1280
1281fn require_host_pidns() -> Result<()> {
1283 if rustix::process::getpid().is_init() {
1284 anyhow::bail!("This command must be run with the podman --pid=host flag")
1285 }
1286 tracing::trace!("OK: we're not pid 1");
1287 Ok(())
1288}
1289
1290fn require_host_userns() -> Result<()> {
1293 let proc1 = "/proc/1";
1294 let pid1_uid = Path::new(proc1)
1295 .metadata()
1296 .with_context(|| format!("Querying {proc1}"))?
1297 .uid();
1298 ensure!(
1301 pid1_uid == 0,
1302 "{proc1} is owned by {pid1_uid}, not zero; this command must be run in the root user namespace (e.g. not rootless podman)"
1303 );
1304 tracing::trace!("OK: we're in a matching user namespace with pid1");
1305 Ok(())
1306}
1307
1308pub(crate) fn setup_tmp_mount() -> Result<()> {
1313 let st = rustix::fs::statfs("/tmp")?;
1314 if st.f_type == libc::TMPFS_MAGIC {
1315 tracing::trace!("Already have tmpfs /tmp")
1316 } else {
1317 Command::new("mount")
1320 .args(["tmpfs", "-t", "tmpfs", "/tmp"])
1321 .run_capture_stderr()?;
1322 }
1323 Ok(())
1324}
1325
1326#[context("Ensuring sys mount {fspath} {fstype}")]
1329pub(crate) fn setup_sys_mount(fstype: &str, fspath: &str) -> Result<()> {
1330 tracing::debug!("Setting up sys mounts");
1331 let rootfs = format!("/proc/1/root/{fspath}");
1332 if !Path::new(rootfs.as_str()).try_exists()? {
1334 return Ok(());
1335 }
1336
1337 if std::fs::read_dir(rootfs)?.next().is_none() {
1339 return Ok(());
1340 }
1341
1342 if Path::new(fspath).try_exists()? && std::fs::read_dir(fspath)?.next().is_some() {
1346 return Ok(());
1347 }
1348
1349 Command::new("mount")
1351 .args(["-t", fstype, fstype, fspath])
1352 .run_capture_stderr()?;
1353
1354 Ok(())
1355}
1356
1357#[context("Verifying fetch")]
1359async fn verify_target_fetch(
1360 tmpdir: &Dir,
1361 imgref: &ostree_container::OstreeImageReference,
1362) -> Result<()> {
1363 let tmpdir = &TempDir::new_in(&tmpdir)?;
1364 let tmprepo = &ostree::Repo::create_at_dir(tmpdir.as_fd(), ".", ostree::RepoMode::Bare, None)
1365 .context("Init tmp repo")?;
1366
1367 tracing::trace!("Verifying fetch for {imgref}");
1368 let mut imp =
1369 ostree_container::store::ImageImporter::new(tmprepo, imgref, Default::default()).await?;
1370 use ostree_container::store::PrepareResult;
1371 let prep = match imp.prepare().await? {
1372 PrepareResult::AlreadyPresent(_) => unreachable!(),
1374 PrepareResult::Ready(r) => r,
1375 };
1376 tracing::debug!("Fetched manifest with digest {}", prep.manifest_digest);
1377 Ok(())
1378}
1379
1380async fn prepare_install(
1382 config_opts: InstallConfigOpts,
1383 source_opts: InstallSourceOpts,
1384 target_opts: InstallTargetOpts,
1385 mut composefs_options: InstallComposefsOpts,
1386) -> Result<Arc<State>> {
1387 tracing::trace!("Preparing install");
1388 let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
1389 .context("Opening /")?;
1390
1391 let host_is_container = crate::containerenv::is_container(&rootfs);
1392 let external_source = source_opts.source_imgref.is_some();
1393 let (source, target_rootfs) = match source_opts.source_imgref {
1394 None => {
1395 ensure!(
1396 host_is_container,
1397 "Either --source-imgref must be defined or this command must be executed inside a podman container."
1398 );
1399
1400 crate::cli::require_root(true)?;
1401
1402 require_host_pidns()?;
1403 require_host_userns()?;
1406 let container_info = crate::containerenv::get_container_execution_info(&rootfs)?;
1407 match container_info.rootless.as_deref() {
1409 Some("1") => anyhow::bail!(
1410 "Cannot install from rootless podman; this command must be run as root"
1411 ),
1412 Some(o) => tracing::debug!("rootless={o}"),
1413 None => tracing::debug!(
1415 "notice: Did not find rootless= entry in {}",
1416 crate::containerenv::PATH,
1417 ),
1418 };
1419 tracing::trace!("Read container engine info {:?}", container_info);
1420
1421 let source = SourceInfo::from_container(&rootfs, &container_info)?;
1422 (source, Some(rootfs.try_clone()?))
1423 }
1424 Some(source) => {
1425 crate::cli::require_root(false)?;
1426 let source = SourceInfo::from_imageref(&source, &rootfs)?;
1427 (source, None)
1428 }
1429 };
1430
1431 if target_opts.target_no_signature_verification {
1434 tracing::debug!(
1436 "Use of --target-no-signature-verification flag which is enabled by default"
1437 );
1438 }
1439 let target_sigverify = sigpolicy_from_opt(target_opts.enforce_container_sigpolicy);
1440 let target_imgname = target_opts
1441 .target_imgref
1442 .as_deref()
1443 .unwrap_or(source.imageref.name.as_str());
1444 let target_transport =
1445 ostree_container::Transport::try_from(target_opts.target_transport.as_str())?;
1446 let target_imgref = ostree_container::OstreeImageReference {
1447 sigverify: target_sigverify,
1448 imgref: ostree_container::ImageReference {
1449 transport: target_transport,
1450 name: target_imgname.to_string(),
1451 },
1452 };
1453 tracing::debug!("Target image reference: {target_imgref}");
1454
1455 let composefs_required = if let Some(root) = target_rootfs.as_ref() {
1456 crate::kernel::find_kernel(root)?
1457 .map(|k| k.unified)
1458 .unwrap_or(false)
1459 } else {
1460 false
1461 };
1462
1463 tracing::debug!("Composefs required: {composefs_required}");
1464
1465 if composefs_required {
1466 composefs_options.composefs_backend = true;
1467 }
1468
1469 bootc_mount::ensure_mirrored_host_mount("/dev")?;
1471 bootc_mount::ensure_mirrored_host_mount("/var/lib/containers")?;
1474 bootc_mount::ensure_mirrored_host_mount("/var/tmp")?;
1477 setup_tmp_mount()?;
1479 let tempdir = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
1482 osbuild::adjust_for_bootc_image_builder(&rootfs, &tempdir)?;
1484
1485 if target_opts.run_fetch_check {
1486 verify_target_fetch(&tempdir, &target_imgref).await?;
1487 }
1488
1489 if !external_source && std::env::var_os("BOOTC_SKIP_UNSHARE").is_none() {
1492 super::cli::ensure_self_unshared_mount_namespace()?;
1493 }
1494
1495 setup_sys_mount("efivarfs", EFIVARFS)?;
1496
1497 let selinux_state = reexecute_self_for_selinux_if_needed(&source, config_opts.disable_selinux)?;
1499 tracing::debug!("SELinux state: {selinux_state:?}");
1500
1501 println!("Installing image: {:#}", &target_imgref);
1502 if let Some(digest) = source.digest.as_deref() {
1503 println!("Digest: {digest}");
1504 }
1505
1506 let install_config = config::load_config()?;
1507 if install_config.is_some() {
1508 tracing::debug!("Loaded install configuration");
1509 } else {
1510 tracing::debug!("No install configuration found");
1511 }
1512
1513 let prepareroot_config = {
1515 let kf = ostree_prepareroot::require_config_from_root(&rootfs)?;
1516 let mut r = HashMap::new();
1517 for grp in kf.groups() {
1518 for key in kf.keys(&grp)? {
1519 let key = key.as_str();
1520 let value = kf.value(&grp, key)?;
1521 r.insert(format!("{grp}.{key}"), value.to_string());
1522 }
1523 }
1524 r
1525 };
1526
1527 let root_ssh_authorized_keys = config_opts
1530 .root_ssh_authorized_keys
1531 .as_ref()
1532 .map(|p| std::fs::read_to_string(p).with_context(|| format!("Reading {p}")))
1533 .transpose()?;
1534
1535 let state = Arc::new(State {
1539 selinux_state,
1540 source,
1541 config_opts,
1542 target_opts,
1543 target_imgref,
1544 install_config,
1545 prepareroot_config,
1546 root_ssh_authorized_keys,
1547 container_root: rootfs,
1548 tempdir,
1549 host_is_container,
1550 composefs_required,
1551 composefs_options,
1552 });
1553
1554 Ok(state)
1555}
1556
1557impl PostFetchState {
1558 pub(crate) fn new(state: &State, d: &Dir) -> Result<Self> {
1559 let detected_bootloader = {
1562 if let Some(bootloader) = state.composefs_options.bootloader.clone() {
1563 bootloader
1564 } else {
1565 if crate::bootloader::supports_bootupd(d)? {
1566 crate::spec::Bootloader::Grub
1567 } else {
1568 crate::spec::Bootloader::Systemd
1569 }
1570 }
1571 };
1572 println!("Bootloader: {detected_bootloader}");
1573 let r = Self {
1574 detected_bootloader,
1575 };
1576 Ok(r)
1577 }
1578}
1579
1580async fn install_with_sysroot(
1585 state: &State,
1586 rootfs: &RootSetup,
1587 storage: &Storage,
1588 boot_uuid: &str,
1589 bound_images: BoundImages,
1590 has_ostree: bool,
1591) -> Result<()> {
1592 let ostree = storage.get_ostree()?;
1593 let c_storage = storage.get_ensure_imgstore()?;
1594
1595 let (deployment, aleph) = install_container(state, rootfs, ostree, storage, has_ostree).await?;
1598 aleph.write_to(&rootfs.physical_root)?;
1600
1601 let deployment_path = ostree.deployment_dirpath(&deployment);
1602
1603 let deployment_dir = rootfs
1604 .physical_root
1605 .open_dir(&deployment_path)
1606 .context("Opening deployment dir")?;
1607 let postfetch = PostFetchState::new(state, &deployment_dir)?;
1608
1609 if cfg!(target_arch = "s390x") {
1610 crate::bootloader::install_via_zipl(&rootfs.device_info, boot_uuid)?;
1612 } else {
1613 match postfetch.detected_bootloader {
1614 Bootloader::Grub => {
1615 crate::bootloader::install_via_bootupd(
1616 &rootfs.device_info,
1617 &rootfs
1618 .target_root_path
1619 .clone()
1620 .unwrap_or(rootfs.physical_root_path.clone()),
1621 &state.config_opts,
1622 Some(&deployment_path.as_str()),
1623 )?;
1624 }
1625 Bootloader::Systemd => {
1626 anyhow::bail!("bootupd is required for ostree-based installs");
1627 }
1628 }
1629 }
1630 tracing::debug!("Installed bootloader");
1631
1632 tracing::debug!("Performing post-deployment operations");
1633
1634 match bound_images {
1635 BoundImages::Skip => {}
1636 BoundImages::Resolved(resolved_bound_images) => {
1637 for image in resolved_bound_images {
1639 let image = image.image.as_str();
1640 c_storage.pull_from_host_storage(image).await?;
1641 }
1642 }
1643 BoundImages::Unresolved(bound_images) => {
1644 crate::boundimage::pull_images_impl(c_storage, bound_images)
1645 .await
1646 .context("pulling bound images")?;
1647 }
1648 }
1649
1650 Ok(())
1651}
1652
1653enum BoundImages {
1654 Skip,
1655 Resolved(Vec<ResolvedBoundImage>),
1656 Unresolved(Vec<BoundImage>),
1657}
1658
1659impl BoundImages {
1660 async fn from_state(state: &State) -> Result<Self> {
1661 let bound_images = match state.config_opts.bound_images {
1662 BoundImagesOpt::Skip => BoundImages::Skip,
1663 others => {
1664 let queried_images = crate::boundimage::query_bound_images(&state.container_root)?;
1665 match others {
1666 BoundImagesOpt::Stored => {
1667 let mut r = Vec::with_capacity(queried_images.len());
1669 for image in queried_images {
1670 let resolved = ResolvedBoundImage::from_image(&image).await?;
1671 tracing::debug!("Resolved {}: {}", resolved.image, resolved.digest);
1672 r.push(resolved)
1673 }
1674 BoundImages::Resolved(r)
1675 }
1676 BoundImagesOpt::Pull => {
1677 BoundImages::Unresolved(queried_images)
1679 }
1680 BoundImagesOpt::Skip => anyhow::bail!("unreachable error"),
1681 }
1682 }
1683 };
1684
1685 Ok(bound_images)
1686 }
1687}
1688
1689async fn ostree_install(state: &State, rootfs: &RootSetup, cleanup: Cleanup) -> Result<()> {
1690 let boot_uuid = rootfs
1692 .get_boot_uuid()?
1693 .or(rootfs.rootfs_uuid.as_deref())
1694 .ok_or_else(|| anyhow!("No uuid for boot/root"))?;
1695 tracing::debug!("boot uuid={boot_uuid}");
1696
1697 let bound_images = BoundImages::from_state(state).await?;
1698
1699 {
1702 let (sysroot, has_ostree) = initialize_ostree_root(state, rootfs).await?;
1703
1704 install_with_sysroot(
1705 state,
1706 rootfs,
1707 &sysroot,
1708 &boot_uuid,
1709 bound_images,
1710 has_ostree,
1711 )
1712 .await?;
1713 let ostree = sysroot.get_ostree()?;
1714
1715 if matches!(cleanup, Cleanup::TriggerOnNextBoot) {
1716 let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
1717 tracing::debug!("Writing {DESTRUCTIVE_CLEANUP}");
1718 sysroot_dir.atomic_write(DESTRUCTIVE_CLEANUP, b"")?;
1719 }
1720
1721 };
1724
1725 install_finalize(&rootfs.physical_root_path).await?;
1727
1728 Ok(())
1729}
1730
1731async fn install_to_filesystem_impl(
1732 state: &State,
1733 rootfs: &mut RootSetup,
1734 cleanup: Cleanup,
1735) -> Result<()> {
1736 if matches!(state.selinux_state, SELinuxFinalState::ForceTargetDisabled) {
1737 rootfs.kargs.extend(&Cmdline::from("selinux=0"));
1738 }
1739 let rootfs = &*rootfs;
1741
1742 match &rootfs.device_info.label {
1743 bootc_blockdev::PartitionType::Dos => crate::utils::medium_visibility_warning(
1744 "Installing to `dos` format partitions is not recommended",
1745 ),
1746 bootc_blockdev::PartitionType::Gpt => {
1747 }
1749 bootc_blockdev::PartitionType::Unknown(o) => {
1750 crate::utils::medium_visibility_warning(&format!("Unknown partition label {o}"))
1751 }
1752 }
1753
1754 if state.composefs_options.composefs_backend {
1755 let (id, verity) = initialize_composefs_repository(state, rootfs).await?;
1758 tracing::info!("id: {id}, verity: {}", verity.to_hex());
1759
1760 setup_composefs_boot(rootfs, state, &id).await?;
1761 } else {
1762 ostree_install(state, rootfs, cleanup).await?;
1763 }
1764
1765 if !rootfs.skip_finalize {
1767 let bootfs = rootfs.boot.as_ref().map(|_| ("boot", "boot"));
1768 for (fsname, fs) in std::iter::once(("root", ".")).chain(bootfs) {
1769 finalize_filesystem(fsname, &rootfs.physical_root, fs)?;
1770 }
1771 }
1772
1773 Ok(())
1774}
1775
1776fn installation_complete() {
1777 println!("Installation complete!");
1778}
1779
1780#[context("Installing to disk")]
1782#[cfg(feature = "install-to-disk")]
1783pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
1784 opts.validate()?;
1785
1786 const INSTALL_DISK_JOURNAL_ID: &str = "8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2";
1788 let source_image = opts
1789 .source_opts
1790 .source_imgref
1791 .as_ref()
1792 .map(|s| s.as_str())
1793 .unwrap_or("none");
1794 let target_device = opts.block_opts.device.as_str();
1795
1796 tracing::info!(
1797 message_id = INSTALL_DISK_JOURNAL_ID,
1798 bootc.source_image = source_image,
1799 bootc.target_device = target_device,
1800 bootc.via_loopback = if opts.via_loopback { "true" } else { "false" },
1801 "Starting disk installation from {} to {}",
1802 source_image,
1803 target_device
1804 );
1805
1806 let mut block_opts = opts.block_opts;
1807 let target_blockdev_meta = block_opts
1808 .device
1809 .metadata()
1810 .with_context(|| format!("Querying {}", &block_opts.device))?;
1811 if opts.via_loopback {
1812 if !opts.config_opts.generic_image {
1813 crate::utils::medium_visibility_warning(
1814 "Automatically enabling --generic-image when installing via loopback",
1815 );
1816 opts.config_opts.generic_image = true;
1817 }
1818 if !target_blockdev_meta.file_type().is_file() {
1819 anyhow::bail!(
1820 "Not a regular file (to be used via loopback): {}",
1821 block_opts.device
1822 );
1823 }
1824 } else if !target_blockdev_meta.file_type().is_block_device() {
1825 anyhow::bail!("Not a block device: {}", block_opts.device);
1826 }
1827
1828 let state = prepare_install(
1829 opts.config_opts,
1830 opts.source_opts,
1831 opts.target_opts,
1832 opts.composefs_opts,
1833 )
1834 .await?;
1835
1836 let (mut rootfs, loopback) = {
1838 let loopback_dev = if opts.via_loopback {
1839 let loopback_dev =
1840 bootc_blockdev::LoopbackDevice::new(block_opts.device.as_std_path())?;
1841 block_opts.device = loopback_dev.path().into();
1842 Some(loopback_dev)
1843 } else {
1844 None
1845 };
1846
1847 let state = state.clone();
1848 let rootfs = tokio::task::spawn_blocking(move || {
1849 baseline::install_create_rootfs(&state, block_opts)
1850 })
1851 .await??;
1852 (rootfs, loopback_dev)
1853 };
1854
1855 install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip).await?;
1856
1857 let (root_path, luksdev) = rootfs.into_storage();
1859 Task::new_and_run(
1860 "Unmounting filesystems",
1861 "umount",
1862 ["-R", root_path.as_str()],
1863 )?;
1864 if let Some(luksdev) = luksdev.as_deref() {
1865 Task::new_and_run("Closing root LUKS device", "cryptsetup", ["close", luksdev])?;
1866 }
1867
1868 if let Some(loopback_dev) = loopback {
1869 loopback_dev.close()?;
1870 }
1871
1872 if let Some(state) = Arc::into_inner(state) {
1874 state.consume()?;
1875 } else {
1876 tracing::warn!("Failed to consume state Arc");
1878 }
1879
1880 installation_complete();
1881
1882 Ok(())
1883}
1884
1885#[context("Requiring directory contains only mount points")]
1896fn require_dir_contains_only_mounts(parent_fd: &Dir, dir_name: &str) -> Result<()> {
1897 tracing::trace!("Checking directory {dir_name} for non-mount entries");
1898 let Some(dir_fd) = parent_fd.open_dir_noxdev(dir_name)? else {
1899 tracing::trace!("{dir_name} is a mount point");
1901 return Ok(());
1902 };
1903
1904 if dir_fd.entries()?.next().is_none() {
1905 anyhow::bail!("Found empty directory: {dir_name}");
1906 }
1907
1908 for entry in dir_fd.entries()? {
1909 tracing::trace!("Checking entry in {dir_name}");
1910 let entry = DirEntryUtf8::from_cap_std(entry?);
1911 let entry_name = entry.file_name()?;
1912
1913 if entry_name == LOST_AND_FOUND {
1914 continue;
1915 }
1916
1917 let etype = entry.file_type()?;
1918 if etype == FileType::dir() {
1919 require_dir_contains_only_mounts(&dir_fd, &entry_name)?;
1920 } else {
1921 anyhow::bail!("Found entry in {dir_name}: {entry_name}");
1922 }
1923 }
1924
1925 Ok(())
1926}
1927
1928#[context("Verifying empty rootfs")]
1929fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
1930 for e in rootfs_fd.entries()? {
1931 let e = DirEntryUtf8::from_cap_std(e?);
1932 let name = e.file_name()?;
1933 if name == LOST_AND_FOUND {
1934 continue;
1935 }
1936
1937 let etype = e.file_type()?;
1939 if etype == FileType::dir() {
1940 require_dir_contains_only_mounts(rootfs_fd, &name)?;
1941 } else {
1942 anyhow::bail!("Non-empty root filesystem; found {name:?}");
1943 }
1944 }
1945 Ok(())
1946}
1947
1948fn remove_all_in_dir_no_xdev(d: &Dir, mount_err: bool) -> Result<()> {
1952 for entry in d.entries()? {
1953 let entry = entry?;
1954 let name = entry.file_name();
1955 let etype = entry.file_type()?;
1956 if etype == FileType::dir() {
1957 if let Some(subdir) = d.open_dir_noxdev(&name)? {
1958 remove_all_in_dir_no_xdev(&subdir, mount_err)?;
1959 d.remove_dir(&name)?;
1960 } else if mount_err {
1961 anyhow::bail!("Found unexpected mount point {name:?}");
1962 }
1963 } else {
1964 d.remove_file_optional(&name)?;
1965 }
1966 }
1967 anyhow::Ok(())
1968}
1969
1970#[context("Removing boot directory content except loader dir on ostree")]
1971fn remove_all_except_loader_dirs(bootdir: &Dir, is_ostree: bool) -> Result<()> {
1972 let entries = bootdir
1973 .entries()
1974 .context("Reading boot directory entries")?;
1975
1976 for entry in entries {
1977 let entry = entry.context("Reading directory entry")?;
1978 let file_name = entry.file_name();
1979 let file_name = if let Some(n) = file_name.to_str() {
1980 n
1981 } else {
1982 anyhow::bail!("Invalid non-UTF8 filename: {file_name:?} in /boot");
1983 };
1984
1985 if is_ostree && file_name.starts_with("loader") {
1989 continue;
1990 }
1991
1992 let etype = entry.file_type()?;
1993 if etype == FileType::dir() {
1994 if let Some(subdir) = bootdir.open_dir_noxdev(&file_name)? {
1996 remove_all_in_dir_no_xdev(&subdir, false)
1997 .with_context(|| format!("Removing directory contents: {}", file_name))?;
1998 bootdir.remove_dir(&file_name)?;
1999 }
2000 } else {
2001 bootdir
2002 .remove_file_optional(&file_name)
2003 .with_context(|| format!("Removing file: {}", file_name))?;
2004 }
2005 }
2006 Ok(())
2007}
2008
2009#[context("Removing boot directory content")]
2010fn clean_boot_directories(rootfs: &Dir, rootfs_path: &Utf8Path, is_ostree: bool) -> Result<()> {
2011 let bootdir =
2012 crate::utils::open_dir_remount_rw(rootfs, BOOT.into()).context("Opening /boot")?;
2013
2014 if ARCH_USES_EFI {
2015 crate::bootloader::mount_esp_part(&rootfs, &rootfs_path, is_ostree)?;
2018 }
2019
2020 remove_all_except_loader_dirs(&bootdir, is_ostree).context("Emptying /boot")?;
2022
2023 if ARCH_USES_EFI {
2025 if let Some(efidir) = bootdir
2026 .open_dir_optional(crate::bootloader::EFI_DIR)
2027 .context("Opening /boot/efi")?
2028 {
2029 remove_all_in_dir_no_xdev(&efidir, false).context("Emptying EFI system partition")?;
2030 }
2031 }
2032
2033 Ok(())
2034}
2035
2036struct RootMountInfo {
2037 mount_spec: String,
2038 kargs: Vec<String>,
2039}
2040
2041fn find_root_args_to_inherit(
2044 cmdline: &bytes::Cmdline,
2045 root_info: &Filesystem,
2046) -> Result<RootMountInfo> {
2047 let root = cmdline
2049 .find_utf8("root")?
2050 .and_then(|p| p.value().map(|p| p.to_string()));
2051 let (mount_spec, kargs) = if let Some(root) = root {
2052 let rootflags = cmdline.find(ROOTFLAGS);
2053 let inherit_kargs = cmdline.find_all_starting_with(INITRD_ARG_PREFIX);
2054 (
2055 root,
2056 rootflags
2057 .into_iter()
2058 .chain(inherit_kargs)
2059 .map(|p| utf8::Parameter::try_from(p).map(|p| p.to_string()))
2060 .collect::<Result<Vec<_>, _>>()?,
2061 )
2062 } else {
2063 let uuid = root_info
2064 .uuid
2065 .as_deref()
2066 .ok_or_else(|| anyhow!("No filesystem uuid found in target root"))?;
2067 (format!("UUID={uuid}"), Vec::new())
2068 };
2069
2070 Ok(RootMountInfo { mount_spec, kargs })
2071}
2072
2073fn warn_on_host_root(rootfs_fd: &Dir) -> Result<()> {
2074 const DELAY_SECONDS: u64 = 20;
2076
2077 let host_root_dfd = &Dir::open_ambient_dir("/proc/1/root", cap_std::ambient_authority())?;
2078 let host_root_devstat = rustix::fs::fstatvfs(host_root_dfd)?;
2079 let target_devstat = rustix::fs::fstatvfs(rootfs_fd)?;
2080 if host_root_devstat.f_fsid != target_devstat.f_fsid {
2081 tracing::debug!("Not the host root");
2082 return Ok(());
2083 }
2084 let dashes = "----------------------------";
2085 let timeout = Duration::from_secs(DELAY_SECONDS);
2086 eprintln!("{dashes}");
2087 crate::utils::medium_visibility_warning(
2088 "WARNING: This operation will OVERWRITE THE BOOTED HOST ROOT FILESYSTEM and is NOT REVERSIBLE.",
2089 );
2090 eprintln!("Waiting {timeout:?} to continue; interrupt (Control-C) to cancel.");
2091 eprintln!("{dashes}");
2092
2093 let bar = indicatif::ProgressBar::new_spinner();
2094 bar.enable_steady_tick(Duration::from_millis(100));
2095 std::thread::sleep(timeout);
2096 bar.finish();
2097
2098 Ok(())
2099}
2100
2101pub enum Cleanup {
2102 Skip,
2103 TriggerOnNextBoot,
2104}
2105
2106#[context("Installing to filesystem")]
2108pub(crate) async fn install_to_filesystem(
2109 opts: InstallToFilesystemOpts,
2110 targeting_host_root: bool,
2111 cleanup: Cleanup,
2112) -> Result<()> {
2113 const INSTALL_FILESYSTEM_JOURNAL_ID: &str = "9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3";
2115 let source_image = opts
2116 .source_opts
2117 .source_imgref
2118 .as_ref()
2119 .map(|s| s.as_str())
2120 .unwrap_or("none");
2121 let target_path = opts.filesystem_opts.root_path.as_str();
2122
2123 tracing::info!(
2124 message_id = INSTALL_FILESYSTEM_JOURNAL_ID,
2125 bootc.source_image = source_image,
2126 bootc.target_path = target_path,
2127 bootc.targeting_host_root = if targeting_host_root { "true" } else { "false" },
2128 "Starting filesystem installation from {} to {}",
2129 source_image,
2130 target_path
2131 );
2132
2133 let state = prepare_install(
2139 opts.config_opts,
2140 opts.source_opts,
2141 opts.target_opts,
2142 opts.composefs_opts,
2143 )
2144 .await?;
2145
2146 let mut fsopts = opts.filesystem_opts;
2148
2149 if targeting_host_root
2152 && fsopts.root_path.as_str() == ALONGSIDE_ROOT_MOUNT
2153 && !fsopts.root_path.try_exists()?
2154 {
2155 tracing::debug!("Mounting host / to {ALONGSIDE_ROOT_MOUNT}");
2156 std::fs::create_dir(ALONGSIDE_ROOT_MOUNT)?;
2157 bootc_mount::bind_mount_from_pidns(
2158 bootc_mount::PID1,
2159 "/".into(),
2160 ALONGSIDE_ROOT_MOUNT.into(),
2161 true,
2162 )
2163 .context("Mounting host / to {ALONGSIDE_ROOT_MOUNT}")?;
2164 }
2165
2166 let target_root_path = fsopts.root_path.clone();
2167 let target_rootfs_fd =
2169 Dir::open_ambient_dir(&target_root_path, cap_std::ambient_authority())
2170 .with_context(|| format!("Opening target root directory {target_root_path}"))?;
2171
2172 tracing::debug!("Target root filesystem: {target_root_path}");
2173
2174 if let Some(false) = target_rootfs_fd.is_mountpoint(".")? {
2175 anyhow::bail!("Not a mountpoint: {target_root_path}");
2176 }
2177
2178 {
2180 let root_path = &fsopts.root_path;
2181 let st = root_path
2182 .symlink_metadata()
2183 .with_context(|| format!("Querying target filesystem {root_path}"))?;
2184 if !st.is_dir() {
2185 anyhow::bail!("Not a directory: {root_path}");
2186 }
2187 }
2188
2189 if !fsopts.acknowledge_destructive {
2191 warn_on_host_root(&target_rootfs_fd)?;
2192 }
2193
2194 let possible_physical_root = fsopts.root_path.join("sysroot");
2197 let possible_ostree_dir = possible_physical_root.join("ostree");
2198 let is_already_ostree = possible_ostree_dir.exists();
2199 if is_already_ostree {
2200 tracing::debug!(
2201 "ostree detected in {possible_ostree_dir}, assuming target is a deployment root and using {possible_physical_root}"
2202 );
2203 fsopts.root_path = possible_physical_root;
2204 };
2205
2206 let rootfs_fd = if is_already_ostree {
2209 let root_path = &fsopts.root_path;
2210 let rootfs_fd = Dir::open_ambient_dir(&fsopts.root_path, cap_std::ambient_authority())
2211 .with_context(|| format!("Opening target root directory {root_path}"))?;
2212
2213 tracing::debug!("Root filesystem: {root_path}");
2214
2215 if let Some(false) = rootfs_fd.is_mountpoint(".")? {
2216 anyhow::bail!("Not a mountpoint: {root_path}");
2217 }
2218 rootfs_fd
2219 } else {
2220 target_rootfs_fd.try_clone()?
2221 };
2222
2223 match fsopts.replace {
2224 Some(ReplaceMode::Wipe) => {
2225 let rootfs_fd = rootfs_fd.try_clone()?;
2226 println!("Wiping contents of root");
2227 tokio::task::spawn_blocking(move || remove_all_in_dir_no_xdev(&rootfs_fd, true))
2228 .await??;
2229 }
2230 Some(ReplaceMode::Alongside) => {
2231 clean_boot_directories(&target_rootfs_fd, &target_root_path, is_already_ostree)?
2232 }
2233 None => require_empty_rootdir(&rootfs_fd)?,
2234 }
2235
2236 let inspect = bootc_mount::inspect_filesystem(&fsopts.root_path)?;
2238
2239 let config_root_mount_spec = state
2244 .install_config
2245 .as_ref()
2246 .and_then(|c| c.root_mount_spec.as_ref());
2247 let root_info = if let Some(s) = fsopts.root_mount_spec.as_ref().or(config_root_mount_spec) {
2248 RootMountInfo {
2249 mount_spec: s.to_string(),
2250 kargs: Vec::new(),
2251 }
2252 } else if targeting_host_root {
2253 let cmdline = bytes::Cmdline::from_proc()?;
2255 find_root_args_to_inherit(&cmdline, &inspect)?
2256 } else {
2257 let uuid = inspect
2260 .uuid
2261 .as_deref()
2262 .ok_or_else(|| anyhow!("No filesystem uuid found in target root"))?;
2263 let kargs = match inspect.fstype.as_str() {
2264 "btrfs" => {
2265 let subvol = crate::utils::find_mount_option(&inspect.options, "subvol");
2266 subvol
2267 .map(|vol| format!("rootflags=subvol={vol}"))
2268 .into_iter()
2269 .collect::<Vec<_>>()
2270 }
2271 _ => Vec::new(),
2272 };
2273 RootMountInfo {
2274 mount_spec: format!("UUID={uuid}"),
2275 kargs,
2276 }
2277 };
2278 tracing::debug!("Root mount: {} {:?}", root_info.mount_spec, root_info.kargs);
2279
2280 let boot_is_mount = {
2281 let root_dev = rootfs_fd.dir_metadata()?.dev();
2282 let boot_dev = target_rootfs_fd
2283 .symlink_metadata_optional(BOOT)?
2284 .ok_or_else(|| {
2285 anyhow!("No /{BOOT} directory found in root; this is is currently required")
2286 })?
2287 .dev();
2288 tracing::debug!("root_dev={root_dev} boot_dev={boot_dev}");
2289 root_dev != boot_dev
2290 };
2291 let boot_uuid = if boot_is_mount {
2293 let boot_path = target_root_path.join(BOOT);
2294 tracing::debug!("boot_path={boot_path}");
2295 let u = bootc_mount::inspect_filesystem(&boot_path)
2296 .with_context(|| format!("Inspecting /{BOOT}"))?
2297 .uuid
2298 .ok_or_else(|| anyhow!("No UUID found for /{BOOT}"))?;
2299 Some(u)
2300 } else {
2301 None
2302 };
2303 tracing::debug!("boot UUID: {boot_uuid:?}");
2304
2305 let backing_device = {
2308 let mut dev = inspect.source;
2309 loop {
2310 tracing::debug!("Finding parents for {dev}");
2311 let mut parents = bootc_blockdev::find_parent_devices(&dev)?.into_iter();
2312 let Some(parent) = parents.next() else {
2313 break;
2314 };
2315 if let Some(next) = parents.next() {
2316 anyhow::bail!(
2317 "Found multiple parent devices {parent} and {next}; not currently supported"
2318 );
2319 }
2320 dev = parent;
2321 }
2322 dev
2323 };
2324 tracing::debug!("Backing device: {backing_device}");
2325 let device_info = bootc_blockdev::partitions_of(Utf8Path::new(&backing_device))?;
2326
2327 let rootarg = format!("root={}", root_info.mount_spec);
2328 let config_boot_mount_spec = state
2330 .install_config
2331 .as_ref()
2332 .and_then(|c| c.boot_mount_spec.as_ref());
2333 let mut boot = if let Some(spec) = fsopts.boot_mount_spec.as_ref().or(config_boot_mount_spec) {
2334 if spec.is_empty() {
2337 None
2338 } else {
2339 Some(MountSpec::new(&spec, "/boot"))
2340 }
2341 } else {
2342 read_boot_fstab_entry(&rootfs_fd)?
2345 .filter(|spec| spec.get_source_uuid().is_some())
2346 .or_else(|| {
2347 boot_uuid
2348 .as_deref()
2349 .map(|boot_uuid| MountSpec::new_uuid_src(boot_uuid, "/boot"))
2350 })
2351 };
2352 if let Some(boot) = boot.as_mut() {
2355 boot.push_option("ro");
2356 }
2357 let bootarg = boot.as_ref().map(|boot| format!("boot={}", &boot.source));
2360
2361 let mut kargs = if root_info.mount_spec.is_empty() {
2364 Vec::new()
2365 } else {
2366 [rootarg]
2367 .into_iter()
2368 .chain(root_info.kargs)
2369 .collect::<Vec<_>>()
2370 };
2371
2372 kargs.push(RW_KARG.to_string());
2373
2374 if let Some(bootarg) = bootarg {
2375 kargs.push(bootarg);
2376 }
2377
2378 let kargs = Cmdline::from(kargs.join(" "));
2379
2380 let skip_finalize =
2381 matches!(fsopts.replace, Some(ReplaceMode::Alongside)) || fsopts.skip_finalize;
2382 let mut rootfs = RootSetup {
2383 #[cfg(feature = "install-to-disk")]
2384 luks_device: None,
2385 device_info,
2386 physical_root_path: fsopts.root_path,
2387 physical_root: rootfs_fd,
2388 target_root_path: Some(target_root_path.clone()),
2389 rootfs_uuid: inspect.uuid.clone(),
2390 boot,
2391 kargs,
2392 skip_finalize,
2393 };
2394
2395 install_to_filesystem_impl(&state, &mut rootfs, cleanup).await?;
2396
2397 drop(rootfs);
2399
2400 installation_complete();
2401
2402 Ok(())
2403}
2404
2405pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) -> Result<()> {
2406 const INSTALL_EXISTING_ROOT_JOURNAL_ID: &str = "7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1";
2408 let source_image = opts
2409 .source_opts
2410 .source_imgref
2411 .as_ref()
2412 .map(|s| s.as_str())
2413 .unwrap_or("none");
2414 let target_path = opts.root_path.as_str();
2415
2416 tracing::info!(
2417 message_id = INSTALL_EXISTING_ROOT_JOURNAL_ID,
2418 bootc.source_image = source_image,
2419 bootc.target_path = target_path,
2420 bootc.cleanup = if opts.cleanup {
2421 "trigger_on_next_boot"
2422 } else {
2423 "skip"
2424 },
2425 "Starting installation to existing root from {} to {}",
2426 source_image,
2427 target_path
2428 );
2429
2430 let cleanup = match opts.cleanup {
2431 true => Cleanup::TriggerOnNextBoot,
2432 false => Cleanup::Skip,
2433 };
2434
2435 let opts = InstallToFilesystemOpts {
2436 filesystem_opts: InstallTargetFilesystemOpts {
2437 root_path: opts.root_path,
2438 root_mount_spec: None,
2439 boot_mount_spec: None,
2440 replace: opts.replace,
2441 skip_finalize: true,
2442 acknowledge_destructive: opts.acknowledge_destructive,
2443 },
2444 source_opts: opts.source_opts,
2445 target_opts: opts.target_opts,
2446 config_opts: opts.config_opts,
2447 composefs_opts: opts.composefs_opts,
2448 };
2449
2450 install_to_filesystem(opts, true, cleanup).await
2451}
2452
2453fn read_boot_fstab_entry(root: &Dir) -> Result<Option<MountSpec>> {
2455 let fstab_path = "etc/fstab";
2456 let fstab = match root.open_optional(fstab_path)? {
2457 Some(f) => f,
2458 None => return Ok(None),
2459 };
2460
2461 let reader = std::io::BufReader::new(fstab);
2462 for line in std::io::BufRead::lines(reader) {
2463 let line = line?;
2464 let line = line.trim();
2465
2466 if line.is_empty() || line.starts_with('#') {
2468 continue;
2469 }
2470
2471 let spec = MountSpec::from_str(line)?;
2473
2474 if spec.target == "/boot" {
2476 return Ok(Some(spec));
2477 }
2478 }
2479
2480 Ok(None)
2481}
2482
2483pub(crate) async fn install_reset(opts: InstallResetOpts) -> Result<()> {
2484 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2485 if !opts.experimental {
2486 anyhow::bail!("This command requires --experimental");
2487 }
2488
2489 let prog: ProgressWriter = opts.progress.try_into()?;
2490
2491 let sysroot = &crate::cli::get_storage().await?;
2492 let ostree = sysroot.get_ostree()?;
2493 let repo = &ostree.repo();
2494 let (booted_ostree, _deployments, host) = crate::status::get_status_require_booted(ostree)?;
2495
2496 let stateroots = list_stateroots(ostree)?;
2497 let target_stateroot = if let Some(s) = opts.stateroot {
2498 s
2499 } else {
2500 let now = chrono::Utc::now();
2501 let r = allocate_new_stateroot(&ostree, &stateroots, now)?;
2502 r.name
2503 };
2504
2505 let booted_stateroot = booted_ostree.stateroot();
2506 assert!(booted_stateroot.as_str() != target_stateroot);
2507 let (fetched, spec) = if let Some(target) = opts.target_opts.imageref()? {
2508 let mut new_spec = host.spec;
2509 new_spec.image = Some(target.into());
2510 let fetched = crate::deploy::pull(
2511 repo,
2512 &new_spec.image.as_ref().unwrap(),
2513 None,
2514 opts.quiet,
2515 prog.clone(),
2516 )
2517 .await?;
2518 (fetched, new_spec)
2519 } else {
2520 let imgstate = host
2521 .status
2522 .booted
2523 .map(|b| b.query_image(repo))
2524 .transpose()?
2525 .flatten()
2526 .ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
2527 (Box::new((*imgstate).into()), host.spec)
2528 };
2529 let spec = crate::deploy::RequiredHostSpec::from_spec(&spec)?;
2530
2531 let mut kargs = crate::bootc_kargs::get_kargs_in_root(rootfs, std::env::consts::ARCH)?;
2534
2535 if !opts.no_root_kargs {
2537 let bootcfg = booted_ostree
2538 .deployment
2539 .bootconfig()
2540 .ok_or_else(|| anyhow!("Missing bootcfg for booted deployment"))?;
2541 if let Some(options) = bootcfg.get("options") {
2542 let options_cmdline = Cmdline::from(options.as_str());
2543 let root_kargs = crate::bootc_kargs::root_args_from_cmdline(&options_cmdline);
2544 kargs.extend(&root_kargs);
2545 }
2546 }
2547
2548 if let Some(user_kargs) = opts.karg.as_ref() {
2550 for karg in user_kargs {
2551 kargs.extend(karg);
2552 }
2553 }
2554
2555 let from = MergeState::Reset {
2556 stateroot: target_stateroot.clone(),
2557 kargs,
2558 };
2559 crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone(), false).await?;
2560
2561 if let Some(boot_spec) = read_boot_fstab_entry(rootfs)? {
2563 let staged_deployment = ostree
2564 .staged_deployment()
2565 .ok_or_else(|| anyhow!("No staged deployment found"))?;
2566 let deployment_path = ostree.deployment_dirpath(&staged_deployment);
2567 let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
2568 let deployment_root = sysroot_dir.open_dir(&deployment_path)?;
2569
2570 crate::lsm::atomic_replace_labeled(
2572 &deployment_root,
2573 "etc/fstab",
2574 0o644.into(),
2575 None,
2576 |w| writeln!(w, "{}", boot_spec.to_fstab()).map_err(Into::into),
2577 )?;
2578
2579 tracing::debug!(
2580 "Copied /boot entry to new stateroot: {}",
2581 boot_spec.to_fstab()
2582 );
2583 }
2584
2585 sysroot.update_mtime()?;
2586
2587 if opts.apply {
2588 crate::reboot::reboot()?;
2589 }
2590 Ok(())
2591}
2592
2593pub(crate) async fn install_finalize(target: &Utf8Path) -> Result<()> {
2595 const INSTALL_FINALIZE_JOURNAL_ID: &str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0";
2597
2598 tracing::info!(
2599 message_id = INSTALL_FINALIZE_JOURNAL_ID,
2600 bootc.target_path = target.as_str(),
2601 "Starting installation finalization for target: {}",
2602 target
2603 );
2604
2605 crate::cli::require_root(false)?;
2606 let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(target)));
2607 sysroot.load(gio::Cancellable::NONE)?;
2608 let deployments = sysroot.deployments();
2609 if deployments.is_empty() {
2611 anyhow::bail!("Failed to find deployment in {target}");
2612 }
2613
2614 tracing::info!(
2616 message_id = INSTALL_FINALIZE_JOURNAL_ID,
2617 bootc.target_path = target.as_str(),
2618 "Successfully finalized installation for target: {}",
2619 target
2620 );
2621
2622 Ok(())
2626}
2627
2628#[cfg(test)]
2629mod tests {
2630 use super::*;
2631
2632 #[test]
2633 fn install_opts_serializable() {
2634 let c: InstallToDiskOpts = serde_json::from_value(serde_json::json!({
2635 "device": "/dev/vda"
2636 }))
2637 .unwrap();
2638 assert_eq!(c.block_opts.device, "/dev/vda");
2639 }
2640
2641 #[test]
2642 fn test_mountspec() {
2643 let mut ms = MountSpec::new("/dev/vda4", "/boot");
2644 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto defaults 0 0");
2645 ms.push_option("ro");
2646 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto ro 0 0");
2647 ms.push_option("relatime");
2648 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto ro,relatime 0 0");
2649 }
2650
2651 #[test]
2652 fn test_gather_root_args() {
2653 let inspect = Filesystem {
2655 source: "/dev/vda4".into(),
2656 target: "/".into(),
2657 fstype: "xfs".into(),
2658 maj_min: "252:4".into(),
2659 options: "rw".into(),
2660 uuid: Some("965eb3c7-5a3f-470d-aaa2-1bcf04334bc6".into()),
2661 children: None,
2662 };
2663 let kargs = bytes::Cmdline::from("");
2664 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
2665 assert_eq!(r.mount_spec, "UUID=965eb3c7-5a3f-470d-aaa2-1bcf04334bc6");
2666
2667 let kargs = bytes::Cmdline::from(
2668 "root=/dev/mapper/root rw someother=karg rd.lvm.lv=root systemd.debug=1",
2669 );
2670
2671 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
2673 assert_eq!(r.mount_spec, "/dev/mapper/root");
2674 assert_eq!(r.kargs.len(), 1);
2675 assert_eq!(r.kargs[0], "rd.lvm.lv=root");
2676
2677 let kargs = bytes::Cmdline::from(
2679 b"root=/dev/mapper/root rw non-utf8=\xff rd.lvm.lv=root systemd.debug=1",
2680 );
2681 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
2682 assert_eq!(r.mount_spec, "/dev/mapper/root");
2683 assert_eq!(r.kargs.len(), 1);
2684 assert_eq!(r.kargs[0], "rd.lvm.lv=root");
2685
2686 let kargs = bytes::Cmdline::from(
2688 b"root=/dev/mapper/ro\xffot rw non-utf8=\xff rd.lvm.lv=root systemd.debug=1",
2689 );
2690 let r = find_root_args_to_inherit(&kargs, &inspect);
2691 assert!(r.is_err());
2692
2693 let kargs = bytes::Cmdline::from(
2695 b"root=/dev/mapper/root rw non-utf8=\xff rd.lvm.lv=ro\xffot systemd.debug=1",
2696 );
2697 let r = find_root_args_to_inherit(&kargs, &inspect);
2698 assert!(r.is_err());
2699 }
2700
2701 #[test]
2704 fn test_remove_all_noxdev() -> Result<()> {
2705 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2706
2707 td.create_dir_all("foo/bar/baz")?;
2708 td.write("foo/bar/baz/test", b"sometest")?;
2709 td.symlink_contents("/absolute-nonexistent-link", "somelink")?;
2710 td.write("toptestfile", b"othertestcontents")?;
2711
2712 remove_all_in_dir_no_xdev(&td, true).unwrap();
2713
2714 assert_eq!(td.entries()?.count(), 0);
2715
2716 Ok(())
2717 }
2718
2719 #[test]
2720 fn test_read_boot_fstab_entry() -> Result<()> {
2721 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2722
2723 assert!(read_boot_fstab_entry(&td)?.is_none());
2725
2726 td.create_dir("etc")?;
2728 td.write("etc/fstab", "UUID=test-uuid / ext4 defaults 0 0\n")?;
2729 assert!(read_boot_fstab_entry(&td)?.is_none());
2730
2731 let fstab_content = "\
2733# /etc/fstab
2734UUID=root-uuid / ext4 defaults 0 0
2735UUID=boot-uuid /boot ext4 ro 0 0
2736UUID=home-uuid /home ext4 defaults 0 0
2737";
2738 td.write("etc/fstab", fstab_content)?;
2739 let boot_spec = read_boot_fstab_entry(&td)?.unwrap();
2740 assert_eq!(boot_spec.source, "UUID=boot-uuid");
2741 assert_eq!(boot_spec.target, "/boot");
2742 assert_eq!(boot_spec.fstype, "ext4");
2743 assert_eq!(boot_spec.options, Some("ro".to_string()));
2744
2745 let fstab_content = "\
2747# /etc/fstab
2748# Created by anaconda
2749UUID=root-uuid / ext4 defaults 0 0
2750# Boot partition
2751UUID=boot-uuid /boot ext4 defaults 0 0
2752";
2753 td.write("etc/fstab", fstab_content)?;
2754 let boot_spec = read_boot_fstab_entry(&td)?.unwrap();
2755 assert_eq!(boot_spec.source, "UUID=boot-uuid");
2756 assert_eq!(boot_spec.target, "/boot");
2757
2758 Ok(())
2759 }
2760
2761 #[test]
2762 fn test_require_dir_contains_only_mounts() -> Result<()> {
2763 {
2765 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2766 td.create_dir("empty")?;
2767 assert!(require_dir_contains_only_mounts(&td, "empty").is_err());
2768 }
2769
2770 {
2772 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2773 td.create_dir_all("var/lost+found")?;
2774 assert!(require_dir_contains_only_mounts(&td, "var").is_ok());
2775 }
2776
2777 {
2779 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2780 td.create_dir("var")?;
2781 td.write("var/test.txt", b"content")?;
2782 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2783 }
2784
2785 {
2787 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2788 td.create_dir_all("var/lib/containers")?;
2789 td.write("var/lib/containers/storage.db", b"data")?;
2790 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2791 }
2792
2793 {
2795 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2796 td.create_dir_all("boot/grub2")?;
2797 td.write("boot/grub2/grub.cfg", b"config")?;
2798 assert!(require_dir_contains_only_mounts(&td, "boot").is_err());
2799 }
2800
2801 {
2803 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2804 td.create_dir_all("var/lib/containers")?;
2805 td.create_dir_all("var/log/journal")?;
2806 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2807 }
2808
2809 {
2811 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2812 td.create_dir_all("var/lost+found")?;
2813 td.write("var/data.txt", b"content")?;
2814 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2815 }
2816
2817 {
2819 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2820 td.create_dir("var")?;
2821 td.symlink_contents("../usr/lib", "var/lib")?;
2822 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2823 }
2824
2825 {
2827 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2828 td.create_dir_all("var/lib/containers/storage/overlay")?;
2829 td.write("var/lib/containers/storage/overlay/file.txt", b"data")?;
2830 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2831 }
2832
2833 Ok(())
2834 }
2835}