1mod aleph;
143#[cfg(feature = "install-to-disk")]
144pub(crate) mod baseline;
145pub(crate) mod completion;
146pub(crate) mod config;
147mod osbuild;
148pub(crate) mod osconfig;
149
150use std::collections::HashMap;
151use std::io::Write;
152use std::os::fd::{AsFd, AsRawFd};
153use std::os::unix::process::CommandExt;
154use std::path::Path;
155use std::process;
156use std::process::Command;
157use std::str::FromStr;
158use std::sync::Arc;
159use std::time::Duration;
160
161use aleph::InstallAleph;
162use anyhow::{Context, Result, anyhow, ensure};
163use bootc_kernel_cmdline::utf8::{Cmdline, CmdlineOwned};
164use bootc_utils::CommandRunExt;
165use camino::Utf8Path;
166use camino::Utf8PathBuf;
167use canon_json::CanonJsonSerialize;
168use cap_std::fs::{Dir, MetadataExt};
169use cap_std_ext::cap_std;
170use cap_std_ext::cap_std::fs::FileType;
171use cap_std_ext::cap_std::fs_utf8::DirEntry as DirEntryUtf8;
172use cap_std_ext::cap_tempfile::TempDir;
173use cap_std_ext::cmdext::CapStdExtCommandExt;
174use cap_std_ext::prelude::CapStdExtDirExt;
175use clap::ValueEnum;
176use fn_error_context::context;
177use ostree::gio;
178use ostree_ext::ostree;
179use ostree_ext::ostree_prepareroot::{ComposefsState, Tristate};
180use ostree_ext::prelude::Cast;
181use ostree_ext::sysroot::{SysrootLock, allocate_new_stateroot, list_stateroots};
182use ostree_ext::{container as ostree_container, ostree_prepareroot};
183#[cfg(feature = "install-to-disk")]
184use rustix::fs::FileTypeExt;
185use rustix::fs::MetadataExt as _;
186use serde::{Deserialize, Serialize};
187
188#[cfg(feature = "install-to-disk")]
189use self::baseline::InstallBlockDeviceOpts;
190use crate::bootc_composefs::{boot::setup_composefs_boot, repo::initialize_composefs_repository};
191use crate::boundimage::{BoundImage, ResolvedBoundImage};
192use crate::containerenv::ContainerExecutionInfo;
193use crate::deploy::{
194 MergeState, PreparedImportMeta, PreparedPullResult, prepare_for_pull, pull_from_prepared,
195};
196use crate::lsm;
197use crate::progress_jsonl::ProgressWriter;
198use crate::spec::{Bootloader, ImageReference};
199use crate::store::Storage;
200use crate::task::Task;
201use crate::utils::sigpolicy_from_opt;
202use bootc_kernel_cmdline::{INITRD_ARG_PREFIX, ROOTFLAGS, bytes, utf8};
203use bootc_mount::Filesystem;
204use composefs::fsverity::FsVerityHashValue;
205
206pub(crate) const BOOT: &str = "boot";
208#[cfg(feature = "install-to-disk")]
210const RUN_BOOTC: &str = "/run/bootc";
211const ALONGSIDE_ROOT_MOUNT: &str = "/target";
213pub(crate) const DESTRUCTIVE_CLEANUP: &str = "etc/bootc-destructive-cleanup";
215const LOST_AND_FOUND: &str = "lost+found";
217const OSTREE_COMPOSEFS_SUPER: &str = ".ostree.cfs";
219const SELINUXFS: &str = "/sys/fs/selinux";
221pub(crate) const EFIVARFS: &str = "/sys/firmware/efi/efivars";
223pub(crate) const ARCH_USES_EFI: bool = cfg!(any(target_arch = "x86_64", target_arch = "aarch64"));
224
225pub(crate) const EFI_LOADER_INFO: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
226
227const DEFAULT_REPO_CONFIG: &[(&str, &str)] = &[
228 ("sysroot.bootloader", "none"),
230 ("sysroot.bootprefix", "true"),
233 ("sysroot.readonly", "true"),
234];
235
236pub(crate) const RW_KARG: &str = "rw";
238
239#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
240pub(crate) struct InstallTargetOpts {
241 #[clap(long, default_value = "registry")]
245 #[serde(default)]
246 pub(crate) target_transport: String,
247
248 #[clap(long)]
250 pub(crate) target_imgref: Option<String>,
251
252 #[clap(long, hide = true)]
262 #[serde(default)]
263 pub(crate) target_no_signature_verification: bool,
264
265 #[clap(long)]
269 #[serde(default)]
270 pub(crate) enforce_container_sigpolicy: bool,
271
272 #[clap(long)]
275 #[serde(default)]
276 pub(crate) run_fetch_check: bool,
277
278 #[clap(long)]
281 #[serde(default)]
282 pub(crate) skip_fetch_check: bool,
283
284 #[clap(long = "experimental-unified-storage", hide = true)]
290 #[serde(default)]
291 pub(crate) unified_storage_exp: bool,
292}
293
294#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
295pub(crate) struct InstallSourceOpts {
296 #[clap(long)]
303 pub(crate) source_imgref: Option<String>,
304}
305
306#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
307#[serde(rename_all = "kebab-case")]
308pub(crate) enum BoundImagesOpt {
309 #[default]
311 Stored,
312 #[clap(hide = true)]
313 Skip,
315 Pull,
319}
320
321impl std::fmt::Display for BoundImagesOpt {
322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 self.to_possible_value().unwrap().get_name().fmt(f)
324 }
325}
326
327#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
328pub(crate) struct InstallConfigOpts {
329 #[clap(long)]
334 #[serde(default)]
335 pub(crate) disable_selinux: bool,
336
337 #[clap(long)]
341 pub(crate) karg: Option<Vec<CmdlineOwned>>,
342
343 #[clap(long)]
351 root_ssh_authorized_keys: Option<Utf8PathBuf>,
352
353 #[clap(long)]
359 #[serde(default)]
360 pub(crate) generic_image: bool,
361
362 #[clap(long)]
364 #[serde(default)]
365 #[arg(default_value_t)]
366 pub(crate) bound_images: BoundImagesOpt,
367
368 #[clap(long)]
370 pub(crate) stateroot: Option<String>,
371
372 #[clap(long)]
374 #[serde(default)]
375 pub(crate) bootupd_skip_boot_uuid: bool,
376}
377
378#[derive(Debug, Default, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
379pub(crate) struct InstallComposefsOpts {
380 #[clap(long, default_value_t)]
382 #[serde(default)]
383 pub(crate) composefs_backend: bool,
384
385 #[clap(long, default_value_t, requires = "composefs_backend")]
387 #[serde(default)]
388 pub(crate) insecure: bool,
389
390 #[clap(long, requires = "composefs_backend")]
392 #[serde(default)]
393 pub(crate) bootloader: Option<Bootloader>,
394
395 #[clap(long, requires = "composefs_backend")]
398 #[serde(default)]
399 pub(crate) uki_addon: Option<Vec<String>>,
400}
401
402#[cfg(feature = "install-to-disk")]
403#[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
404pub(crate) struct InstallToDiskOpts {
405 #[clap(flatten)]
406 #[serde(flatten)]
407 pub(crate) block_opts: InstallBlockDeviceOpts,
408
409 #[clap(flatten)]
410 #[serde(flatten)]
411 pub(crate) source_opts: InstallSourceOpts,
412
413 #[clap(flatten)]
414 #[serde(flatten)]
415 pub(crate) target_opts: InstallTargetOpts,
416
417 #[clap(flatten)]
418 #[serde(flatten)]
419 pub(crate) config_opts: InstallConfigOpts,
420
421 #[clap(long)]
423 #[serde(default)]
424 pub(crate) via_loopback: bool,
425
426 #[clap(flatten)]
427 #[serde(flatten)]
428 pub(crate) composefs_opts: InstallComposefsOpts,
429}
430
431#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
432#[serde(rename_all = "kebab-case")]
433pub(crate) enum ReplaceMode {
434 Wipe,
437 Alongside,
445}
446
447impl std::fmt::Display for ReplaceMode {
448 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
449 self.to_possible_value().unwrap().get_name().fmt(f)
450 }
451}
452
453#[derive(Debug, Clone, clap::Args, PartialEq, Eq)]
455pub(crate) struct InstallTargetFilesystemOpts {
456 pub(crate) root_path: Utf8PathBuf,
461
462 #[clap(long)]
466 pub(crate) root_mount_spec: Option<String>,
467
468 #[clap(long)]
473 pub(crate) boot_mount_spec: Option<String>,
474
475 #[clap(long)]
478 pub(crate) replace: Option<ReplaceMode>,
479
480 #[clap(long)]
482 pub(crate) acknowledge_destructive: bool,
483
484 #[clap(long)]
488 pub(crate) skip_finalize: bool,
489}
490
491#[derive(Debug, Clone, clap::Parser, PartialEq, Eq)]
492pub(crate) struct InstallToFilesystemOpts {
493 #[clap(flatten)]
494 pub(crate) filesystem_opts: InstallTargetFilesystemOpts,
495
496 #[clap(flatten)]
497 pub(crate) source_opts: InstallSourceOpts,
498
499 #[clap(flatten)]
500 pub(crate) target_opts: InstallTargetOpts,
501
502 #[clap(flatten)]
503 pub(crate) config_opts: InstallConfigOpts,
504
505 #[clap(flatten)]
506 pub(crate) composefs_opts: InstallComposefsOpts,
507}
508
509#[derive(Debug, Clone, clap::Parser, PartialEq, Eq)]
510pub(crate) struct InstallToExistingRootOpts {
511 #[clap(long, default_value = "alongside")]
513 pub(crate) replace: Option<ReplaceMode>,
514
515 #[clap(flatten)]
516 pub(crate) source_opts: InstallSourceOpts,
517
518 #[clap(flatten)]
519 pub(crate) target_opts: InstallTargetOpts,
520
521 #[clap(flatten)]
522 pub(crate) config_opts: InstallConfigOpts,
523
524 #[clap(long)]
526 pub(crate) acknowledge_destructive: bool,
527
528 #[clap(long)]
531 pub(crate) cleanup: bool,
532
533 #[clap(default_value = ALONGSIDE_ROOT_MOUNT)]
537 pub(crate) root_path: Utf8PathBuf,
538
539 #[clap(flatten)]
540 pub(crate) composefs_opts: InstallComposefsOpts,
541}
542
543#[derive(Debug, clap::Parser, PartialEq, Eq)]
544pub(crate) struct InstallResetOpts {
545 #[clap(long)]
547 pub(crate) experimental: bool,
548
549 #[clap(flatten)]
550 pub(crate) source_opts: InstallSourceOpts,
551
552 #[clap(flatten)]
553 pub(crate) target_opts: InstallTargetOpts,
554
555 #[clap(long)]
559 pub(crate) stateroot: Option<String>,
560
561 #[clap(long)]
563 pub(crate) quiet: bool,
564
565 #[clap(flatten)]
566 pub(crate) progress: crate::cli::ProgressOptions,
567
568 #[clap(long)]
574 pub(crate) apply: bool,
575
576 #[clap(long)]
578 no_root_kargs: bool,
579
580 #[clap(long)]
584 karg: Option<Vec<CmdlineOwned>>,
585}
586
587#[derive(Debug, clap::Parser, PartialEq, Eq)]
588pub(crate) struct InstallPrintConfigurationOpts {
589 #[clap(long)]
593 pub(crate) all: bool,
594}
595
596#[derive(Debug, Clone)]
598pub(crate) struct SourceInfo {
599 pub(crate) imageref: ostree_container::ImageReference,
601 pub(crate) digest: Option<String>,
603 pub(crate) selinux: bool,
605 pub(crate) in_host_mountns: bool,
607}
608
609#[derive(Debug)]
611pub(crate) struct State {
612 pub(crate) source: SourceInfo,
613 pub(crate) selinux_state: SELinuxFinalState,
615 #[allow(dead_code)]
616 pub(crate) config_opts: InstallConfigOpts,
617 pub(crate) target_opts: InstallTargetOpts,
618 pub(crate) target_imgref: ostree_container::OstreeImageReference,
619 #[allow(dead_code)]
620 pub(crate) prepareroot_config: HashMap<String, String>,
621 pub(crate) install_config: Option<config::InstallConfiguration>,
622 pub(crate) root_ssh_authorized_keys: Option<String>,
624 #[allow(dead_code)]
625 pub(crate) host_is_container: bool,
626 pub(crate) container_root: Dir,
628 pub(crate) tempdir: TempDir,
629
630 #[allow(dead_code)]
632 pub(crate) composefs_required: bool,
633
634 pub(crate) composefs_options: InstallComposefsOpts,
636}
637
638#[derive(Debug)]
640pub(crate) struct PostFetchState {
641 pub(crate) detected_bootloader: crate::spec::Bootloader,
643}
644
645impl InstallTargetOpts {
646 pub(crate) fn imageref(&self) -> Result<Option<ostree_container::OstreeImageReference>> {
647 let Some(target_imgname) = self.target_imgref.as_deref() else {
648 return Ok(None);
649 };
650 let target_transport =
651 ostree_container::Transport::try_from(self.target_transport.as_str())?;
652 let target_imgref = ostree_container::OstreeImageReference {
653 sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
654 imgref: ostree_container::ImageReference {
655 transport: target_transport,
656 name: target_imgname.to_string(),
657 },
658 };
659 Ok(Some(target_imgref))
660 }
661}
662
663impl State {
664 #[context("Loading SELinux policy")]
665 pub(crate) fn load_policy(&self) -> Result<Option<ostree::SePolicy>> {
666 if !self.selinux_state.enabled() {
667 return Ok(None);
668 }
669 let r = lsm::new_sepolicy_at(&self.container_root)?
671 .ok_or_else(|| anyhow::anyhow!("SELinux enabled, but no policy found in root"))?;
672 tracing::debug!("Loaded SELinux policy: {}", r.csum().unwrap());
674 Ok(Some(r))
675 }
676
677 #[context("Finalizing state")]
678 #[allow(dead_code)]
679 pub(crate) fn consume(self) -> Result<()> {
680 self.tempdir.close()?;
681 if let SELinuxFinalState::Enabled(Some(guard)) = self.selinux_state {
683 guard.consume()?;
684 }
685 Ok(())
686 }
687
688 pub(crate) fn require_no_kargs_for_uki(&self) -> Result<()> {
690 if self
691 .config_opts
692 .karg
693 .as_ref()
694 .map(|v| !v.is_empty())
695 .unwrap_or_default()
696 {
697 anyhow::bail!("Cannot use externally specified kernel arguments with UKI");
698 }
699 Ok(())
700 }
701
702 fn stateroot(&self) -> &str {
703 self.config_opts
705 .stateroot
706 .as_deref()
707 .or_else(|| {
708 self.install_config
709 .as_ref()
710 .and_then(|c| c.stateroot.as_deref())
711 })
712 .unwrap_or(ostree_ext::container::deploy::STATEROOT_DEFAULT)
713 }
714}
715
716#[derive(Debug, Clone)]
727pub(crate) struct MountSpec {
728 pub(crate) source: String,
729 pub(crate) target: String,
730 pub(crate) fstype: String,
731 pub(crate) options: Option<String>,
732}
733
734impl MountSpec {
735 const AUTO: &'static str = "auto";
736
737 pub(crate) fn new(src: &str, target: &str) -> Self {
738 MountSpec {
739 source: src.to_string(),
740 target: target.to_string(),
741 fstype: Self::AUTO.to_string(),
742 options: None,
743 }
744 }
745
746 pub(crate) fn new_uuid_src(uuid: &str, target: &str) -> Self {
748 Self::new(&format!("UUID={uuid}"), target)
749 }
750
751 pub(crate) fn get_source_uuid(&self) -> Option<&str> {
752 if let Some((t, rest)) = self.source.split_once('=') {
753 if t.eq_ignore_ascii_case("uuid") {
754 return Some(rest);
755 }
756 }
757 None
758 }
759
760 pub(crate) fn to_fstab(&self) -> String {
761 let options = self.options.as_deref().unwrap_or("defaults");
762 format!(
763 "{} {} {} {} 0 0",
764 self.source, self.target, self.fstype, options
765 )
766 }
767
768 pub(crate) fn push_option(&mut self, opt: &str) {
770 let options = self.options.get_or_insert_with(Default::default);
771 if !options.is_empty() {
772 options.push(',');
773 }
774 options.push_str(opt);
775 }
776}
777
778impl FromStr for MountSpec {
779 type Err = anyhow::Error;
780
781 fn from_str(s: &str) -> Result<Self> {
782 let mut parts = s.split_ascii_whitespace().fuse();
783 let source = parts.next().unwrap_or_default();
784 if source.is_empty() {
785 tracing::debug!("Empty mount specification");
786 return Ok(Self {
787 source: String::new(),
788 target: String::new(),
789 fstype: Self::AUTO.into(),
790 options: None,
791 });
792 }
793 let target = parts
794 .next()
795 .ok_or_else(|| anyhow!("Missing target in mount specification {s}"))?;
796 let fstype = parts.next().unwrap_or(Self::AUTO);
797 let options = parts.next().map(ToOwned::to_owned);
798 Ok(Self {
799 source: source.to_string(),
800 fstype: fstype.to_string(),
801 target: target.to_string(),
802 options,
803 })
804 }
805}
806
807impl SourceInfo {
808 #[context("Gathering source info from container env")]
811 pub(crate) fn from_container(
812 root: &Dir,
813 container_info: &ContainerExecutionInfo,
814 ) -> Result<Self> {
815 if !container_info.engine.starts_with("podman") {
816 anyhow::bail!("Currently this command only supports being executed via podman");
817 }
818 if container_info.imageid.is_empty() {
819 anyhow::bail!("Invalid empty imageid");
820 }
821 let imageref = ostree_container::ImageReference {
822 transport: ostree_container::Transport::ContainerStorage,
823 name: container_info.image.clone(),
824 };
825 tracing::debug!("Finding digest for image ID {}", container_info.imageid);
826 let digest = crate::podman::imageid_to_digest(&container_info.imageid)?;
827
828 Self::new(imageref, Some(digest), root, true)
829 }
830
831 #[context("Creating source info from a given imageref")]
832 pub(crate) fn from_imageref(imageref: &str, root: &Dir) -> Result<Self> {
833 let imageref = ostree_container::ImageReference::try_from(imageref)?;
834 Self::new(imageref, None, root, false)
835 }
836
837 fn have_selinux_from_repo(root: &Dir) -> Result<bool> {
838 let cancellable = ostree::gio::Cancellable::NONE;
839
840 let commit = Command::new("ostree")
841 .args(["--repo=/ostree/repo", "rev-parse", "--single"])
842 .run_get_string()?;
843 let repo = ostree::Repo::open_at_dir(root.as_fd(), "ostree/repo")?;
844 let root = repo
845 .read_commit(commit.trim(), cancellable)
846 .context("Reading commit")?
847 .0;
848 let root = root.downcast_ref::<ostree::RepoFile>().unwrap();
849 let xattrs = root.xattrs(cancellable)?;
850 Ok(crate::lsm::xattrs_have_selinux(&xattrs))
851 }
852
853 fn new(
855 imageref: ostree_container::ImageReference,
856 digest: Option<String>,
857 root: &Dir,
858 in_host_mountns: bool,
859 ) -> Result<Self> {
860 let selinux = if Path::new("/ostree/repo").try_exists()? {
861 Self::have_selinux_from_repo(root)?
862 } else {
863 lsm::have_selinux_policy(root)?
864 };
865 Ok(Self {
866 imageref,
867 digest,
868 selinux,
869 in_host_mountns,
870 })
871 }
872}
873
874pub(crate) fn print_configuration(opts: InstallPrintConfigurationOpts) -> Result<()> {
875 let mut install_config = config::load_config()?.unwrap_or_default();
876 if !opts.all {
877 install_config.filter_to_external();
878 }
879 let stdout = std::io::stdout().lock();
880 anyhow::Ok(install_config.to_canon_json_writer(stdout)?)
881}
882
883#[context("Creating ostree deployment")]
884async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result<(Storage, bool)> {
885 let sepolicy = state.load_policy()?;
886 let sepolicy = sepolicy.as_ref();
887 let rootfs_dir = &root_setup.physical_root;
889 let cancellable = gio::Cancellable::NONE;
890
891 let stateroot = state.stateroot();
892
893 let has_ostree = rootfs_dir.try_exists("ostree/repo")?;
894 if !has_ostree {
895 Task::new("Initializing ostree layout", "ostree")
896 .args(["admin", "init-fs", "--modern", "."])
897 .cwd(rootfs_dir)?
898 .run()?;
899 } else {
900 println!("Reusing extant ostree layout");
901
902 let path = ".".into();
903 let _ = crate::utils::open_dir_remount_rw(rootfs_dir, path)
904 .context("remounting target as read-write")?;
905 crate::utils::remove_immutability(rootfs_dir, path)?;
906 }
907
908 crate::lsm::ensure_dir_labeled(rootfs_dir, "", Some("/".into()), 0o755.into(), sepolicy)?;
911
912 if has_ostree && root_setup.boot.is_some() {
915 if let Some(boot) = &root_setup.boot {
916 let source_boot = &boot.source;
917 let target_boot = root_setup.physical_root_path.join(BOOT);
918 tracing::debug!("Mount {source_boot} to {target_boot} on ostree");
919 bootc_mount::mount(source_boot, &target_boot)?;
920 }
921 }
922
923 if rootfs_dir.try_exists("boot")? {
925 crate::lsm::ensure_dir_labeled(rootfs_dir, "boot", None, 0o755.into(), sepolicy)?;
926 }
927
928 let ostree_opts = state
930 .install_config
931 .as_ref()
932 .and_then(|c| c.ostree.as_ref())
933 .into_iter()
934 .flat_map(|o| o.to_config_tuples());
935
936 let repo_config: Vec<_> = DEFAULT_REPO_CONFIG
937 .iter()
938 .copied()
939 .chain(ostree_opts)
940 .collect();
941
942 for (k, v) in repo_config.iter() {
943 Command::new("ostree")
944 .args(["config", "--repo", "ostree/repo", "set", k, v])
945 .cwd_dir(rootfs_dir.try_clone()?)
946 .run_capture_stderr()?;
947 }
948
949 let sysroot = {
950 let path = format!(
951 "/proc/{}/fd/{}",
952 process::id(),
953 rootfs_dir.as_fd().as_raw_fd()
954 );
955 ostree::Sysroot::new(Some(&gio::File::for_path(path)))
956 };
957 sysroot.load(cancellable)?;
958 let repo = &sysroot.repo();
959
960 let repo_verity_state = ostree_ext::fsverity::is_verity_enabled(&repo)?;
961 let prepare_root_composefs = state
962 .prepareroot_config
963 .get("composefs.enabled")
964 .map(|v| ComposefsState::from_str(&v))
965 .transpose()?
966 .unwrap_or(ComposefsState::default());
967 if prepare_root_composefs.requires_fsverity() || repo_verity_state.desired == Tristate::Enabled
968 {
969 ostree_ext::fsverity::ensure_verity(repo).await?;
970 }
971
972 if let Some(booted) = sysroot.booted_deployment() {
973 if stateroot == booted.stateroot() {
974 anyhow::bail!("Cannot redeploy over booted stateroot {stateroot}");
975 }
976 }
977
978 let sysroot_dir = crate::utils::sysroot_dir(&sysroot)?;
979
980 let stateroot_path = format!("ostree/deploy/{stateroot}");
985 if !sysroot_dir.try_exists(stateroot_path)? {
986 sysroot
987 .init_osname(stateroot, cancellable)
988 .context("initializing stateroot")?;
989 }
990
991 state.tempdir.create_dir("temp-run")?;
992 let temp_run = state.tempdir.open_dir("temp-run")?;
993
994 if let Some(policy) = sepolicy {
997 let ostree_dir = rootfs_dir.open_dir("ostree")?;
998 crate::lsm::ensure_dir_labeled(
999 &ostree_dir,
1000 ".",
1001 Some("/usr".into()),
1002 0o755.into(),
1003 Some(policy),
1004 )?;
1005 }
1006
1007 sysroot.load(cancellable)?;
1008 let sysroot = SysrootLock::new_from_sysroot(&sysroot).await?;
1009 let storage = Storage::new_ostree(sysroot, &temp_run)?;
1010
1011 Ok((storage, has_ostree))
1012}
1013
1014fn check_disk_space(
1015 repo_fd: impl AsFd,
1016 image_meta: &PreparedImportMeta,
1017 imgref: &ImageReference,
1018) -> Result<()> {
1019 let stat = rustix::fs::fstatvfs(repo_fd)?;
1020 let bytes_avail: u64 = stat.f_bsize * stat.f_bavail;
1021 tracing::trace!("bytes_avail: {bytes_avail}");
1022
1023 if image_meta.bytes_to_fetch > bytes_avail {
1024 anyhow::bail!(
1025 "Insufficient free space for {image} (available: {bytes_avail} required: {bytes_to_fetch})",
1026 bytes_avail = ostree_ext::glib::format_size(bytes_avail),
1027 bytes_to_fetch = ostree_ext::glib::format_size(image_meta.bytes_to_fetch),
1028 image = imgref.image,
1029 );
1030 }
1031
1032 Ok(())
1033}
1034
1035#[context("Creating ostree deployment")]
1036async fn install_container(
1037 state: &State,
1038 root_setup: &RootSetup,
1039 sysroot: &ostree::Sysroot,
1040 storage: &Storage,
1041 has_ostree: bool,
1042) -> Result<(ostree::Deployment, InstallAleph)> {
1043 let sepolicy = state.load_policy()?;
1044 let sepolicy = sepolicy.as_ref();
1045 let stateroot = state.stateroot();
1046
1047 let (src_imageref, proxy_cfg) = if !state.source.in_host_mountns {
1049 (state.source.imageref.clone(), None)
1050 } else {
1051 let src_imageref = {
1052 let digest = state
1054 .source
1055 .digest
1056 .as_ref()
1057 .ok_or_else(|| anyhow::anyhow!("Missing container image digest"))?;
1058 let spec = crate::utils::digested_pullspec(&state.source.imageref.name, digest);
1059 ostree_container::ImageReference {
1060 transport: ostree_container::Transport::ContainerStorage,
1061 name: spec,
1062 }
1063 };
1064
1065 let proxy_cfg = crate::deploy::new_proxy_config();
1066 (src_imageref, Some(proxy_cfg))
1067 };
1068 let src_imageref = ostree_container::OstreeImageReference {
1069 sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
1072 imgref: src_imageref,
1073 };
1074
1075 let spec_imgref = ImageReference::from(src_imageref.clone());
1078 let repo = &sysroot.repo();
1079 repo.set_disable_fsync(true);
1080
1081 let use_unified = state.target_opts.unified_storage_exp;
1085
1086 let prepared = if use_unified {
1087 tracing::info!("Using unified storage path for installation");
1088 crate::deploy::prepare_for_pull_unified(
1089 repo,
1090 &spec_imgref,
1091 Some(&state.target_imgref),
1092 storage,
1093 )
1094 .await?
1095 } else {
1096 prepare_for_pull(repo, &spec_imgref, Some(&state.target_imgref)).await?
1097 };
1098
1099 let pulled_image = match prepared {
1100 PreparedPullResult::AlreadyPresent(existing) => existing,
1101 PreparedPullResult::Ready(image_meta) => {
1102 check_disk_space(root_setup.physical_root.as_fd(), &image_meta, &spec_imgref)?;
1103 pull_from_prepared(&spec_imgref, false, ProgressWriter::default(), *image_meta).await?
1104 }
1105 };
1106
1107 repo.set_disable_fsync(false);
1108
1109 let merged_ostree_root = sysroot
1112 .repo()
1113 .read_commit(pulled_image.ostree_commit.as_str(), gio::Cancellable::NONE)?
1114 .0;
1115 let kargsd = crate::bootc_kargs::get_kargs_from_ostree_root(
1116 &sysroot.repo(),
1117 merged_ostree_root.downcast_ref().unwrap(),
1118 std::env::consts::ARCH,
1119 )?;
1120
1121 if ostree_ext::bootabletree::commit_has_aboot_img(&merged_ostree_root, None)? {
1124 tracing::debug!("Setting bootloader to aboot");
1125 Command::new("ostree")
1126 .args([
1127 "config",
1128 "--repo",
1129 "ostree/repo",
1130 "set",
1131 "sysroot.bootloader",
1132 "aboot",
1133 ])
1134 .cwd_dir(root_setup.physical_root.try_clone()?)
1135 .run_capture_stderr()
1136 .context("Setting bootloader config to aboot")?;
1137 sysroot.repo().reload_config(None::<&gio::Cancellable>)?;
1138 }
1139
1140 let install_config_kargs = state.install_config.as_ref().and_then(|c| c.kargs.as_ref());
1142
1143 let mut kargs = Cmdline::new();
1149
1150 kargs.extend(&root_setup.kargs);
1151
1152 if let Some(install_config_kargs) = install_config_kargs {
1153 for karg in install_config_kargs {
1154 kargs.extend(&Cmdline::from(karg.as_str()));
1155 }
1156 }
1157
1158 kargs.extend(&kargsd);
1159
1160 if let Some(cli_kargs) = state.config_opts.karg.as_ref() {
1161 for karg in cli_kargs {
1162 kargs.extend(karg);
1163 }
1164 }
1165
1166 let kargs_strs: Vec<&str> = kargs.iter_str().collect();
1168
1169 let mut options = ostree_container::deploy::DeployOpts::default();
1170 options.kargs = Some(kargs_strs.as_slice());
1171 options.target_imgref = Some(&state.target_imgref);
1172 options.proxy_cfg = proxy_cfg;
1173 options.skip_completion = true; options.no_clean = has_ostree;
1175 let imgstate = crate::utils::async_task_with_spinner(
1176 "Deploying container image",
1177 ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)),
1178 )
1179 .await?;
1180
1181 let deployment = sysroot
1182 .deployments()
1183 .into_iter()
1184 .next()
1185 .ok_or_else(|| anyhow::anyhow!("Failed to find deployment"))?;
1186 let path = sysroot.deployment_dirpath(&deployment);
1188 let root = root_setup
1189 .physical_root
1190 .open_dir(path.as_str())
1191 .context("Opening deployment dir")?;
1192
1193 if let Some(policy) = sepolicy {
1197 let deployment_root_meta = root.dir_metadata()?;
1198 let deployment_root_devino = (deployment_root_meta.dev(), deployment_root_meta.ino());
1199 for d in ["ostree", "boot"] {
1200 let mut pathbuf = Utf8PathBuf::from(d);
1201 crate::lsm::ensure_dir_labeled_recurse(
1202 &root_setup.physical_root,
1203 &mut pathbuf,
1204 policy,
1205 Some(deployment_root_devino),
1206 )
1207 .with_context(|| format!("Recursive SELinux relabeling of {d}"))?;
1208 }
1209
1210 if let Some(cfs_super) = root.open_optional(OSTREE_COMPOSEFS_SUPER)? {
1211 let label = crate::lsm::require_label(policy, "/usr".into(), 0o644)?;
1212 crate::lsm::set_security_selinux(cfs_super.as_fd(), label.as_bytes())?;
1213 } else {
1214 tracing::warn!("Missing {OSTREE_COMPOSEFS_SUPER}; composefs is not enabled?");
1215 }
1216 }
1217
1218 if let Some(boot) = root_setup.boot.as_ref() {
1222 if !boot.source.is_empty() {
1223 crate::lsm::atomic_replace_labeled(&root, "etc/fstab", 0o644.into(), sepolicy, |w| {
1224 writeln!(w, "{}", boot.to_fstab()).map_err(Into::into)
1225 })?;
1226 }
1227 }
1228
1229 if let Some(contents) = state.root_ssh_authorized_keys.as_deref() {
1230 osconfig::inject_root_ssh_authorized_keys(&root, sepolicy, contents)?;
1231 }
1232
1233 let aleph = InstallAleph::new(&src_imageref, &imgstate, &state.selinux_state)?;
1234 Ok((deployment, aleph))
1235}
1236
1237pub(crate) fn run_in_host_mountns(cmd: &str) -> Result<Command> {
1239 let mut c = Command::new(bootc_utils::reexec::executable_path()?);
1240 c.lifecycle_bind()
1241 .args(["exec-in-host-mount-namespace", cmd]);
1242 Ok(c)
1243}
1244
1245#[context("Re-exec in host mountns")]
1246pub(crate) fn exec_in_host_mountns(args: &[std::ffi::OsString]) -> Result<()> {
1247 let (cmd, args) = args
1248 .split_first()
1249 .ok_or_else(|| anyhow::anyhow!("Missing command"))?;
1250 tracing::trace!("{cmd:?} {args:?}");
1251 let pid1mountns = std::fs::File::open("/proc/1/ns/mnt").context("open pid1 mountns")?;
1252 rustix::thread::move_into_link_name_space(
1253 pid1mountns.as_fd(),
1254 Some(rustix::thread::LinkNameSpaceType::Mount),
1255 )
1256 .context("setns")?;
1257 rustix::process::chdir("/").context("chdir")?;
1258 if !Utf8Path::new("/usr").try_exists().context("/usr")?
1261 && Utf8Path::new("/root/usr")
1262 .try_exists()
1263 .context("/root/usr")?
1264 {
1265 tracing::debug!("Using supermin workaround");
1266 rustix::process::chroot("/root").context("chroot")?;
1267 }
1268 Err(Command::new(cmd).args(args).arg0(bootc_utils::NAME).exec()).context("exec")?
1269}
1270
1271pub(crate) struct RootSetup {
1272 #[cfg(feature = "install-to-disk")]
1273 luks_device: Option<String>,
1274 pub(crate) device_info: bootc_blockdev::PartitionTable,
1275 pub(crate) physical_root_path: Utf8PathBuf,
1278 pub(crate) physical_root: Dir,
1280 pub(crate) target_root_path: Option<Utf8PathBuf>,
1282 pub(crate) rootfs_uuid: Option<String>,
1283 skip_finalize: bool,
1285 boot: Option<MountSpec>,
1286 pub(crate) kargs: CmdlineOwned,
1287}
1288
1289fn require_boot_uuid(spec: &MountSpec) -> Result<&str> {
1290 spec.get_source_uuid()
1291 .ok_or_else(|| anyhow!("/boot is not specified via UUID= (this is currently required)"))
1292}
1293
1294impl RootSetup {
1295 pub(crate) fn get_boot_uuid(&self) -> Result<Option<&str>> {
1298 self.boot.as_ref().map(require_boot_uuid).transpose()
1299 }
1300
1301 #[cfg(feature = "install-to-disk")]
1303 fn into_storage(self) -> (Utf8PathBuf, Option<String>) {
1304 (self.physical_root_path, self.luks_device)
1305 }
1306}
1307
1308#[derive(Debug)]
1309#[allow(dead_code)]
1310pub(crate) enum SELinuxFinalState {
1311 ForceTargetDisabled,
1313 Enabled(Option<crate::lsm::SetEnforceGuard>),
1315 HostDisabled,
1317 Disabled,
1319}
1320
1321impl SELinuxFinalState {
1322 pub(crate) fn enabled(&self) -> bool {
1324 match self {
1325 SELinuxFinalState::ForceTargetDisabled | SELinuxFinalState::Disabled => false,
1326 SELinuxFinalState::Enabled(_) | SELinuxFinalState::HostDisabled => true,
1327 }
1328 }
1329
1330 pub(crate) fn to_aleph(&self) -> &'static str {
1333 match self {
1334 SELinuxFinalState::ForceTargetDisabled => "force-target-disabled",
1335 SELinuxFinalState::Enabled(_) => "enabled",
1336 SELinuxFinalState::HostDisabled => "host-disabled",
1337 SELinuxFinalState::Disabled => "disabled",
1338 }
1339 }
1340}
1341
1342pub(crate) fn reexecute_self_for_selinux_if_needed(
1347 srcdata: &SourceInfo,
1348 override_disable_selinux: bool,
1349) -> Result<SELinuxFinalState> {
1350 if srcdata.selinux {
1352 let host_selinux = crate::lsm::selinux_enabled()?;
1353 tracing::debug!("Target has SELinux, host={host_selinux}");
1354 let r = if override_disable_selinux {
1355 println!("notice: Target has SELinux enabled, overriding to disable");
1356 SELinuxFinalState::ForceTargetDisabled
1357 } else if host_selinux {
1358 setup_sys_mount("selinuxfs", SELINUXFS)?;
1364 let g = crate::lsm::selinux_ensure_install_or_setenforce()?;
1366 SELinuxFinalState::Enabled(g)
1367 } else {
1368 SELinuxFinalState::HostDisabled
1369 };
1370 Ok(r)
1371 } else {
1372 Ok(SELinuxFinalState::Disabled)
1373 }
1374}
1375
1376pub(crate) fn finalize_filesystem(
1379 fsname: &str,
1380 root: &Dir,
1381 path: impl AsRef<Utf8Path>,
1382) -> Result<()> {
1383 let path = path.as_ref();
1384 Task::new(format!("Trimming {fsname}"), "fstrim")
1386 .args(["--quiet-unsupported", "-v", path.as_str()])
1387 .cwd(root)?
1388 .run()?;
1389 Task::new(format!("Finalizing filesystem {fsname}"), "mount")
1392 .cwd(root)?
1393 .args(["-o", "remount,ro", path.as_str()])
1394 .run()?;
1395 for a in ["-f", "-u"] {
1397 Command::new("fsfreeze")
1398 .cwd_dir(root.try_clone()?)
1399 .args([a, path.as_str()])
1400 .run_capture_stderr()?;
1401 }
1402 Ok(())
1403}
1404
1405fn require_host_pidns() -> Result<()> {
1407 if rustix::process::getpid().is_init() {
1408 anyhow::bail!("This command must be run with the podman --pid=host flag")
1409 }
1410 tracing::trace!("OK: we're not pid 1");
1411 Ok(())
1412}
1413
1414fn require_host_userns() -> Result<()> {
1417 let proc1 = "/proc/1";
1418 let pid1_uid = Path::new(proc1)
1419 .metadata()
1420 .with_context(|| format!("Querying {proc1}"))?
1421 .uid();
1422 ensure!(
1425 pid1_uid == 0,
1426 "{proc1} is owned by {pid1_uid}, not zero; this command must be run in the root user namespace (e.g. not rootless podman)"
1427 );
1428 tracing::trace!("OK: we're in a matching user namespace with pid1");
1429 Ok(())
1430}
1431
1432pub(crate) fn setup_tmp_mount() -> Result<()> {
1437 let st = rustix::fs::statfs("/tmp")?;
1438 if st.f_type == libc::TMPFS_MAGIC {
1439 tracing::trace!("Already have tmpfs /tmp")
1440 } else {
1441 Command::new("mount")
1444 .args(["tmpfs", "-t", "tmpfs", "/tmp"])
1445 .run_capture_stderr()?;
1446 }
1447 Ok(())
1448}
1449
1450#[context("Ensuring sys mount {fspath} {fstype}")]
1453pub(crate) fn setup_sys_mount(fstype: &str, fspath: &str) -> Result<()> {
1454 tracing::debug!("Setting up sys mounts");
1455 let rootfs = format!("/proc/1/root/{fspath}");
1456 if !Path::new(rootfs.as_str()).try_exists()? {
1458 return Ok(());
1459 }
1460
1461 if std::fs::read_dir(rootfs)?.next().is_none() {
1463 return Ok(());
1464 }
1465
1466 if Path::new(fspath).try_exists()? && std::fs::read_dir(fspath)?.next().is_some() {
1470 return Ok(());
1471 }
1472
1473 Command::new("mount")
1475 .args(["-t", fstype, fstype, fspath])
1476 .run_capture_stderr()?;
1477
1478 Ok(())
1479}
1480
1481#[context("Verifying fetch")]
1483async fn verify_target_fetch(
1484 tmpdir: &Dir,
1485 imgref: &ostree_container::OstreeImageReference,
1486) -> Result<()> {
1487 let tmpdir = &TempDir::new_in(&tmpdir)?;
1488 let tmprepo = &ostree::Repo::create_at_dir(tmpdir.as_fd(), ".", ostree::RepoMode::Bare, None)
1489 .context("Init tmp repo")?;
1490
1491 tracing::trace!("Verifying fetch for {imgref}");
1492 let mut imp =
1493 ostree_container::store::ImageImporter::new(tmprepo, imgref, Default::default()).await?;
1494 use ostree_container::store::PrepareResult;
1495 let prep = match imp.prepare().await? {
1496 PrepareResult::AlreadyPresent(_) => unreachable!(),
1498 PrepareResult::Ready(r) => r,
1499 };
1500 tracing::debug!("Fetched manifest with digest {}", prep.manifest_digest);
1501 Ok(())
1502}
1503
1504async fn prepare_install(
1506 mut config_opts: InstallConfigOpts,
1507 source_opts: InstallSourceOpts,
1508 target_opts: InstallTargetOpts,
1509 mut composefs_options: InstallComposefsOpts,
1510) -> Result<Arc<State>> {
1511 tracing::trace!("Preparing install");
1512 let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
1513 .context("Opening /")?;
1514
1515 let host_is_container = crate::containerenv::is_container(&rootfs);
1516 let external_source = source_opts.source_imgref.is_some();
1517 let (source, target_rootfs) = match source_opts.source_imgref {
1518 None => {
1519 ensure!(
1520 host_is_container,
1521 "Either --source-imgref must be defined or this command must be executed inside a podman container."
1522 );
1523
1524 crate::cli::require_root(true)?;
1525
1526 require_host_pidns()?;
1527 require_host_userns()?;
1530 let container_info = crate::containerenv::get_container_execution_info(&rootfs)?;
1531 match container_info.rootless.as_deref() {
1533 Some("1") => anyhow::bail!(
1534 "Cannot install from rootless podman; this command must be run as root"
1535 ),
1536 Some(o) => tracing::debug!("rootless={o}"),
1537 None => tracing::debug!(
1539 "notice: Did not find rootless= entry in {}",
1540 crate::containerenv::PATH,
1541 ),
1542 };
1543 tracing::trace!("Read container engine info {:?}", container_info);
1544
1545 let source = SourceInfo::from_container(&rootfs, &container_info)?;
1546 (source, Some(rootfs.try_clone()?))
1547 }
1548 Some(source) => {
1549 crate::cli::require_root(false)?;
1550 let source = SourceInfo::from_imageref(&source, &rootfs)?;
1551 (source, None)
1552 }
1553 };
1554
1555 if target_opts.target_no_signature_verification {
1558 tracing::debug!(
1560 "Use of --target-no-signature-verification flag which is enabled by default"
1561 );
1562 }
1563 let target_sigverify = sigpolicy_from_opt(target_opts.enforce_container_sigpolicy);
1564 let target_imgname = target_opts
1565 .target_imgref
1566 .as_deref()
1567 .unwrap_or(source.imageref.name.as_str());
1568 let target_transport =
1569 ostree_container::Transport::try_from(target_opts.target_transport.as_str())?;
1570 let target_imgref = ostree_container::OstreeImageReference {
1571 sigverify: target_sigverify,
1572 imgref: ostree_container::ImageReference {
1573 transport: target_transport,
1574 name: target_imgname.to_string(),
1575 },
1576 };
1577 tracing::debug!("Target image reference: {target_imgref}");
1578
1579 let composefs_required = if let Some(root) = target_rootfs.as_ref() {
1580 crate::kernel::find_kernel(root)?
1581 .map(|k| k.kernel.unified)
1582 .unwrap_or(false)
1583 } else {
1584 false
1585 };
1586
1587 tracing::debug!("Composefs required: {composefs_required}");
1588
1589 if composefs_required {
1590 composefs_options.composefs_backend = true;
1591 }
1592
1593 bootc_mount::ensure_mirrored_host_mount("/dev")?;
1595 bootc_mount::ensure_mirrored_host_mount("/var/lib/containers")?;
1598 bootc_mount::ensure_mirrored_host_mount("/var/tmp")?;
1601 bootc_mount::ensure_mirrored_host_mount("/run/udev")?;
1604 setup_tmp_mount()?;
1606 let tempdir = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
1609 osbuild::adjust_for_bootc_image_builder(&rootfs, &tempdir)?;
1611
1612 if target_opts.run_fetch_check {
1613 verify_target_fetch(&tempdir, &target_imgref).await?;
1614 }
1615
1616 if !external_source && std::env::var_os("BOOTC_SKIP_UNSHARE").is_none() {
1619 super::cli::ensure_self_unshared_mount_namespace()?;
1620 }
1621
1622 setup_sys_mount("efivarfs", EFIVARFS)?;
1623
1624 let selinux_state = reexecute_self_for_selinux_if_needed(&source, config_opts.disable_selinux)?;
1626 tracing::debug!("SELinux state: {selinux_state:?}");
1627
1628 println!("Installing image: {:#}", &target_imgref);
1629 if let Some(digest) = source.digest.as_deref() {
1630 println!("Digest: {digest}");
1631 }
1632
1633 let install_config = config::load_config()?;
1634 if let Some(ref config) = install_config {
1635 tracing::debug!("Loaded install configuration");
1636 if !config_opts.bootupd_skip_boot_uuid {
1639 config_opts.bootupd_skip_boot_uuid = config
1640 .bootupd
1641 .as_ref()
1642 .and_then(|b| b.skip_boot_uuid)
1643 .unwrap_or(false);
1644 }
1645 } else {
1646 tracing::debug!("No install configuration found");
1647 }
1648
1649 let prepareroot_config = {
1651 let kf = ostree_prepareroot::require_config_from_root(&rootfs)?;
1652 let mut r = HashMap::new();
1653 for grp in kf.groups() {
1654 for key in kf.keys(&grp)? {
1655 let key = key.as_str();
1656 let value = kf.value(&grp, key)?;
1657 r.insert(format!("{grp}.{key}"), value.to_string());
1658 }
1659 }
1660 r
1661 };
1662
1663 let root_ssh_authorized_keys = config_opts
1666 .root_ssh_authorized_keys
1667 .as_ref()
1668 .map(|p| std::fs::read_to_string(p).with_context(|| format!("Reading {p}")))
1669 .transpose()?;
1670
1671 let state = Arc::new(State {
1675 selinux_state,
1676 source,
1677 config_opts,
1678 target_opts,
1679 target_imgref,
1680 install_config,
1681 prepareroot_config,
1682 root_ssh_authorized_keys,
1683 container_root: rootfs,
1684 tempdir,
1685 host_is_container,
1686 composefs_required,
1687 composefs_options,
1688 });
1689
1690 Ok(state)
1691}
1692
1693impl PostFetchState {
1694 pub(crate) fn new(state: &State, d: &Dir) -> Result<Self> {
1695 let detected_bootloader = {
1698 if let Some(bootloader) = state.composefs_options.bootloader.clone() {
1699 bootloader
1700 } else {
1701 if crate::bootloader::supports_bootupd(d)? {
1702 crate::spec::Bootloader::Grub
1703 } else {
1704 crate::spec::Bootloader::Systemd
1705 }
1706 }
1707 };
1708 println!("Bootloader: {detected_bootloader}");
1709 let r = Self {
1710 detected_bootloader,
1711 };
1712 Ok(r)
1713 }
1714}
1715
1716async fn install_with_sysroot(
1721 state: &State,
1722 rootfs: &RootSetup,
1723 storage: &Storage,
1724 boot_uuid: &str,
1725 bound_images: BoundImages,
1726 has_ostree: bool,
1727) -> Result<()> {
1728 let ostree = storage.get_ostree()?;
1729 let c_storage = storage.get_ensure_imgstore()?;
1730
1731 let (deployment, aleph) = install_container(state, rootfs, ostree, storage, has_ostree).await?;
1734 aleph.write_to(&rootfs.physical_root)?;
1736
1737 let deployment_path = ostree.deployment_dirpath(&deployment);
1738
1739 let deployment_dir = rootfs
1740 .physical_root
1741 .open_dir(&deployment_path)
1742 .context("Opening deployment dir")?;
1743 let postfetch = PostFetchState::new(state, &deployment_dir)?;
1744
1745 if cfg!(target_arch = "s390x") {
1746 crate::bootloader::install_via_zipl(&rootfs.device_info, boot_uuid)?;
1748 } else {
1749 match postfetch.detected_bootloader {
1750 Bootloader::Grub => {
1751 crate::bootloader::install_via_bootupd(
1752 &rootfs.device_info,
1753 &rootfs
1754 .target_root_path
1755 .clone()
1756 .unwrap_or(rootfs.physical_root_path.clone()),
1757 &state.config_opts,
1758 Some(&deployment_path.as_str()),
1759 )?;
1760 }
1761 Bootloader::Systemd => {
1762 anyhow::bail!("bootupd is required for ostree-based installs");
1763 }
1764 }
1765 }
1766 tracing::debug!("Installed bootloader");
1767
1768 tracing::debug!("Performing post-deployment operations");
1769
1770 match bound_images {
1771 BoundImages::Skip => {}
1772 BoundImages::Resolved(resolved_bound_images) => {
1773 for image in resolved_bound_images {
1775 let image = image.image.as_str();
1776 c_storage.pull_from_host_storage(image).await?;
1777 }
1778 }
1779 BoundImages::Unresolved(bound_images) => {
1780 crate::boundimage::pull_images_impl(c_storage, bound_images)
1781 .await
1782 .context("pulling bound images")?;
1783 }
1784 }
1785
1786 Ok(())
1787}
1788
1789enum BoundImages {
1790 Skip,
1791 Resolved(Vec<ResolvedBoundImage>),
1792 Unresolved(Vec<BoundImage>),
1793}
1794
1795impl BoundImages {
1796 async fn from_state(state: &State) -> Result<Self> {
1797 let bound_images = match state.config_opts.bound_images {
1798 BoundImagesOpt::Skip => BoundImages::Skip,
1799 others => {
1800 let queried_images = crate::boundimage::query_bound_images(&state.container_root)?;
1801 match others {
1802 BoundImagesOpt::Stored => {
1803 let mut r = Vec::with_capacity(queried_images.len());
1805 for image in queried_images {
1806 let resolved = ResolvedBoundImage::from_image(&image).await?;
1807 tracing::debug!("Resolved {}: {}", resolved.image, resolved.digest);
1808 r.push(resolved)
1809 }
1810 BoundImages::Resolved(r)
1811 }
1812 BoundImagesOpt::Pull => {
1813 BoundImages::Unresolved(queried_images)
1815 }
1816 BoundImagesOpt::Skip => anyhow::bail!("unreachable error"),
1817 }
1818 }
1819 };
1820
1821 Ok(bound_images)
1822 }
1823}
1824
1825async fn ostree_install(state: &State, rootfs: &RootSetup, cleanup: Cleanup) -> Result<()> {
1826 let boot_uuid = rootfs
1828 .get_boot_uuid()?
1829 .or(rootfs.rootfs_uuid.as_deref())
1830 .ok_or_else(|| anyhow!("No uuid for boot/root"))?;
1831 tracing::debug!("boot uuid={boot_uuid}");
1832
1833 let bound_images = BoundImages::from_state(state).await?;
1834
1835 {
1838 let (sysroot, has_ostree) = initialize_ostree_root(state, rootfs).await?;
1839
1840 install_with_sysroot(
1841 state,
1842 rootfs,
1843 &sysroot,
1844 &boot_uuid,
1845 bound_images,
1846 has_ostree,
1847 )
1848 .await?;
1849 let ostree = sysroot.get_ostree()?;
1850
1851 if matches!(cleanup, Cleanup::TriggerOnNextBoot) {
1852 let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
1853 tracing::debug!("Writing {DESTRUCTIVE_CLEANUP}");
1854 sysroot_dir.atomic_write(DESTRUCTIVE_CLEANUP, b"")?;
1855 }
1856
1857 };
1860
1861 install_finalize(&rootfs.physical_root_path).await?;
1863
1864 Ok(())
1865}
1866
1867async fn install_to_filesystem_impl(
1868 state: &State,
1869 rootfs: &mut RootSetup,
1870 cleanup: Cleanup,
1871) -> Result<()> {
1872 if matches!(state.selinux_state, SELinuxFinalState::ForceTargetDisabled) {
1873 rootfs.kargs.extend(&Cmdline::from("selinux=0"));
1874 }
1875 let rootfs = &*rootfs;
1877
1878 match &rootfs.device_info.label {
1879 bootc_blockdev::PartitionType::Dos => crate::utils::medium_visibility_warning(
1880 "Installing to `dos` format partitions is not recommended",
1881 ),
1882 bootc_blockdev::PartitionType::Gpt => {
1883 }
1885 bootc_blockdev::PartitionType::Unknown(o) => {
1886 crate::utils::medium_visibility_warning(&format!("Unknown partition label {o}"))
1887 }
1888 }
1889
1890 if state.composefs_options.composefs_backend {
1891 let (id, verity) = initialize_composefs_repository(state, rootfs).await?;
1894 tracing::info!("id: {id}, verity: {}", verity.to_hex());
1895
1896 setup_composefs_boot(rootfs, state, &id).await?;
1897 } else {
1898 ostree_install(state, rootfs, cleanup).await?;
1899 }
1900
1901 if !rootfs.skip_finalize {
1903 let bootfs = rootfs.boot.as_ref().map(|_| ("boot", "boot"));
1904 for (fsname, fs) in std::iter::once(("root", ".")).chain(bootfs) {
1905 finalize_filesystem(fsname, &rootfs.physical_root, fs)?;
1906 }
1907 }
1908
1909 Ok(())
1910}
1911
1912fn installation_complete() {
1913 println!("Installation complete!");
1914}
1915
1916#[context("Installing to disk")]
1918#[cfg(feature = "install-to-disk")]
1919pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
1920 const INSTALL_DISK_JOURNAL_ID: &str = "8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2";
1922 let source_image = opts
1923 .source_opts
1924 .source_imgref
1925 .as_ref()
1926 .map(|s| s.as_str())
1927 .unwrap_or("none");
1928 let target_device = opts.block_opts.device.as_str();
1929
1930 tracing::info!(
1931 message_id = INSTALL_DISK_JOURNAL_ID,
1932 bootc.source_image = source_image,
1933 bootc.target_device = target_device,
1934 bootc.via_loopback = if opts.via_loopback { "true" } else { "false" },
1935 "Starting disk installation from {} to {}",
1936 source_image,
1937 target_device
1938 );
1939
1940 let mut block_opts = opts.block_opts;
1941 let target_blockdev_meta = block_opts
1942 .device
1943 .metadata()
1944 .with_context(|| format!("Querying {}", &block_opts.device))?;
1945 if opts.via_loopback {
1946 if !opts.config_opts.generic_image {
1947 crate::utils::medium_visibility_warning(
1948 "Automatically enabling --generic-image when installing via loopback",
1949 );
1950 opts.config_opts.generic_image = true;
1951 }
1952 if !target_blockdev_meta.file_type().is_file() {
1953 anyhow::bail!(
1954 "Not a regular file (to be used via loopback): {}",
1955 block_opts.device
1956 );
1957 }
1958 } else if !target_blockdev_meta.file_type().is_block_device() {
1959 anyhow::bail!("Not a block device: {}", block_opts.device);
1960 }
1961
1962 let state = prepare_install(
1963 opts.config_opts,
1964 opts.source_opts,
1965 opts.target_opts,
1966 opts.composefs_opts,
1967 )
1968 .await?;
1969
1970 let (mut rootfs, loopback) = {
1972 let loopback_dev = if opts.via_loopback {
1973 let loopback_dev =
1974 bootc_blockdev::LoopbackDevice::new(block_opts.device.as_std_path())?;
1975 block_opts.device = loopback_dev.path().into();
1976 Some(loopback_dev)
1977 } else {
1978 None
1979 };
1980
1981 let state = state.clone();
1982 let rootfs = tokio::task::spawn_blocking(move || {
1983 baseline::install_create_rootfs(&state, block_opts)
1984 })
1985 .await??;
1986 (rootfs, loopback_dev)
1987 };
1988
1989 install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip).await?;
1990
1991 let (root_path, luksdev) = rootfs.into_storage();
1993 Task::new_and_run(
1994 "Unmounting filesystems",
1995 "umount",
1996 ["-R", root_path.as_str()],
1997 )?;
1998 if let Some(luksdev) = luksdev.as_deref() {
1999 Task::new_and_run("Closing root LUKS device", "cryptsetup", ["close", luksdev])?;
2000 }
2001
2002 if let Some(loopback_dev) = loopback {
2003 loopback_dev.close()?;
2004 }
2005
2006 if let Some(state) = Arc::into_inner(state) {
2008 state.consume()?;
2009 } else {
2010 tracing::warn!("Failed to consume state Arc");
2012 }
2013
2014 installation_complete();
2015
2016 Ok(())
2017}
2018
2019#[context("Requiring directory contains only mount points")]
2030fn require_dir_contains_only_mounts(parent_fd: &Dir, dir_name: &str) -> Result<()> {
2031 tracing::trace!("Checking directory {dir_name} for non-mount entries");
2032 let Some(dir_fd) = parent_fd.open_dir_noxdev(dir_name)? else {
2033 tracing::trace!("{dir_name} is a mount point");
2035 return Ok(());
2036 };
2037
2038 if dir_fd.entries()?.next().is_none() {
2039 anyhow::bail!("Found empty directory: {dir_name}");
2040 }
2041
2042 for entry in dir_fd.entries()? {
2043 tracing::trace!("Checking entry in {dir_name}");
2044 let entry = DirEntryUtf8::from_cap_std(entry?);
2045 let entry_name = entry.file_name()?;
2046
2047 if entry_name == LOST_AND_FOUND {
2048 continue;
2049 }
2050
2051 let etype = entry.file_type()?;
2052 if etype == FileType::dir() {
2053 require_dir_contains_only_mounts(&dir_fd, &entry_name)?;
2054 } else {
2055 anyhow::bail!("Found entry in {dir_name}: {entry_name}");
2056 }
2057 }
2058
2059 Ok(())
2060}
2061
2062#[context("Verifying empty rootfs")]
2063fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
2064 for e in rootfs_fd.entries()? {
2065 let e = DirEntryUtf8::from_cap_std(e?);
2066 let name = e.file_name()?;
2067 if name == LOST_AND_FOUND {
2068 continue;
2069 }
2070
2071 let etype = e.file_type()?;
2073 if etype == FileType::dir() {
2074 require_dir_contains_only_mounts(rootfs_fd, &name)?;
2075 } else {
2076 anyhow::bail!("Non-empty root filesystem; found {name:?}");
2077 }
2078 }
2079 Ok(())
2080}
2081
2082fn remove_all_in_dir_no_xdev(d: &Dir, mount_err: bool) -> Result<()> {
2086 for entry in d.entries()? {
2087 let entry = entry?;
2088 let name = entry.file_name();
2089 let etype = entry.file_type()?;
2090 if etype == FileType::dir() {
2091 if let Some(subdir) = d.open_dir_noxdev(&name)? {
2092 remove_all_in_dir_no_xdev(&subdir, mount_err)?;
2093 d.remove_dir(&name)?;
2094 } else if mount_err {
2095 anyhow::bail!("Found unexpected mount point {name:?}");
2096 }
2097 } else {
2098 d.remove_file_optional(&name)?;
2099 }
2100 }
2101 anyhow::Ok(())
2102}
2103
2104#[context("Removing boot directory content except loader dir on ostree")]
2105fn remove_all_except_loader_dirs(bootdir: &Dir, is_ostree: bool) -> Result<()> {
2106 let entries = bootdir
2107 .entries()
2108 .context("Reading boot directory entries")?;
2109
2110 for entry in entries {
2111 let entry = entry.context("Reading directory entry")?;
2112 let file_name = entry.file_name();
2113 let file_name = if let Some(n) = file_name.to_str() {
2114 n
2115 } else {
2116 anyhow::bail!("Invalid non-UTF8 filename: {file_name:?} in /boot");
2117 };
2118
2119 if is_ostree && file_name.starts_with("loader") {
2123 continue;
2124 }
2125
2126 let etype = entry.file_type()?;
2127 if etype == FileType::dir() {
2128 if let Some(subdir) = bootdir.open_dir_noxdev(&file_name)? {
2130 remove_all_in_dir_no_xdev(&subdir, false)
2131 .with_context(|| format!("Removing directory contents: {}", file_name))?;
2132 bootdir.remove_dir(&file_name)?;
2133 }
2134 } else {
2135 bootdir
2136 .remove_file_optional(&file_name)
2137 .with_context(|| format!("Removing file: {}", file_name))?;
2138 }
2139 }
2140 Ok(())
2141}
2142
2143#[context("Removing boot directory content")]
2144fn clean_boot_directories(rootfs: &Dir, rootfs_path: &Utf8Path, is_ostree: bool) -> Result<()> {
2145 let bootdir =
2146 crate::utils::open_dir_remount_rw(rootfs, BOOT.into()).context("Opening /boot")?;
2147
2148 if ARCH_USES_EFI {
2149 crate::bootloader::mount_esp_part(&rootfs, &rootfs_path, is_ostree)?;
2152 }
2153
2154 remove_all_except_loader_dirs(&bootdir, is_ostree).context("Emptying /boot")?;
2156
2157 if ARCH_USES_EFI {
2159 if let Some(efidir) = bootdir
2160 .open_dir_optional(crate::bootloader::EFI_DIR)
2161 .context("Opening /boot/efi")?
2162 {
2163 remove_all_in_dir_no_xdev(&efidir, false).context("Emptying EFI system partition")?;
2164 }
2165 }
2166
2167 Ok(())
2168}
2169
2170struct RootMountInfo {
2171 mount_spec: String,
2172 kargs: Vec<String>,
2173}
2174
2175fn find_root_args_to_inherit(
2178 cmdline: &bytes::Cmdline,
2179 root_info: &Filesystem,
2180) -> Result<RootMountInfo> {
2181 let root = cmdline
2183 .find_utf8("root")?
2184 .and_then(|p| p.value().map(|p| p.to_string()));
2185 let (mount_spec, kargs) = if let Some(root) = root {
2186 let rootflags = cmdline.find(ROOTFLAGS);
2187 let inherit_kargs = cmdline.find_all_starting_with(INITRD_ARG_PREFIX);
2188 (
2189 root,
2190 rootflags
2191 .into_iter()
2192 .chain(inherit_kargs)
2193 .map(|p| utf8::Parameter::try_from(p).map(|p| p.to_string()))
2194 .collect::<Result<Vec<_>, _>>()?,
2195 )
2196 } else {
2197 let uuid = root_info
2198 .uuid
2199 .as_deref()
2200 .ok_or_else(|| anyhow!("No filesystem uuid found in target root"))?;
2201 (format!("UUID={uuid}"), Vec::new())
2202 };
2203
2204 Ok(RootMountInfo { mount_spec, kargs })
2205}
2206
2207fn warn_on_host_root(rootfs_fd: &Dir) -> Result<()> {
2208 const DELAY_SECONDS: u64 = 20;
2210
2211 let host_root_dfd = &Dir::open_ambient_dir("/proc/1/root", cap_std::ambient_authority())?;
2212 let host_root_devstat = rustix::fs::fstatvfs(host_root_dfd)?;
2213 let target_devstat = rustix::fs::fstatvfs(rootfs_fd)?;
2214 if host_root_devstat.f_fsid != target_devstat.f_fsid {
2215 tracing::debug!("Not the host root");
2216 return Ok(());
2217 }
2218 let dashes = "----------------------------";
2219 let timeout = Duration::from_secs(DELAY_SECONDS);
2220 eprintln!("{dashes}");
2221 crate::utils::medium_visibility_warning(
2222 "WARNING: This operation will OVERWRITE THE BOOTED HOST ROOT FILESYSTEM and is NOT REVERSIBLE.",
2223 );
2224 eprintln!("Waiting {timeout:?} to continue; interrupt (Control-C) to cancel.");
2225 eprintln!("{dashes}");
2226
2227 let bar = indicatif::ProgressBar::new_spinner();
2228 bar.enable_steady_tick(Duration::from_millis(100));
2229 std::thread::sleep(timeout);
2230 bar.finish();
2231
2232 Ok(())
2233}
2234
2235pub enum Cleanup {
2236 Skip,
2237 TriggerOnNextBoot,
2238}
2239
2240#[context("Installing to filesystem")]
2242pub(crate) async fn install_to_filesystem(
2243 opts: InstallToFilesystemOpts,
2244 targeting_host_root: bool,
2245 cleanup: Cleanup,
2246) -> Result<()> {
2247 const INSTALL_FILESYSTEM_JOURNAL_ID: &str = "9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3";
2249 let source_image = opts
2250 .source_opts
2251 .source_imgref
2252 .as_ref()
2253 .map(|s| s.as_str())
2254 .unwrap_or("none");
2255 let target_path = opts.filesystem_opts.root_path.as_str();
2256
2257 tracing::info!(
2258 message_id = INSTALL_FILESYSTEM_JOURNAL_ID,
2259 bootc.source_image = source_image,
2260 bootc.target_path = target_path,
2261 bootc.targeting_host_root = if targeting_host_root { "true" } else { "false" },
2262 "Starting filesystem installation from {} to {}",
2263 source_image,
2264 target_path
2265 );
2266
2267 let state = prepare_install(
2273 opts.config_opts,
2274 opts.source_opts,
2275 opts.target_opts,
2276 opts.composefs_opts,
2277 )
2278 .await?;
2279
2280 let mut fsopts = opts.filesystem_opts;
2282
2283 if targeting_host_root
2286 && fsopts.root_path.as_str() == ALONGSIDE_ROOT_MOUNT
2287 && !fsopts.root_path.try_exists()?
2288 {
2289 tracing::debug!("Mounting host / to {ALONGSIDE_ROOT_MOUNT}");
2290 std::fs::create_dir(ALONGSIDE_ROOT_MOUNT)?;
2291 bootc_mount::bind_mount_from_pidns(
2292 bootc_mount::PID1,
2293 "/".into(),
2294 ALONGSIDE_ROOT_MOUNT.into(),
2295 true,
2296 )
2297 .context("Mounting host / to {ALONGSIDE_ROOT_MOUNT}")?;
2298 }
2299
2300 let target_root_path = fsopts.root_path.clone();
2301 let target_rootfs_fd =
2303 Dir::open_ambient_dir(&target_root_path, cap_std::ambient_authority())
2304 .with_context(|| format!("Opening target root directory {target_root_path}"))?;
2305
2306 tracing::debug!("Target root filesystem: {target_root_path}");
2307
2308 if let Some(false) = target_rootfs_fd.is_mountpoint(".")? {
2309 anyhow::bail!("Not a mountpoint: {target_root_path}");
2310 }
2311
2312 {
2314 let root_path = &fsopts.root_path;
2315 let st = root_path
2316 .symlink_metadata()
2317 .with_context(|| format!("Querying target filesystem {root_path}"))?;
2318 if !st.is_dir() {
2319 anyhow::bail!("Not a directory: {root_path}");
2320 }
2321 }
2322
2323 if !fsopts.acknowledge_destructive {
2325 warn_on_host_root(&target_rootfs_fd)?;
2326 }
2327
2328 let possible_physical_root = fsopts.root_path.join("sysroot");
2331 let possible_ostree_dir = possible_physical_root.join("ostree");
2332 let is_already_ostree = possible_ostree_dir.exists();
2333 if is_already_ostree {
2334 tracing::debug!(
2335 "ostree detected in {possible_ostree_dir}, assuming target is a deployment root and using {possible_physical_root}"
2336 );
2337 fsopts.root_path = possible_physical_root;
2338 };
2339
2340 let rootfs_fd = if is_already_ostree {
2343 let root_path = &fsopts.root_path;
2344 let rootfs_fd = Dir::open_ambient_dir(&fsopts.root_path, cap_std::ambient_authority())
2345 .with_context(|| format!("Opening target root directory {root_path}"))?;
2346
2347 tracing::debug!("Root filesystem: {root_path}");
2348
2349 if let Some(false) = rootfs_fd.is_mountpoint(".")? {
2350 anyhow::bail!("Not a mountpoint: {root_path}");
2351 }
2352 rootfs_fd
2353 } else {
2354 target_rootfs_fd.try_clone()?
2355 };
2356
2357 match fsopts.replace {
2358 Some(ReplaceMode::Wipe) => {
2359 let rootfs_fd = rootfs_fd.try_clone()?;
2360 println!("Wiping contents of root");
2361 tokio::task::spawn_blocking(move || remove_all_in_dir_no_xdev(&rootfs_fd, true))
2362 .await??;
2363 }
2364 Some(ReplaceMode::Alongside) => {
2365 clean_boot_directories(&target_rootfs_fd, &target_root_path, is_already_ostree)?
2366 }
2367 None => require_empty_rootdir(&rootfs_fd)?,
2368 }
2369
2370 let inspect = bootc_mount::inspect_filesystem(&fsopts.root_path)?;
2372
2373 let config_root_mount_spec = state
2378 .install_config
2379 .as_ref()
2380 .and_then(|c| c.root_mount_spec.as_ref());
2381 let root_info = if let Some(s) = fsopts.root_mount_spec.as_ref().or(config_root_mount_spec) {
2382 RootMountInfo {
2383 mount_spec: s.to_string(),
2384 kargs: Vec::new(),
2385 }
2386 } else if targeting_host_root {
2387 let cmdline = bytes::Cmdline::from_proc()?;
2389 find_root_args_to_inherit(&cmdline, &inspect)?
2390 } else {
2391 let uuid = inspect
2394 .uuid
2395 .as_deref()
2396 .ok_or_else(|| anyhow!("No filesystem uuid found in target root"))?;
2397 let kargs = match inspect.fstype.as_str() {
2398 "btrfs" => {
2399 let subvol = crate::utils::find_mount_option(&inspect.options, "subvol");
2400 subvol
2401 .map(|vol| format!("rootflags=subvol={vol}"))
2402 .into_iter()
2403 .collect::<Vec<_>>()
2404 }
2405 _ => Vec::new(),
2406 };
2407 RootMountInfo {
2408 mount_spec: format!("UUID={uuid}"),
2409 kargs,
2410 }
2411 };
2412 tracing::debug!("Root mount: {} {:?}", root_info.mount_spec, root_info.kargs);
2413
2414 let boot_is_mount = {
2415 if let Some(boot_metadata) = target_rootfs_fd.symlink_metadata_optional(BOOT)? {
2416 let root_dev = rootfs_fd.dir_metadata()?.dev();
2417 let boot_dev = boot_metadata.dev();
2418 tracing::debug!("root_dev={root_dev} boot_dev={boot_dev}");
2419 root_dev != boot_dev
2420 } else {
2421 tracing::debug!("No /{BOOT} directory found");
2422 false
2423 }
2424 };
2425 let boot_uuid = if boot_is_mount {
2427 let boot_path = target_root_path.join(BOOT);
2428 tracing::debug!("boot_path={boot_path}");
2429 let u = bootc_mount::inspect_filesystem(&boot_path)
2430 .with_context(|| format!("Inspecting /{BOOT}"))?
2431 .uuid
2432 .ok_or_else(|| anyhow!("No UUID found for /{BOOT}"))?;
2433 Some(u)
2434 } else {
2435 None
2436 };
2437 tracing::debug!("boot UUID: {boot_uuid:?}");
2438
2439 let backing_device = {
2442 let mut dev = inspect.source;
2443 loop {
2444 tracing::debug!("Finding parents for {dev}");
2445 let mut parents = bootc_blockdev::find_parent_devices(&dev)?.into_iter();
2446 let Some(parent) = parents.next() else {
2447 break;
2448 };
2449 if let Some(next) = parents.next() {
2450 anyhow::bail!(
2451 "Found multiple parent devices {parent} and {next}; not currently supported"
2452 );
2453 }
2454 dev = parent;
2455 }
2456 dev
2457 };
2458 tracing::debug!("Backing device: {backing_device}");
2459 let device_info = bootc_blockdev::partitions_of(Utf8Path::new(&backing_device))?;
2460
2461 let rootarg = format!("root={}", root_info.mount_spec);
2462 let config_boot_mount_spec = state
2464 .install_config
2465 .as_ref()
2466 .and_then(|c| c.boot_mount_spec.as_ref());
2467 let mut boot = if let Some(spec) = fsopts.boot_mount_spec.as_ref().or(config_boot_mount_spec) {
2468 if spec.is_empty() {
2471 None
2472 } else {
2473 Some(MountSpec::new(&spec, "/boot"))
2474 }
2475 } else {
2476 read_boot_fstab_entry(&rootfs_fd)?
2479 .filter(|spec| spec.get_source_uuid().is_some())
2480 .or_else(|| {
2481 boot_uuid
2482 .as_deref()
2483 .map(|boot_uuid| MountSpec::new_uuid_src(boot_uuid, "/boot"))
2484 })
2485 };
2486 if let Some(boot) = boot.as_mut() {
2489 boot.push_option("ro");
2490 }
2491 let bootarg = boot.as_ref().map(|boot| format!("boot={}", &boot.source));
2494
2495 let mut kargs = if root_info.mount_spec.is_empty() {
2498 Vec::new()
2499 } else {
2500 [rootarg]
2501 .into_iter()
2502 .chain(root_info.kargs)
2503 .collect::<Vec<_>>()
2504 };
2505
2506 kargs.push(RW_KARG.to_string());
2507
2508 if let Some(bootarg) = bootarg {
2509 kargs.push(bootarg);
2510 }
2511
2512 let kargs = Cmdline::from(kargs.join(" "));
2513
2514 let skip_finalize =
2515 matches!(fsopts.replace, Some(ReplaceMode::Alongside)) || fsopts.skip_finalize;
2516 let mut rootfs = RootSetup {
2517 #[cfg(feature = "install-to-disk")]
2518 luks_device: None,
2519 device_info,
2520 physical_root_path: fsopts.root_path,
2521 physical_root: rootfs_fd,
2522 target_root_path: Some(target_root_path.clone()),
2523 rootfs_uuid: inspect.uuid.clone(),
2524 boot,
2525 kargs,
2526 skip_finalize,
2527 };
2528
2529 install_to_filesystem_impl(&state, &mut rootfs, cleanup).await?;
2530
2531 drop(rootfs);
2533
2534 installation_complete();
2535
2536 Ok(())
2537}
2538
2539pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) -> Result<()> {
2540 const INSTALL_EXISTING_ROOT_JOURNAL_ID: &str = "7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1";
2542 let source_image = opts
2543 .source_opts
2544 .source_imgref
2545 .as_ref()
2546 .map(|s| s.as_str())
2547 .unwrap_or("none");
2548 let target_path = opts.root_path.as_str();
2549
2550 tracing::info!(
2551 message_id = INSTALL_EXISTING_ROOT_JOURNAL_ID,
2552 bootc.source_image = source_image,
2553 bootc.target_path = target_path,
2554 bootc.cleanup = if opts.cleanup {
2555 "trigger_on_next_boot"
2556 } else {
2557 "skip"
2558 },
2559 "Starting installation to existing root from {} to {}",
2560 source_image,
2561 target_path
2562 );
2563
2564 let cleanup = match opts.cleanup {
2565 true => Cleanup::TriggerOnNextBoot,
2566 false => Cleanup::Skip,
2567 };
2568
2569 let opts = InstallToFilesystemOpts {
2570 filesystem_opts: InstallTargetFilesystemOpts {
2571 root_path: opts.root_path,
2572 root_mount_spec: None,
2573 boot_mount_spec: None,
2574 replace: opts.replace,
2575 skip_finalize: true,
2576 acknowledge_destructive: opts.acknowledge_destructive,
2577 },
2578 source_opts: opts.source_opts,
2579 target_opts: opts.target_opts,
2580 config_opts: opts.config_opts,
2581 composefs_opts: opts.composefs_opts,
2582 };
2583
2584 install_to_filesystem(opts, true, cleanup).await
2585}
2586
2587fn read_boot_fstab_entry(root: &Dir) -> Result<Option<MountSpec>> {
2589 let fstab_path = "etc/fstab";
2590 let fstab = match root.open_optional(fstab_path)? {
2591 Some(f) => f,
2592 None => return Ok(None),
2593 };
2594
2595 let reader = std::io::BufReader::new(fstab);
2596 for line in std::io::BufRead::lines(reader) {
2597 let line = line?;
2598 let line = line.trim();
2599
2600 if line.is_empty() || line.starts_with('#') {
2602 continue;
2603 }
2604
2605 let spec = MountSpec::from_str(line)?;
2607
2608 if spec.target == "/boot" {
2610 return Ok(Some(spec));
2611 }
2612 }
2613
2614 Ok(None)
2615}
2616
2617pub(crate) async fn install_reset(opts: InstallResetOpts) -> Result<()> {
2618 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2619 if !opts.experimental {
2620 anyhow::bail!("This command requires --experimental");
2621 }
2622
2623 let prog: ProgressWriter = opts.progress.try_into()?;
2624
2625 let sysroot = &crate::cli::get_storage().await?;
2626 let ostree = sysroot.get_ostree()?;
2627 let repo = &ostree.repo();
2628 let (booted_ostree, _deployments, host) = crate::status::get_status_require_booted(ostree)?;
2629
2630 let stateroots = list_stateroots(ostree)?;
2631 let target_stateroot = if let Some(s) = opts.stateroot {
2632 s
2633 } else {
2634 let now = chrono::Utc::now();
2635 let r = allocate_new_stateroot(&ostree, &stateroots, now)?;
2636 r.name
2637 };
2638
2639 let booted_stateroot = booted_ostree.stateroot();
2640 assert!(booted_stateroot.as_str() != target_stateroot);
2641 let (fetched, spec) = if let Some(target) = opts.target_opts.imageref()? {
2642 let mut new_spec = host.spec;
2643 new_spec.image = Some(target.into());
2644 let fetched = crate::deploy::pull(
2645 repo,
2646 &new_spec.image.as_ref().unwrap(),
2647 None,
2648 opts.quiet,
2649 prog.clone(),
2650 )
2651 .await?;
2652 (fetched, new_spec)
2653 } else {
2654 let imgstate = host
2655 .status
2656 .booted
2657 .map(|b| b.query_image(repo))
2658 .transpose()?
2659 .flatten()
2660 .ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
2661 (Box::new((*imgstate).into()), host.spec)
2662 };
2663 let spec = crate::deploy::RequiredHostSpec::from_spec(&spec)?;
2664
2665 let mut kargs = crate::bootc_kargs::get_kargs_in_root(rootfs, std::env::consts::ARCH)?;
2668
2669 if !opts.no_root_kargs {
2671 let bootcfg = booted_ostree
2672 .deployment
2673 .bootconfig()
2674 .ok_or_else(|| anyhow!("Missing bootcfg for booted deployment"))?;
2675 if let Some(options) = bootcfg.get("options") {
2676 let options_cmdline = Cmdline::from(options.as_str());
2677 let root_kargs = crate::bootc_kargs::root_args_from_cmdline(&options_cmdline);
2678 kargs.extend(&root_kargs);
2679 }
2680 }
2681
2682 if let Some(user_kargs) = opts.karg.as_ref() {
2684 for karg in user_kargs {
2685 kargs.extend(karg);
2686 }
2687 }
2688
2689 let from = MergeState::Reset {
2690 stateroot: target_stateroot.clone(),
2691 kargs,
2692 };
2693 crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone(), false).await?;
2694
2695 if let Some(boot_spec) = read_boot_fstab_entry(rootfs)? {
2697 let staged_deployment = ostree
2698 .staged_deployment()
2699 .ok_or_else(|| anyhow!("No staged deployment found"))?;
2700 let deployment_path = ostree.deployment_dirpath(&staged_deployment);
2701 let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
2702 let deployment_root = sysroot_dir.open_dir(&deployment_path)?;
2703
2704 crate::lsm::atomic_replace_labeled(
2706 &deployment_root,
2707 "etc/fstab",
2708 0o644.into(),
2709 None,
2710 |w| writeln!(w, "{}", boot_spec.to_fstab()).map_err(Into::into),
2711 )?;
2712
2713 tracing::debug!(
2714 "Copied /boot entry to new stateroot: {}",
2715 boot_spec.to_fstab()
2716 );
2717 }
2718
2719 sysroot.update_mtime()?;
2720
2721 if opts.apply {
2722 crate::reboot::reboot()?;
2723 }
2724 Ok(())
2725}
2726
2727pub(crate) async fn install_finalize(target: &Utf8Path) -> Result<()> {
2729 const INSTALL_FINALIZE_JOURNAL_ID: &str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0";
2731
2732 tracing::info!(
2733 message_id = INSTALL_FINALIZE_JOURNAL_ID,
2734 bootc.target_path = target.as_str(),
2735 "Starting installation finalization for target: {}",
2736 target
2737 );
2738
2739 crate::cli::require_root(false)?;
2740 let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(target)));
2741 sysroot.load(gio::Cancellable::NONE)?;
2742 let deployments = sysroot.deployments();
2743 if deployments.is_empty() {
2745 anyhow::bail!("Failed to find deployment in {target}");
2746 }
2747
2748 tracing::info!(
2750 message_id = INSTALL_FINALIZE_JOURNAL_ID,
2751 bootc.target_path = target.as_str(),
2752 "Successfully finalized installation for target: {}",
2753 target
2754 );
2755
2756 Ok(())
2760}
2761
2762#[cfg(test)]
2763mod tests {
2764 use super::*;
2765
2766 #[test]
2767 fn install_opts_serializable() {
2768 let c: InstallToDiskOpts = serde_json::from_value(serde_json::json!({
2769 "device": "/dev/vda"
2770 }))
2771 .unwrap();
2772 assert_eq!(c.block_opts.device, "/dev/vda");
2773 }
2774
2775 #[test]
2776 fn test_mountspec() {
2777 let mut ms = MountSpec::new("/dev/vda4", "/boot");
2778 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto defaults 0 0");
2779 ms.push_option("ro");
2780 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto ro 0 0");
2781 ms.push_option("relatime");
2782 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto ro,relatime 0 0");
2783 }
2784
2785 #[test]
2786 fn test_gather_root_args() {
2787 let inspect = Filesystem {
2789 source: "/dev/vda4".into(),
2790 target: "/".into(),
2791 fstype: "xfs".into(),
2792 maj_min: "252:4".into(),
2793 options: "rw".into(),
2794 uuid: Some("965eb3c7-5a3f-470d-aaa2-1bcf04334bc6".into()),
2795 children: None,
2796 };
2797 let kargs = bytes::Cmdline::from("");
2798 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
2799 assert_eq!(r.mount_spec, "UUID=965eb3c7-5a3f-470d-aaa2-1bcf04334bc6");
2800
2801 let kargs = bytes::Cmdline::from(
2802 "root=/dev/mapper/root rw someother=karg rd.lvm.lv=root systemd.debug=1",
2803 );
2804
2805 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
2807 assert_eq!(r.mount_spec, "/dev/mapper/root");
2808 assert_eq!(r.kargs.len(), 1);
2809 assert_eq!(r.kargs[0], "rd.lvm.lv=root");
2810
2811 let kargs = bytes::Cmdline::from(
2813 b"root=/dev/mapper/root rw non-utf8=\xff rd.lvm.lv=root systemd.debug=1",
2814 );
2815 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
2816 assert_eq!(r.mount_spec, "/dev/mapper/root");
2817 assert_eq!(r.kargs.len(), 1);
2818 assert_eq!(r.kargs[0], "rd.lvm.lv=root");
2819
2820 let kargs = bytes::Cmdline::from(
2822 b"root=/dev/mapper/ro\xffot rw non-utf8=\xff rd.lvm.lv=root systemd.debug=1",
2823 );
2824 let r = find_root_args_to_inherit(&kargs, &inspect);
2825 assert!(r.is_err());
2826
2827 let kargs = bytes::Cmdline::from(
2829 b"root=/dev/mapper/root rw non-utf8=\xff rd.lvm.lv=ro\xffot systemd.debug=1",
2830 );
2831 let r = find_root_args_to_inherit(&kargs, &inspect);
2832 assert!(r.is_err());
2833 }
2834
2835 #[test]
2838 fn test_remove_all_noxdev() -> Result<()> {
2839 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2840
2841 td.create_dir_all("foo/bar/baz")?;
2842 td.write("foo/bar/baz/test", b"sometest")?;
2843 td.symlink_contents("/absolute-nonexistent-link", "somelink")?;
2844 td.write("toptestfile", b"othertestcontents")?;
2845
2846 remove_all_in_dir_no_xdev(&td, true).unwrap();
2847
2848 assert_eq!(td.entries()?.count(), 0);
2849
2850 Ok(())
2851 }
2852
2853 #[test]
2854 fn test_read_boot_fstab_entry() -> Result<()> {
2855 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2856
2857 assert!(read_boot_fstab_entry(&td)?.is_none());
2859
2860 td.create_dir("etc")?;
2862 td.write("etc/fstab", "UUID=test-uuid / ext4 defaults 0 0\n")?;
2863 assert!(read_boot_fstab_entry(&td)?.is_none());
2864
2865 let fstab_content = "\
2867# /etc/fstab
2868UUID=root-uuid / ext4 defaults 0 0
2869UUID=boot-uuid /boot ext4 ro 0 0
2870UUID=home-uuid /home ext4 defaults 0 0
2871";
2872 td.write("etc/fstab", fstab_content)?;
2873 let boot_spec = read_boot_fstab_entry(&td)?.unwrap();
2874 assert_eq!(boot_spec.source, "UUID=boot-uuid");
2875 assert_eq!(boot_spec.target, "/boot");
2876 assert_eq!(boot_spec.fstype, "ext4");
2877 assert_eq!(boot_spec.options, Some("ro".to_string()));
2878
2879 let fstab_content = "\
2881# /etc/fstab
2882# Created by anaconda
2883UUID=root-uuid / ext4 defaults 0 0
2884# Boot partition
2885UUID=boot-uuid /boot ext4 defaults 0 0
2886";
2887 td.write("etc/fstab", fstab_content)?;
2888 let boot_spec = read_boot_fstab_entry(&td)?.unwrap();
2889 assert_eq!(boot_spec.source, "UUID=boot-uuid");
2890 assert_eq!(boot_spec.target, "/boot");
2891
2892 Ok(())
2893 }
2894
2895 #[test]
2896 fn test_require_dir_contains_only_mounts() -> Result<()> {
2897 {
2899 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2900 td.create_dir("empty")?;
2901 assert!(require_dir_contains_only_mounts(&td, "empty").is_err());
2902 }
2903
2904 {
2906 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2907 td.create_dir_all("var/lost+found")?;
2908 assert!(require_dir_contains_only_mounts(&td, "var").is_ok());
2909 }
2910
2911 {
2913 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2914 td.create_dir("var")?;
2915 td.write("var/test.txt", b"content")?;
2916 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2917 }
2918
2919 {
2921 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2922 td.create_dir_all("var/lib/containers")?;
2923 td.write("var/lib/containers/storage.db", b"data")?;
2924 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2925 }
2926
2927 {
2929 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2930 td.create_dir_all("boot/grub2")?;
2931 td.write("boot/grub2/grub.cfg", b"config")?;
2932 assert!(require_dir_contains_only_mounts(&td, "boot").is_err());
2933 }
2934
2935 {
2937 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2938 td.create_dir_all("var/lib/containers")?;
2939 td.create_dir_all("var/log/journal")?;
2940 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2941 }
2942
2943 {
2945 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2946 td.create_dir_all("var/lost+found")?;
2947 td.write("var/data.txt", b"content")?;
2948 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2949 }
2950
2951 {
2953 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2954 td.create_dir("var")?;
2955 td.symlink_contents("../usr/lib", "var/lib")?;
2956 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2957 }
2958
2959 {
2961 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2962 td.create_dir_all("var/lib/containers/storage/overlay")?;
2963 td.write("var/lib/containers/storage/overlay/file.txt", b"data")?;
2964 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2965 }
2966
2967 Ok(())
2968 }
2969}