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::status::ComposefsCmdline;
191use crate::bootc_composefs::{
192 boot::setup_composefs_boot,
193 repo::{get_imgref, initialize_composefs_repository, open_composefs_repo},
194 status::get_container_manifest_and_config,
195};
196use crate::boundimage::{BoundImage, ResolvedBoundImage};
197use crate::containerenv::ContainerExecutionInfo;
198use crate::deploy::{MergeState, PreparedPullResult, prepare_for_pull, pull_from_prepared};
199use crate::install::config::Filesystem as FilesystemEnum;
200use crate::lsm;
201use crate::progress_jsonl::ProgressWriter;
202use crate::spec::{Bootloader, ImageReference};
203use crate::store::Storage;
204use crate::task::Task;
205use crate::utils::sigpolicy_from_opt;
206use bootc_kernel_cmdline::{INITRD_ARG_PREFIX, ROOTFLAGS, bytes, utf8};
207use bootc_mount::Filesystem;
208use cfsctl::composefs;
209use composefs::fsverity::FsVerityHashValue;
210
211pub(crate) const BOOT: &str = "boot";
213#[cfg(feature = "install-to-disk")]
215const RUN_BOOTC: &str = "/run/bootc";
216const ALONGSIDE_ROOT_MOUNT: &str = "/target";
218pub(crate) const DESTRUCTIVE_CLEANUP: &str = "etc/bootc-destructive-cleanup";
220const LOST_AND_FOUND: &str = "lost+found";
222const OSTREE_COMPOSEFS_SUPER: &str = ".ostree.cfs";
224const SELINUXFS: &str = "/sys/fs/selinux";
226pub(crate) const EFIVARFS: &str = "/sys/firmware/efi/efivars";
228pub(crate) const ARCH_USES_EFI: bool = cfg!(any(target_arch = "x86_64", target_arch = "aarch64"));
229
230pub(crate) const EFI_LOADER_INFO: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
231
232const DEFAULT_REPO_CONFIG: &[(&str, &str)] = &[
233 ("sysroot.bootloader", "none"),
235 ("sysroot.bootprefix", "true"),
238 ("sysroot.readonly", "true"),
239];
240
241pub(crate) const RW_KARG: &str = "rw";
243
244#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
245pub(crate) struct InstallTargetOpts {
246 #[clap(long, default_value = "registry")]
250 #[serde(default)]
251 pub(crate) target_transport: String,
252
253 #[clap(long)]
255 pub(crate) target_imgref: Option<String>,
256
257 #[clap(long, hide = true)]
267 #[serde(default)]
268 pub(crate) target_no_signature_verification: bool,
269
270 #[clap(long)]
274 #[serde(default)]
275 pub(crate) enforce_container_sigpolicy: bool,
276
277 #[clap(long)]
280 #[serde(default)]
281 pub(crate) run_fetch_check: bool,
282
283 #[clap(long)]
286 #[serde(default)]
287 pub(crate) skip_fetch_check: bool,
288
289 #[clap(long = "experimental-unified-storage", hide = true)]
295 #[serde(default)]
296 pub(crate) unified_storage_exp: bool,
297}
298
299#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
300pub(crate) struct InstallSourceOpts {
301 #[clap(long)]
308 pub(crate) source_imgref: Option<String>,
309}
310
311#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
312#[serde(rename_all = "kebab-case")]
313pub(crate) enum BoundImagesOpt {
314 #[default]
316 Stored,
317 #[clap(hide = true)]
318 Skip,
320 Pull,
324}
325
326impl std::fmt::Display for BoundImagesOpt {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 self.to_possible_value().unwrap().get_name().fmt(f)
329 }
330}
331
332#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
333pub(crate) struct InstallConfigOpts {
334 #[clap(long)]
339 #[serde(default)]
340 pub(crate) disable_selinux: bool,
341
342 #[clap(long)]
346 pub(crate) karg: Option<Vec<CmdlineOwned>>,
347
348 #[clap(long)]
352 pub(crate) karg_delete: Option<Vec<String>>,
353
354 #[clap(long)]
362 root_ssh_authorized_keys: Option<Utf8PathBuf>,
363
364 #[clap(long)]
370 #[serde(default)]
371 pub(crate) generic_image: bool,
372
373 #[clap(long)]
375 #[serde(default)]
376 #[arg(default_value_t)]
377 pub(crate) bound_images: BoundImagesOpt,
378
379 #[clap(long)]
381 pub(crate) stateroot: Option<String>,
382
383 #[clap(long)]
385 #[serde(default)]
386 pub(crate) bootupd_skip_boot_uuid: bool,
387
388 #[clap(long)]
390 #[serde(default)]
391 pub(crate) bootloader: Option<Bootloader>,
392}
393
394#[derive(Debug, Default, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
395pub(crate) struct InstallComposefsOpts {
396 #[clap(long, default_value_t)]
398 #[serde(default)]
399 pub(crate) composefs_backend: bool,
400
401 #[clap(long, default_value_t, requires = "composefs_backend")]
403 #[serde(default)]
404 pub(crate) allow_missing_verity: bool,
405
406 #[clap(long, requires = "composefs_backend")]
409 #[serde(default)]
410 pub(crate) uki_addon: Option<Vec<String>>,
411}
412
413#[cfg(feature = "install-to-disk")]
414#[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
415pub(crate) struct InstallToDiskOpts {
416 #[clap(flatten)]
417 #[serde(flatten)]
418 pub(crate) block_opts: InstallBlockDeviceOpts,
419
420 #[clap(flatten)]
421 #[serde(flatten)]
422 pub(crate) source_opts: InstallSourceOpts,
423
424 #[clap(flatten)]
425 #[serde(flatten)]
426 pub(crate) target_opts: InstallTargetOpts,
427
428 #[clap(flatten)]
429 #[serde(flatten)]
430 pub(crate) config_opts: InstallConfigOpts,
431
432 #[clap(long)]
434 #[serde(default)]
435 pub(crate) via_loopback: bool,
436
437 #[clap(flatten)]
438 #[serde(flatten)]
439 pub(crate) composefs_opts: InstallComposefsOpts,
440}
441
442#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
443#[serde(rename_all = "kebab-case")]
444pub(crate) enum ReplaceMode {
445 Wipe,
448 Alongside,
456}
457
458impl std::fmt::Display for ReplaceMode {
459 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
460 self.to_possible_value().unwrap().get_name().fmt(f)
461 }
462}
463
464#[derive(Debug, Clone, clap::Args, PartialEq, Eq)]
466pub(crate) struct InstallTargetFilesystemOpts {
467 pub(crate) root_path: Utf8PathBuf,
472
473 #[clap(long)]
477 pub(crate) root_mount_spec: Option<String>,
478
479 #[clap(long)]
484 pub(crate) boot_mount_spec: Option<String>,
485
486 #[clap(long)]
489 pub(crate) replace: Option<ReplaceMode>,
490
491 #[clap(long)]
493 pub(crate) acknowledge_destructive: bool,
494
495 #[clap(long)]
499 pub(crate) skip_finalize: bool,
500}
501
502#[derive(Debug, Clone, clap::Parser, PartialEq, Eq)]
503pub(crate) struct InstallToFilesystemOpts {
504 #[clap(flatten)]
505 pub(crate) filesystem_opts: InstallTargetFilesystemOpts,
506
507 #[clap(flatten)]
508 pub(crate) source_opts: InstallSourceOpts,
509
510 #[clap(flatten)]
511 pub(crate) target_opts: InstallTargetOpts,
512
513 #[clap(flatten)]
514 pub(crate) config_opts: InstallConfigOpts,
515
516 #[clap(flatten)]
517 pub(crate) composefs_opts: InstallComposefsOpts,
518}
519
520#[derive(Debug, Clone, clap::Parser, PartialEq, Eq)]
521pub(crate) struct InstallToExistingRootOpts {
522 #[clap(long, default_value = "alongside")]
524 pub(crate) replace: Option<ReplaceMode>,
525
526 #[clap(flatten)]
527 pub(crate) source_opts: InstallSourceOpts,
528
529 #[clap(flatten)]
530 pub(crate) target_opts: InstallTargetOpts,
531
532 #[clap(flatten)]
533 pub(crate) config_opts: InstallConfigOpts,
534
535 #[clap(long)]
537 pub(crate) acknowledge_destructive: bool,
538
539 #[clap(long)]
542 pub(crate) cleanup: bool,
543
544 #[clap(default_value = ALONGSIDE_ROOT_MOUNT)]
548 pub(crate) root_path: Utf8PathBuf,
549
550 #[clap(flatten)]
551 pub(crate) composefs_opts: InstallComposefsOpts,
552}
553
554#[derive(Debug, clap::Parser, PartialEq, Eq)]
555pub(crate) struct InstallResetOpts {
556 #[clap(long)]
558 pub(crate) experimental: bool,
559
560 #[clap(flatten)]
561 pub(crate) source_opts: InstallSourceOpts,
562
563 #[clap(flatten)]
564 pub(crate) target_opts: InstallTargetOpts,
565
566 #[clap(long)]
570 pub(crate) stateroot: Option<String>,
571
572 #[clap(long)]
574 pub(crate) quiet: bool,
575
576 #[clap(flatten)]
577 pub(crate) progress: crate::cli::ProgressOptions,
578
579 #[clap(long)]
585 pub(crate) apply: bool,
586
587 #[clap(long)]
589 no_root_kargs: bool,
590
591 #[clap(long)]
595 karg: Option<Vec<CmdlineOwned>>,
596}
597
598#[derive(Debug, clap::Parser, PartialEq, Eq)]
599pub(crate) struct InstallPrintConfigurationOpts {
600 #[clap(long)]
604 pub(crate) all: bool,
605}
606
607#[derive(Debug, Clone)]
609pub(crate) struct SourceInfo {
610 pub(crate) imageref: ostree_container::ImageReference,
612 pub(crate) digest: Option<String>,
614 pub(crate) selinux: bool,
616 pub(crate) in_host_mountns: bool,
618}
619
620#[derive(Debug)]
622pub(crate) struct State {
623 pub(crate) source: SourceInfo,
624 pub(crate) selinux_state: SELinuxFinalState,
626 #[allow(dead_code)]
627 pub(crate) config_opts: InstallConfigOpts,
628 pub(crate) target_opts: InstallTargetOpts,
629 pub(crate) target_imgref: ostree_container::OstreeImageReference,
630 #[allow(dead_code)]
631 pub(crate) prepareroot_config: HashMap<String, String>,
632 pub(crate) install_config: Option<config::InstallConfiguration>,
633 pub(crate) root_ssh_authorized_keys: Option<String>,
635 #[allow(dead_code)]
636 pub(crate) host_is_container: bool,
637 pub(crate) container_root: Dir,
639 pub(crate) tempdir: TempDir,
640
641 #[allow(dead_code)]
643 pub(crate) composefs_required: bool,
644
645 pub(crate) composefs_options: InstallComposefsOpts,
647}
648
649#[derive(Debug)]
651pub(crate) struct PostFetchState {
652 pub(crate) detected_bootloader: crate::spec::Bootloader,
654}
655
656impl InstallTargetOpts {
657 pub(crate) fn imageref(&self) -> Result<Option<ostree_container::OstreeImageReference>> {
658 let Some(target_imgname) = self.target_imgref.as_deref() else {
659 return Ok(None);
660 };
661 let target_transport =
662 ostree_container::Transport::try_from(self.target_transport.as_str())?;
663 let target_imgref = ostree_container::OstreeImageReference {
664 sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
665 imgref: ostree_container::ImageReference {
666 transport: target_transport,
667 name: target_imgname.to_string(),
668 },
669 };
670 Ok(Some(target_imgref))
671 }
672}
673
674impl State {
675 #[context("Loading SELinux policy")]
676 pub(crate) fn load_policy(&self) -> Result<Option<ostree::SePolicy>> {
677 if !self.selinux_state.enabled() {
678 return Ok(None);
679 }
680 let r = lsm::new_sepolicy_at(&self.container_root)?
682 .ok_or_else(|| anyhow::anyhow!("SELinux enabled, but no policy found in root"))?;
683 tracing::debug!("Loaded SELinux policy: {}", r.csum().unwrap());
685 Ok(Some(r))
686 }
687
688 #[context("Finalizing state")]
689 #[allow(dead_code)]
690 pub(crate) fn consume(self) -> Result<()> {
691 self.tempdir.close()?;
692 if let SELinuxFinalState::Enabled(Some(guard)) = self.selinux_state {
694 guard.consume()?;
695 }
696 Ok(())
697 }
698
699 pub(crate) fn require_no_kargs_for_uki(&self) -> Result<()> {
701 if self
702 .config_opts
703 .karg
704 .as_ref()
705 .map(|v| !v.is_empty())
706 .unwrap_or_default()
707 {
708 anyhow::bail!("Cannot use externally specified kernel arguments with UKI");
709 }
710 Ok(())
711 }
712
713 fn stateroot(&self) -> &str {
714 self.config_opts
716 .stateroot
717 .as_deref()
718 .or_else(|| {
719 self.install_config
720 .as_ref()
721 .and_then(|c| c.stateroot.as_deref())
722 })
723 .unwrap_or(ostree_ext::container::deploy::STATEROOT_DEFAULT)
724 }
725}
726
727#[derive(Debug, Clone)]
738pub(crate) struct MountSpec {
739 pub(crate) source: String,
740 pub(crate) target: String,
741 pub(crate) fstype: String,
742 pub(crate) options: Option<String>,
743}
744
745impl MountSpec {
746 const AUTO: &'static str = "auto";
747
748 pub(crate) fn new(src: &str, target: &str) -> Self {
749 MountSpec {
750 source: src.to_string(),
751 target: target.to_string(),
752 fstype: Self::AUTO.to_string(),
753 options: None,
754 }
755 }
756
757 pub(crate) fn new_uuid_src(uuid: &str, target: &str) -> Self {
759 Self::new(&format!("UUID={uuid}"), target)
760 }
761
762 pub(crate) fn get_source_uuid(&self) -> Option<&str> {
763 if let Some((t, rest)) = self.source.split_once('=') {
764 if t.eq_ignore_ascii_case("uuid") {
765 return Some(rest);
766 }
767 }
768 None
769 }
770
771 pub(crate) fn to_fstab(&self) -> String {
772 let options = self.options.as_deref().unwrap_or("defaults");
773 format!(
774 "{} {} {} {} 0 0",
775 self.source, self.target, self.fstype, options
776 )
777 }
778
779 pub(crate) fn push_option(&mut self, opt: &str) {
781 let options = self.options.get_or_insert_with(Default::default);
782 if !options.is_empty() {
783 options.push(',');
784 }
785 options.push_str(opt);
786 }
787}
788
789impl FromStr for MountSpec {
790 type Err = anyhow::Error;
791
792 fn from_str(s: &str) -> Result<Self> {
793 let mut parts = s.split_ascii_whitespace().fuse();
794 let source = parts.next().unwrap_or_default();
795 if source.is_empty() {
796 tracing::debug!("Empty mount specification");
797 return Ok(Self {
798 source: String::new(),
799 target: String::new(),
800 fstype: Self::AUTO.into(),
801 options: None,
802 });
803 }
804 let target = parts
805 .next()
806 .ok_or_else(|| anyhow!("Missing target in mount specification {s}"))?;
807 let fstype = parts.next().unwrap_or(Self::AUTO);
808 let options = parts.next().map(ToOwned::to_owned);
809 Ok(Self {
810 source: source.to_string(),
811 fstype: fstype.to_string(),
812 target: target.to_string(),
813 options,
814 })
815 }
816}
817
818impl SourceInfo {
819 #[context("Gathering source info from container env")]
822 pub(crate) fn from_container(
823 root: &Dir,
824 container_info: &ContainerExecutionInfo,
825 ) -> Result<Self> {
826 if !container_info.engine.starts_with("podman") {
827 anyhow::bail!("Currently this command only supports being executed via podman");
828 }
829 if container_info.imageid.is_empty() {
830 anyhow::bail!("Invalid empty imageid");
831 }
832 let imageref = ostree_container::ImageReference {
833 transport: ostree_container::Transport::ContainerStorage,
834 name: container_info.image.clone(),
835 };
836 tracing::debug!("Finding digest for image ID {}", container_info.imageid);
837 let digest = crate::podman::imageid_to_digest(&container_info.imageid)?;
838
839 Self::new(imageref, Some(digest), root, true)
840 }
841
842 #[context("Creating source info from a given imageref")]
843 pub(crate) fn from_imageref(imageref: &str, root: &Dir) -> Result<Self> {
844 let imageref = ostree_container::ImageReference::try_from(imageref)?;
845 Self::new(imageref, None, root, false)
846 }
847
848 fn have_selinux_from_repo(root: &Dir) -> Result<bool> {
849 let cancellable = ostree::gio::Cancellable::NONE;
850
851 let commit = Command::new("ostree")
852 .args(["--repo=/ostree/repo", "rev-parse", "--single"])
853 .run_get_string()?;
854 let repo = ostree::Repo::open_at_dir(root.as_fd(), "ostree/repo")?;
855 let root = repo
856 .read_commit(commit.trim(), cancellable)
857 .context("Reading commit")?
858 .0;
859 let root = root.downcast_ref::<ostree::RepoFile>().unwrap();
860 let xattrs = root.xattrs(cancellable)?;
861 Ok(crate::lsm::xattrs_have_selinux(&xattrs))
862 }
863
864 fn new(
866 imageref: ostree_container::ImageReference,
867 digest: Option<String>,
868 root: &Dir,
869 in_host_mountns: bool,
870 ) -> Result<Self> {
871 let selinux = if Path::new("/ostree/repo").try_exists()? {
872 Self::have_selinux_from_repo(root)?
873 } else {
874 lsm::have_selinux_policy(root)?
875 };
876 Ok(Self {
877 imageref,
878 digest,
879 selinux,
880 in_host_mountns,
881 })
882 }
883}
884
885pub(crate) fn print_configuration(opts: InstallPrintConfigurationOpts) -> Result<()> {
886 let mut install_config = config::load_config()?.unwrap_or_default();
887 if !opts.all {
888 install_config.filter_to_external();
889 }
890 let stdout = std::io::stdout().lock();
891 anyhow::Ok(install_config.to_canon_json_writer(stdout)?)
892}
893
894#[context("Creating ostree deployment")]
895async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result<(Storage, bool)> {
896 let sepolicy = state.load_policy()?;
897 let sepolicy = sepolicy.as_ref();
898 let rootfs_dir = &root_setup.physical_root;
900 let cancellable = gio::Cancellable::NONE;
901
902 let stateroot = state.stateroot();
903
904 let has_ostree = rootfs_dir.try_exists("ostree/repo")?;
905 if !has_ostree {
906 Task::new("Initializing ostree layout", "ostree")
907 .args(["admin", "init-fs", "--modern", "."])
908 .cwd(rootfs_dir)?
909 .run()?;
910 } else {
911 println!("Reusing extant ostree layout");
912
913 let path = ".".into();
914 let _ = crate::utils::open_dir_remount_rw(rootfs_dir, path)
915 .context("remounting target as read-write")?;
916 crate::utils::remove_immutability(rootfs_dir, path)?;
917 }
918
919 crate::lsm::ensure_dir_labeled(rootfs_dir, "", Some("/".into()), 0o755.into(), sepolicy)?;
922
923 if has_ostree && root_setup.boot.is_some() {
926 if let Some(boot) = &root_setup.boot {
927 let source_boot = &boot.source;
928 let target_boot = root_setup.physical_root_path.join(BOOT);
929 tracing::debug!("Mount {source_boot} to {target_boot} on ostree");
930 bootc_mount::mount(source_boot, &target_boot)?;
931 }
932 }
933
934 if rootfs_dir.try_exists("boot")? {
936 crate::lsm::ensure_dir_labeled(rootfs_dir, "boot", None, 0o755.into(), sepolicy)?;
937 }
938
939 let ostree_opts = state
941 .install_config
942 .as_ref()
943 .and_then(|c| c.ostree.as_ref())
944 .into_iter()
945 .flat_map(|o| o.to_config_tuples());
946
947 let repo_config: Vec<_> = DEFAULT_REPO_CONFIG
948 .iter()
949 .copied()
950 .chain(ostree_opts)
951 .collect();
952
953 for (k, v) in repo_config.iter() {
954 Command::new("ostree")
955 .args(["config", "--repo", "ostree/repo", "set", k, v])
956 .cwd_dir(rootfs_dir.try_clone()?)
957 .run_capture_stderr()?;
958 }
959
960 let sysroot = {
961 let path = format!(
962 "/proc/{}/fd/{}",
963 process::id(),
964 rootfs_dir.as_fd().as_raw_fd()
965 );
966 ostree::Sysroot::new(Some(&gio::File::for_path(path)))
967 };
968 sysroot.load(cancellable)?;
969 let repo = &sysroot.repo();
970
971 let repo_verity_state = ostree_ext::fsverity::is_verity_enabled(&repo)?;
972 let prepare_root_composefs = state
973 .prepareroot_config
974 .get("composefs.enabled")
975 .map(|v| ComposefsState::from_str(&v))
976 .transpose()?
977 .unwrap_or(ComposefsState::default());
978 if prepare_root_composefs.requires_fsverity() || repo_verity_state.desired == Tristate::Enabled
979 {
980 ostree_ext::fsverity::ensure_verity(repo).await?;
981 }
982
983 if let Some(booted) = sysroot.booted_deployment() {
984 if stateroot == booted.stateroot() {
985 anyhow::bail!("Cannot redeploy over booted stateroot {stateroot}");
986 }
987 }
988
989 let sysroot_dir = crate::utils::sysroot_dir(&sysroot)?;
990
991 let stateroot_path = format!("ostree/deploy/{stateroot}");
996 if !sysroot_dir.try_exists(stateroot_path)? {
997 sysroot
998 .init_osname(stateroot, cancellable)
999 .context("initializing stateroot")?;
1000 }
1001
1002 state.tempdir.create_dir("temp-run")?;
1003 let temp_run = state.tempdir.open_dir("temp-run")?;
1004
1005 if let Some(policy) = sepolicy {
1008 let ostree_dir = rootfs_dir.open_dir("ostree")?;
1009 crate::lsm::ensure_dir_labeled(
1010 &ostree_dir,
1011 ".",
1012 Some("/usr".into()),
1013 0o755.into(),
1014 Some(policy),
1015 )?;
1016 }
1017
1018 sysroot.load(cancellable)?;
1019 let sysroot = SysrootLock::new_from_sysroot(&sysroot).await?;
1020 let storage = Storage::new_ostree(sysroot, &temp_run)?;
1021
1022 Ok((storage, has_ostree))
1023}
1024
1025#[context("Creating ostree deployment")]
1026async fn install_container(
1027 state: &State,
1028 root_setup: &RootSetup,
1029 sysroot: &ostree::Sysroot,
1030 storage: &Storage,
1031 has_ostree: bool,
1032) -> Result<(ostree::Deployment, InstallAleph)> {
1033 let sepolicy = state.load_policy()?;
1034 let sepolicy = sepolicy.as_ref();
1035 let stateroot = state.stateroot();
1036
1037 let (src_imageref, proxy_cfg) = if !state.source.in_host_mountns {
1039 (state.source.imageref.clone(), None)
1040 } else {
1041 let src_imageref = {
1042 let digest = state
1044 .source
1045 .digest
1046 .as_ref()
1047 .ok_or_else(|| anyhow::anyhow!("Missing container image digest"))?;
1048 let spec = crate::utils::digested_pullspec(&state.source.imageref.name, digest);
1049 ostree_container::ImageReference {
1050 transport: ostree_container::Transport::ContainerStorage,
1051 name: spec,
1052 }
1053 };
1054
1055 let proxy_cfg = crate::deploy::new_proxy_config();
1056 (src_imageref, Some(proxy_cfg))
1057 };
1058 let src_imageref = ostree_container::OstreeImageReference {
1059 sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
1062 imgref: src_imageref,
1063 };
1064
1065 let spec_imgref = ImageReference::from(src_imageref.clone());
1068 let repo = &sysroot.repo();
1069 repo.set_disable_fsync(true);
1070
1071 let use_unified = state.target_opts.unified_storage_exp;
1075
1076 let prepared = if use_unified {
1077 tracing::info!("Using unified storage path for installation");
1078 crate::deploy::prepare_for_pull_unified(
1079 repo,
1080 &spec_imgref,
1081 Some(&state.target_imgref),
1082 storage,
1083 None,
1084 )
1085 .await?
1086 } else {
1087 prepare_for_pull(repo, &spec_imgref, Some(&state.target_imgref), None).await?
1088 };
1089
1090 let pulled_image = match prepared {
1091 PreparedPullResult::AlreadyPresent(existing) => existing,
1092 PreparedPullResult::Ready(image_meta) => {
1093 crate::deploy::check_disk_space_ostree(repo, &image_meta, &spec_imgref)?;
1094 pull_from_prepared(&spec_imgref, false, ProgressWriter::default(), *image_meta).await?
1095 }
1096 };
1097
1098 repo.set_disable_fsync(false);
1099
1100 let merged_ostree_root = sysroot
1103 .repo()
1104 .read_commit(pulled_image.ostree_commit.as_str(), gio::Cancellable::NONE)?
1105 .0;
1106 let kargsd = crate::bootc_kargs::get_kargs_from_ostree_root(
1107 &sysroot.repo(),
1108 merged_ostree_root.downcast_ref().unwrap(),
1109 std::env::consts::ARCH,
1110 )?;
1111
1112 if ostree_ext::bootabletree::commit_has_aboot_img(&merged_ostree_root, None)? {
1115 tracing::debug!("Setting bootloader to aboot");
1116 Command::new("ostree")
1117 .args([
1118 "config",
1119 "--repo",
1120 "ostree/repo",
1121 "set",
1122 "sysroot.bootloader",
1123 "aboot",
1124 ])
1125 .cwd_dir(root_setup.physical_root.try_clone()?)
1126 .run_capture_stderr()
1127 .context("Setting bootloader config to aboot")?;
1128 sysroot.repo().reload_config(None::<&gio::Cancellable>)?;
1129 }
1130
1131 let install_config_kargs = state.install_config.as_ref().and_then(|c| c.kargs.as_ref());
1133 let install_config_karg_deletes = state
1134 .install_config
1135 .as_ref()
1136 .and_then(|c| c.karg_deletes.as_ref());
1137
1138 let mut kargs = Cmdline::new();
1144 let mut karg_deletes = Vec::<&str>::new();
1145
1146 kargs.extend(&root_setup.kargs);
1147
1148 if let Some(install_config_kargs) = install_config_kargs {
1149 for karg in install_config_kargs {
1150 kargs.extend(&Cmdline::from(karg.as_str()));
1151 }
1152 }
1153
1154 kargs.extend(&kargsd);
1155
1156 if let Some(install_config_karg_deletes) = install_config_karg_deletes {
1158 for karg_delete in install_config_karg_deletes {
1159 karg_deletes.push(karg_delete);
1160 }
1161 }
1162 if let Some(deletes) = state.config_opts.karg_delete.as_ref() {
1163 for karg_delete in deletes {
1164 karg_deletes.push(karg_delete);
1165 }
1166 }
1167 delete_kargs(&mut kargs, &karg_deletes);
1168
1169 if let Some(cli_kargs) = state.config_opts.karg.as_ref() {
1170 for karg in cli_kargs {
1171 kargs.extend(karg);
1172 }
1173 }
1174
1175 let kargs_strs: Vec<&str> = kargs.iter_str().collect();
1177
1178 let mut options = ostree_container::deploy::DeployOpts::default();
1179 options.kargs = Some(kargs_strs.as_slice());
1180 options.target_imgref = Some(&state.target_imgref);
1181 options.proxy_cfg = proxy_cfg;
1182 options.skip_completion = true; options.no_clean = has_ostree;
1184 let imgstate = crate::utils::async_task_with_spinner(
1185 "Deploying container image",
1186 ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)),
1187 )
1188 .await?;
1189
1190 let deployment = sysroot
1191 .deployments()
1192 .into_iter()
1193 .next()
1194 .ok_or_else(|| anyhow::anyhow!("Failed to find deployment"))?;
1195 let path = sysroot.deployment_dirpath(&deployment);
1197 let root = root_setup
1198 .physical_root
1199 .open_dir(path.as_str())
1200 .context("Opening deployment dir")?;
1201
1202 if let Some(policy) = sepolicy {
1206 let deployment_root_meta = root.dir_metadata()?;
1207 let deployment_root_devino = (deployment_root_meta.dev(), deployment_root_meta.ino());
1208 for d in ["ostree", "boot"] {
1209 let mut pathbuf = Utf8PathBuf::from(d);
1210 crate::lsm::ensure_dir_labeled_recurse(
1211 &root_setup.physical_root,
1212 &mut pathbuf,
1213 policy,
1214 Some(deployment_root_devino),
1215 )
1216 .with_context(|| format!("Recursive SELinux relabeling of {d}"))?;
1217 }
1218
1219 if let Some(cfs_super) = root.open_optional(OSTREE_COMPOSEFS_SUPER)? {
1220 let label = crate::lsm::require_label(policy, "/usr".into(), 0o644)?;
1221 crate::lsm::set_security_selinux(cfs_super.as_fd(), label.as_bytes())?;
1222 } else {
1223 tracing::warn!("Missing {OSTREE_COMPOSEFS_SUPER}; composefs is not enabled?");
1224 }
1225 }
1226
1227 if let Some(boot) = root_setup.boot.as_ref() {
1231 if !boot.source.is_empty() {
1232 crate::lsm::atomic_replace_labeled(&root, "etc/fstab", 0o644.into(), sepolicy, |w| {
1233 writeln!(w, "{}", boot.to_fstab()).map_err(Into::into)
1234 })?;
1235 }
1236 }
1237
1238 if let Some(contents) = state.root_ssh_authorized_keys.as_deref() {
1239 osconfig::inject_root_ssh_authorized_keys(&root, sepolicy, contents)?;
1240 }
1241
1242 let aleph = InstallAleph::new(
1243 &src_imageref,
1244 &state.target_imgref,
1245 &imgstate,
1246 &state.selinux_state,
1247 )?;
1248 Ok((deployment, aleph))
1249}
1250
1251pub(crate) fn delete_kargs(existing: &mut Cmdline, deletes: &Vec<&str>) {
1252 for delete in deletes {
1253 if let Some(param) = utf8::Parameter::parse(&delete) {
1254 if param.value().is_some() {
1255 existing.remove_exact(¶m);
1256 } else {
1257 existing.remove(¶m.key());
1258 }
1259 }
1260 }
1261}
1262
1263pub(crate) fn run_in_host_mountns(cmd: &str) -> Result<Command> {
1265 let mut c = Command::new(bootc_utils::reexec::executable_path()?);
1266 c.lifecycle_bind()
1267 .args(["exec-in-host-mount-namespace", cmd]);
1268 Ok(c)
1269}
1270
1271#[context("Re-exec in host mountns")]
1272pub(crate) fn exec_in_host_mountns(args: &[std::ffi::OsString]) -> Result<()> {
1273 let (cmd, args) = args
1274 .split_first()
1275 .ok_or_else(|| anyhow::anyhow!("Missing command"))?;
1276 tracing::trace!("{cmd:?} {args:?}");
1277 let pid1mountns = std::fs::File::open("/proc/1/ns/mnt").context("open pid1 mountns")?;
1278 rustix::thread::move_into_link_name_space(
1279 pid1mountns.as_fd(),
1280 Some(rustix::thread::LinkNameSpaceType::Mount),
1281 )
1282 .context("setns")?;
1283 rustix::process::chdir("/").context("chdir")?;
1284 if !Utf8Path::new("/usr").try_exists().context("/usr")?
1287 && Utf8Path::new("/root/usr")
1288 .try_exists()
1289 .context("/root/usr")?
1290 {
1291 tracing::debug!("Using supermin workaround");
1292 rustix::process::chroot("/root").context("chroot")?;
1293 }
1294 Err(Command::new(cmd).args(args).arg0(bootc_utils::NAME).exec()).context("exec")?
1295}
1296
1297pub(crate) struct RootSetup {
1298 #[cfg(feature = "install-to-disk")]
1299 luks_device: Option<String>,
1300 pub(crate) device_info: bootc_blockdev::Device,
1301 pub(crate) physical_root_path: Utf8PathBuf,
1304 pub(crate) physical_root: Dir,
1306 pub(crate) target_root_path: Option<Utf8PathBuf>,
1308 pub(crate) rootfs_uuid: Option<String>,
1309 skip_finalize: bool,
1311 boot: Option<MountSpec>,
1312 pub(crate) kargs: CmdlineOwned,
1313}
1314
1315fn require_boot_uuid(spec: &MountSpec) -> Result<&str> {
1316 spec.get_source_uuid()
1317 .ok_or_else(|| anyhow!("/boot is not specified via UUID= (this is currently required)"))
1318}
1319
1320impl RootSetup {
1321 pub(crate) fn get_boot_uuid(&self) -> Result<Option<&str>> {
1324 self.boot.as_ref().map(require_boot_uuid).transpose()
1325 }
1326
1327 pub(crate) fn boot_mount_spec(&self) -> Option<&MountSpec> {
1329 self.boot.as_ref()
1330 }
1331
1332 #[cfg(feature = "install-to-disk")]
1334 fn into_storage(self) -> (Utf8PathBuf, Option<String>) {
1335 (self.physical_root_path, self.luks_device)
1336 }
1337}
1338
1339#[derive(Debug)]
1340#[allow(dead_code)]
1341pub(crate) enum SELinuxFinalState {
1342 ForceTargetDisabled,
1344 Enabled(Option<crate::lsm::SetEnforceGuard>),
1346 HostDisabled,
1348 Disabled,
1350}
1351
1352impl SELinuxFinalState {
1353 pub(crate) fn enabled(&self) -> bool {
1355 match self {
1356 SELinuxFinalState::ForceTargetDisabled | SELinuxFinalState::Disabled => false,
1357 SELinuxFinalState::Enabled(_) | SELinuxFinalState::HostDisabled => true,
1358 }
1359 }
1360
1361 pub(crate) fn to_aleph(&self) -> &'static str {
1364 match self {
1365 SELinuxFinalState::ForceTargetDisabled => "force-target-disabled",
1366 SELinuxFinalState::Enabled(_) => "enabled",
1367 SELinuxFinalState::HostDisabled => "host-disabled",
1368 SELinuxFinalState::Disabled => "disabled",
1369 }
1370 }
1371}
1372
1373pub(crate) fn reexecute_self_for_selinux_if_needed(
1378 srcdata: &SourceInfo,
1379 override_disable_selinux: bool,
1380) -> Result<SELinuxFinalState> {
1381 if srcdata.selinux {
1383 let host_selinux = crate::lsm::selinux_enabled()?;
1384 tracing::debug!("Target has SELinux, host={host_selinux}");
1385 let r = if override_disable_selinux {
1386 println!("notice: Target has SELinux enabled, overriding to disable");
1387 SELinuxFinalState::ForceTargetDisabled
1388 } else if host_selinux {
1389 setup_sys_mount("selinuxfs", SELINUXFS)?;
1395 let g = crate::lsm::selinux_ensure_install_or_setenforce()?;
1397 SELinuxFinalState::Enabled(g)
1398 } else {
1399 SELinuxFinalState::HostDisabled
1400 };
1401 Ok(r)
1402 } else {
1403 Ok(SELinuxFinalState::Disabled)
1404 }
1405}
1406
1407pub(crate) fn finalize_filesystem(
1410 fsname: &str,
1411 root: &Dir,
1412 path: impl AsRef<Utf8Path>,
1413) -> Result<()> {
1414 let path = path.as_ref();
1415 Task::new(format!("Trimming {fsname}"), "fstrim")
1417 .args(["--quiet-unsupported", "-v", path.as_str()])
1418 .cwd(root)?
1419 .run()?;
1420 Task::new(format!("Finalizing filesystem {fsname}"), "mount")
1423 .cwd(root)?
1424 .args(["-o", "remount,ro", path.as_str()])
1425 .run()?;
1426 for a in ["-f", "-u"] {
1428 Command::new("fsfreeze")
1429 .cwd_dir(root.try_clone()?)
1430 .args([a, path.as_str()])
1431 .run_capture_stderr()?;
1432 }
1433 Ok(())
1434}
1435
1436fn require_host_pidns() -> Result<()> {
1438 if rustix::process::getpid().is_init() {
1439 anyhow::bail!("This command must be run with the podman --pid=host flag")
1440 }
1441 tracing::trace!("OK: we're not pid 1");
1442 Ok(())
1443}
1444
1445fn require_host_userns() -> Result<()> {
1448 let proc1 = "/proc/1";
1449 let pid1_uid = Path::new(proc1)
1450 .metadata()
1451 .with_context(|| format!("Querying {proc1}"))?
1452 .uid();
1453 ensure!(
1456 pid1_uid == 0,
1457 "{proc1} is owned by {pid1_uid}, not zero; this command must be run in the root user namespace (e.g. not rootless podman)"
1458 );
1459 tracing::trace!("OK: we're in a matching user namespace with pid1");
1460 Ok(())
1461}
1462
1463pub(crate) fn setup_tmp_mount() -> Result<()> {
1468 let st = rustix::fs::statfs("/tmp")?;
1469 if st.f_type == libc::TMPFS_MAGIC {
1470 tracing::trace!("Already have tmpfs /tmp")
1471 } else {
1472 Command::new("mount")
1475 .args(["tmpfs", "-t", "tmpfs", "/tmp"])
1476 .run_capture_stderr()?;
1477 }
1478 Ok(())
1479}
1480
1481#[context("Ensuring sys mount {fspath} {fstype}")]
1484pub(crate) fn setup_sys_mount(fstype: &str, fspath: &str) -> Result<()> {
1485 tracing::debug!("Setting up sys mounts");
1486 let rootfs = format!("/proc/1/root/{fspath}");
1487 if !Path::new(rootfs.as_str()).try_exists()? {
1489 return Ok(());
1490 }
1491
1492 if std::fs::read_dir(rootfs)?.next().is_none() {
1494 return Ok(());
1495 }
1496
1497 if Path::new(fspath).try_exists()? && std::fs::read_dir(fspath)?.next().is_some() {
1501 return Ok(());
1502 }
1503
1504 Command::new("mount")
1506 .args(["-t", fstype, fstype, fspath])
1507 .run_capture_stderr()?;
1508
1509 Ok(())
1510}
1511
1512#[context("Verifying fetch")]
1514async fn verify_target_fetch(
1515 tmpdir: &Dir,
1516 imgref: &ostree_container::OstreeImageReference,
1517) -> Result<()> {
1518 let tmpdir = &TempDir::new_in(&tmpdir)?;
1519 let tmprepo = &ostree::Repo::create_at_dir(tmpdir.as_fd(), ".", ostree::RepoMode::Bare, None)
1520 .context("Init tmp repo")?;
1521
1522 tracing::trace!("Verifying fetch for {imgref}");
1523 let mut imp =
1524 ostree_container::store::ImageImporter::new(tmprepo, imgref, Default::default()).await?;
1525 use ostree_container::store::PrepareResult;
1526 let prep = match imp.prepare().await? {
1527 PrepareResult::AlreadyPresent(_) => unreachable!(),
1529 PrepareResult::Ready(r) => r,
1530 };
1531 tracing::debug!("Fetched manifest with digest {}", prep.manifest_digest);
1532 Ok(())
1533}
1534
1535async fn prepare_install(
1537 mut config_opts: InstallConfigOpts,
1538 source_opts: InstallSourceOpts,
1539 target_opts: InstallTargetOpts,
1540 mut composefs_options: InstallComposefsOpts,
1541 target_fs: Option<FilesystemEnum>,
1542) -> Result<Arc<State>> {
1543 tracing::trace!("Preparing install");
1544 let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
1545 .context("Opening /")?;
1546
1547 let host_is_container = crate::containerenv::is_container(&rootfs);
1548 let external_source = source_opts.source_imgref.is_some();
1549 let (source, target_rootfs) = match source_opts.source_imgref {
1550 None => {
1551 ensure!(
1552 host_is_container,
1553 "Either --source-imgref must be defined or this command must be executed inside a podman container."
1554 );
1555
1556 crate::cli::require_root(true)?;
1557
1558 require_host_pidns()?;
1559 require_host_userns()?;
1562 let container_info = crate::containerenv::get_container_execution_info(&rootfs)?;
1563 match container_info.rootless.as_deref() {
1565 Some("1") => anyhow::bail!(
1566 "Cannot install from rootless podman; this command must be run as root"
1567 ),
1568 Some(o) => tracing::debug!("rootless={o}"),
1569 None => tracing::debug!(
1571 "notice: Did not find rootless= entry in {}",
1572 crate::containerenv::PATH,
1573 ),
1574 };
1575 tracing::trace!("Read container engine info {:?}", container_info);
1576
1577 let source = SourceInfo::from_container(&rootfs, &container_info)?;
1578 (source, Some(rootfs.try_clone()?))
1579 }
1580 Some(source) => {
1581 crate::cli::require_root(false)?;
1582 let source = SourceInfo::from_imageref(&source, &rootfs)?;
1583 (source, None)
1584 }
1585 };
1586
1587 if target_opts.target_no_signature_verification {
1590 tracing::debug!(
1592 "Use of --target-no-signature-verification flag which is enabled by default"
1593 );
1594 }
1595 let target_sigverify = sigpolicy_from_opt(target_opts.enforce_container_sigpolicy);
1596 let target_imgname = target_opts
1597 .target_imgref
1598 .as_deref()
1599 .unwrap_or(source.imageref.name.as_str());
1600 let target_transport =
1601 ostree_container::Transport::try_from(target_opts.target_transport.as_str())?;
1602 let target_imgref = ostree_container::OstreeImageReference {
1603 sigverify: target_sigverify,
1604 imgref: ostree_container::ImageReference {
1605 transport: target_transport,
1606 name: target_imgname.to_string(),
1607 },
1608 };
1609 tracing::debug!("Target image reference: {target_imgref}");
1610
1611 let (composefs_required, kernel) = if let Some(root) = target_rootfs.as_ref() {
1612 let kernel = crate::kernel::find_kernel(root)?;
1613
1614 (
1615 kernel.as_ref().map(|k| k.kernel.unified).unwrap_or(false),
1616 kernel,
1617 )
1618 } else {
1619 (false, None)
1620 };
1621
1622 tracing::debug!("Composefs required: {composefs_required}");
1623
1624 if composefs_required {
1625 composefs_options.composefs_backend = true;
1626 }
1627
1628 if composefs_options.composefs_backend
1629 && matches!(config_opts.bootloader, Some(Bootloader::None))
1630 {
1631 anyhow::bail!("Bootloader set to none is not supported with the composefs backend");
1632 }
1633
1634 bootc_mount::ensure_mirrored_host_mount("/dev")?;
1636 bootc_mount::ensure_mirrored_host_mount("/var/lib/containers")?;
1639 bootc_mount::ensure_mirrored_host_mount("/var/tmp")?;
1642 bootc_mount::ensure_mirrored_host_mount("/run/udev")?;
1645 setup_tmp_mount()?;
1647 let tempdir = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
1650 osbuild::adjust_for_bootc_image_builder(&rootfs, &tempdir)?;
1652
1653 if target_opts.run_fetch_check {
1654 verify_target_fetch(&tempdir, &target_imgref).await?;
1655 }
1656
1657 if !external_source && std::env::var_os("BOOTC_SKIP_UNSHARE").is_none() {
1660 super::cli::ensure_self_unshared_mount_namespace()?;
1661 }
1662
1663 setup_sys_mount("efivarfs", EFIVARFS)?;
1664
1665 let selinux_state = reexecute_self_for_selinux_if_needed(&source, config_opts.disable_selinux)?;
1667 tracing::debug!("SELinux state: {selinux_state:?}");
1668
1669 println!("Installing image: {:#}", &target_imgref);
1670 if let Some(digest) = source.digest.as_deref() {
1671 println!("Digest: {digest}");
1672 }
1673
1674 let install_config = config::load_config()?;
1675 if let Some(ref config) = install_config {
1676 tracing::debug!("Loaded install configuration");
1677 if !config_opts.bootupd_skip_boot_uuid {
1680 config_opts.bootupd_skip_boot_uuid = config
1681 .bootupd
1682 .as_ref()
1683 .and_then(|b| b.skip_boot_uuid)
1684 .unwrap_or(false);
1685 }
1686
1687 if config_opts.bootloader.is_none() {
1688 config_opts.bootloader = config.bootloader.clone();
1689 }
1690 } else {
1691 tracing::debug!("No install configuration found");
1692 }
1693
1694 let root_filesystem = target_fs
1695 .or(install_config
1696 .as_ref()
1697 .and_then(|c| c.filesystem_root())
1698 .and_then(|r| r.fstype))
1699 .ok_or_else(|| anyhow::anyhow!("No root filesystem specified"))?;
1700
1701 let mut is_uki = false;
1702
1703 match kernel {
1711 Some(k) => match k.k_type {
1712 crate::kernel::KernelType::Uki { cmdline, .. } => {
1713 let allow_missing_fsverity = cmdline.is_some_and(|cmd| {
1714 ComposefsCmdline::find_in_cmdline(&cmd)
1715 .is_some_and(|cfs_cmdline| cfs_cmdline.allow_missing_fsverity)
1716 });
1717
1718 if !allow_missing_fsverity {
1719 anyhow::ensure!(
1720 root_filesystem.supports_fsverity(),
1721 "Specified filesystem {root_filesystem} does not support fs-verity"
1722 );
1723 }
1724
1725 composefs_options.allow_missing_verity = allow_missing_fsverity;
1726 is_uki = true;
1727 }
1728
1729 crate::kernel::KernelType::Vmlinuz { .. } => {}
1730 },
1731
1732 None => {}
1733 }
1734
1735 if composefs_options.composefs_backend && !composefs_options.allow_missing_verity && !is_uki {
1737 composefs_options.allow_missing_verity = !root_filesystem.supports_fsverity();
1738 }
1739
1740 tracing::info!(
1741 allow_missing_fsverity = composefs_options.allow_missing_verity,
1742 uki = is_uki,
1743 "ComposeFS install prep",
1744 );
1745
1746 if let Some(crate::spec::Bootloader::None) = config_opts.bootloader {
1747 if cfg!(target_arch = "s390x") {
1748 anyhow::bail!("Bootloader set to none is not supported for the s390x architecture");
1749 }
1750 }
1751
1752 let prepareroot_config = {
1754 let kf = ostree_prepareroot::require_config_from_root(&rootfs)?;
1755 let mut r = HashMap::new();
1756 for grp in kf.groups() {
1757 for key in kf.keys(&grp)? {
1758 let key = key.as_str();
1759 let value = kf.value(&grp, key)?;
1760 r.insert(format!("{grp}.{key}"), value.to_string());
1761 }
1762 }
1763 r
1764 };
1765
1766 let root_ssh_authorized_keys = config_opts
1769 .root_ssh_authorized_keys
1770 .as_ref()
1771 .map(|p| std::fs::read_to_string(p).with_context(|| format!("Reading {p}")))
1772 .transpose()?;
1773
1774 let state = Arc::new(State {
1778 selinux_state,
1779 source,
1780 config_opts,
1781 target_opts,
1782 target_imgref,
1783 install_config,
1784 prepareroot_config,
1785 root_ssh_authorized_keys,
1786 container_root: rootfs,
1787 tempdir,
1788 host_is_container,
1789 composefs_required,
1790 composefs_options,
1791 });
1792
1793 Ok(state)
1794}
1795
1796impl PostFetchState {
1797 pub(crate) fn new(state: &State, d: &Dir) -> Result<Self> {
1798 let detected_bootloader = {
1801 if let Some(bootloader) = state.config_opts.bootloader.clone() {
1802 bootloader
1803 } else {
1804 if crate::bootloader::supports_bootupd(d)? {
1805 crate::spec::Bootloader::Grub
1806 } else {
1807 crate::spec::Bootloader::Systemd
1808 }
1809 }
1810 };
1811 println!("Bootloader: {detected_bootloader}");
1812 let r = Self {
1813 detected_bootloader,
1814 };
1815 Ok(r)
1816 }
1817}
1818
1819async fn install_with_sysroot(
1824 state: &State,
1825 rootfs: &RootSetup,
1826 storage: &Storage,
1827 boot_uuid: &str,
1828 bound_images: BoundImages,
1829 has_ostree: bool,
1830) -> Result<()> {
1831 let ostree = storage.get_ostree()?;
1832 let c_storage = storage.get_ensure_imgstore()?;
1833
1834 let (deployment, aleph) = install_container(state, rootfs, ostree, storage, has_ostree).await?;
1837 aleph.write_to(&rootfs.physical_root)?;
1839
1840 let deployment_path = ostree.deployment_dirpath(&deployment);
1841
1842 let deployment_dir = rootfs
1843 .physical_root
1844 .open_dir(&deployment_path)
1845 .context("Opening deployment dir")?;
1846 let postfetch = PostFetchState::new(state, &deployment_dir)?;
1847
1848 if cfg!(target_arch = "s390x") {
1849 crate::bootloader::install_via_zipl(&rootfs.device_info.require_single_root()?, boot_uuid)?;
1852 } else {
1853 match postfetch.detected_bootloader {
1854 Bootloader::Grub => {
1855 crate::bootloader::install_via_bootupd(
1856 &rootfs.device_info,
1857 &rootfs
1858 .target_root_path
1859 .clone()
1860 .unwrap_or(rootfs.physical_root_path.clone()),
1861 &state.config_opts,
1862 Some(&deployment_path.as_str()),
1863 )?;
1864 }
1865 Bootloader::Systemd => {
1866 anyhow::bail!("bootupd is required for ostree-based installs");
1867 }
1868 Bootloader::None => {
1869 tracing::debug!("Skip bootloader installation due set to None");
1870 }
1871 }
1872 }
1873 tracing::debug!("Installed bootloader");
1874
1875 tracing::debug!("Performing post-deployment operations");
1876
1877 match bound_images {
1878 BoundImages::Skip => {}
1879 BoundImages::Resolved(resolved_bound_images) => {
1880 for image in resolved_bound_images {
1882 let image = image.image.as_str();
1883 c_storage.pull_from_host_storage(image).await?;
1884 }
1885 }
1886 BoundImages::Unresolved(bound_images) => {
1887 crate::boundimage::pull_images_impl(c_storage, bound_images)
1888 .await
1889 .context("pulling bound images")?;
1890 }
1891 }
1892
1893 Ok(())
1894}
1895
1896enum BoundImages {
1897 Skip,
1898 Resolved(Vec<ResolvedBoundImage>),
1899 Unresolved(Vec<BoundImage>),
1900}
1901
1902impl BoundImages {
1903 async fn from_state(state: &State) -> Result<Self> {
1904 let bound_images = match state.config_opts.bound_images {
1905 BoundImagesOpt::Skip => BoundImages::Skip,
1906 others => {
1907 let queried_images = crate::boundimage::query_bound_images(&state.container_root)?;
1908 match others {
1909 BoundImagesOpt::Stored => {
1910 let mut r = Vec::with_capacity(queried_images.len());
1912 for image in queried_images {
1913 let resolved = ResolvedBoundImage::from_image(&image).await?;
1914 tracing::debug!("Resolved {}: {}", resolved.image, resolved.digest);
1915 r.push(resolved)
1916 }
1917 BoundImages::Resolved(r)
1918 }
1919 BoundImagesOpt::Pull => {
1920 BoundImages::Unresolved(queried_images)
1922 }
1923 BoundImagesOpt::Skip => anyhow::bail!("unreachable error"),
1924 }
1925 }
1926 };
1927
1928 Ok(bound_images)
1929 }
1930}
1931
1932async fn ostree_install(state: &State, rootfs: &RootSetup, cleanup: Cleanup) -> Result<()> {
1933 let boot_uuid = rootfs
1935 .get_boot_uuid()?
1936 .or(rootfs.rootfs_uuid.as_deref())
1937 .ok_or_else(|| anyhow!("No uuid for boot/root"))?;
1938 tracing::debug!("boot uuid={boot_uuid}");
1939
1940 let bound_images = BoundImages::from_state(state).await?;
1941
1942 {
1945 let (sysroot, has_ostree) = initialize_ostree_root(state, rootfs).await?;
1946
1947 install_with_sysroot(
1948 state,
1949 rootfs,
1950 &sysroot,
1951 &boot_uuid,
1952 bound_images,
1953 has_ostree,
1954 )
1955 .await?;
1956 let ostree = sysroot.get_ostree()?;
1957
1958 if matches!(cleanup, Cleanup::TriggerOnNextBoot) {
1959 let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
1960 tracing::debug!("Writing {DESTRUCTIVE_CLEANUP}");
1961 sysroot_dir.atomic_write(DESTRUCTIVE_CLEANUP, b"")?;
1962 }
1963
1964 sysroot.ensure_imgstore_labeled()?;
1967
1968 };
1971
1972 install_finalize(&rootfs.physical_root_path).await?;
1974
1975 Ok(())
1976}
1977
1978async fn install_to_filesystem_impl(
1979 state: &State,
1980 rootfs: &mut RootSetup,
1981 cleanup: Cleanup,
1982) -> Result<()> {
1983 if matches!(state.selinux_state, SELinuxFinalState::ForceTargetDisabled) {
1984 rootfs.kargs.extend(&Cmdline::from("selinux=0"));
1985 }
1986 let rootfs = &*rootfs;
1988
1989 match rootfs.device_info.pttype.as_deref() {
1990 Some("dos") => crate::utils::medium_visibility_warning(
1991 "Installing to `dos` format partitions is not recommended",
1992 ),
1993 Some("gpt") => {
1994 }
1996 Some(o) => {
1997 crate::utils::medium_visibility_warning(&format!("Unknown partition table type {o}"))
1998 }
1999 None => {
2000 }
2002 }
2003
2004 if state.composefs_options.composefs_backend {
2005 {
2007 let imgref = &state.source.imageref;
2008 let imgref_repr = get_imgref(&imgref.transport.to_string(), &imgref.name);
2009 let img_manifest_config = get_container_manifest_and_config(&imgref_repr).await?;
2010 crate::store::ensure_composefs_dir(&rootfs.physical_root)?;
2011 let cfs_repo = open_composefs_repo(&rootfs.physical_root)?;
2012 crate::deploy::check_disk_space_composefs(
2013 &cfs_repo,
2014 &img_manifest_config.manifest,
2015 &crate::spec::ImageReference {
2016 image: imgref.name.clone(),
2017 transport: imgref.transport.to_string(),
2018 signature: None,
2019 },
2020 )?;
2021 }
2022 let pull_result = initialize_composefs_repository(
2023 state,
2024 rootfs,
2025 state.composefs_options.allow_missing_verity,
2026 )
2027 .await?;
2028 tracing::info!(
2029 "id: {}, verity: {}",
2030 pull_result.config_digest,
2031 pull_result.config_verity.to_hex()
2032 );
2033
2034 setup_composefs_boot(
2035 rootfs,
2036 state,
2037 &pull_result.config_digest,
2038 state.composefs_options.allow_missing_verity,
2039 )
2040 .await?;
2041
2042 if let Some(policy) = state.load_policy()? {
2045 tracing::info!("Labeling composefs objects as /usr");
2046 crate::lsm::relabel_recurse(
2047 &rootfs.physical_root,
2048 "composefs",
2049 Some("/usr".into()),
2050 &policy,
2051 )
2052 .context("SELinux labeling of composefs objects")?;
2053 }
2054 } else {
2055 ostree_install(state, rootfs, cleanup).await?;
2056 }
2057
2058 if let Some(policy) = state.load_policy()? {
2062 tracing::info!("Performing final SELinux relabeling of physical root");
2063 let mut path = Utf8PathBuf::from("");
2064 crate::lsm::ensure_dir_labeled_recurse(&rootfs.physical_root, &mut path, &policy, None)
2065 .context("Final SELinux relabeling of physical root")?;
2066 } else {
2067 tracing::debug!("Skipping final SELinux relabel (SELinux is disabled)");
2068 }
2069
2070 if !rootfs.skip_finalize {
2072 let bootfs = rootfs.boot.as_ref().map(|_| ("boot", "boot"));
2073 for (fsname, fs) in std::iter::once(("root", ".")).chain(bootfs) {
2074 finalize_filesystem(fsname, &rootfs.physical_root, fs)?;
2075 }
2076 }
2077
2078 Ok(())
2079}
2080
2081fn installation_complete() {
2082 println!("Installation complete!");
2083}
2084
2085#[context("Installing to disk")]
2087#[cfg(feature = "install-to-disk")]
2088pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
2089 const INSTALL_DISK_JOURNAL_ID: &str = "8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2";
2091 let source_image = opts
2092 .source_opts
2093 .source_imgref
2094 .as_ref()
2095 .map(|s| s.as_str())
2096 .unwrap_or("none");
2097 let target_device = opts.block_opts.device.as_str();
2098
2099 tracing::info!(
2100 message_id = INSTALL_DISK_JOURNAL_ID,
2101 bootc.source_image = source_image,
2102 bootc.target_device = target_device,
2103 bootc.via_loopback = if opts.via_loopback { "true" } else { "false" },
2104 "Starting disk installation from {} to {}",
2105 source_image,
2106 target_device
2107 );
2108
2109 let mut block_opts = opts.block_opts;
2110 let target_blockdev_meta = block_opts
2111 .device
2112 .metadata()
2113 .with_context(|| format!("Querying {}", &block_opts.device))?;
2114 if opts.via_loopback {
2115 if !opts.config_opts.generic_image {
2116 crate::utils::medium_visibility_warning(
2117 "Automatically enabling --generic-image when installing via loopback",
2118 );
2119 opts.config_opts.generic_image = true;
2120 }
2121 if !target_blockdev_meta.file_type().is_file() {
2122 anyhow::bail!(
2123 "Not a regular file (to be used via loopback): {}",
2124 block_opts.device
2125 );
2126 }
2127 } else if !target_blockdev_meta.file_type().is_block_device() {
2128 anyhow::bail!("Not a block device: {}", block_opts.device);
2129 }
2130
2131 let state = prepare_install(
2132 opts.config_opts,
2133 opts.source_opts,
2134 opts.target_opts,
2135 opts.composefs_opts,
2136 block_opts.filesystem,
2137 )
2138 .await?;
2139
2140 let (mut rootfs, loopback) = {
2142 let loopback_dev = if opts.via_loopback {
2143 let loopback_dev =
2144 bootc_blockdev::LoopbackDevice::new(block_opts.device.as_std_path())?;
2145 block_opts.device = loopback_dev.path().into();
2146 Some(loopback_dev)
2147 } else {
2148 None
2149 };
2150
2151 let state = state.clone();
2152 let rootfs = tokio::task::spawn_blocking(move || {
2153 baseline::install_create_rootfs(&state, block_opts)
2154 })
2155 .await??;
2156 (rootfs, loopback_dev)
2157 };
2158
2159 install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip).await?;
2160
2161 let (root_path, luksdev) = rootfs.into_storage();
2163 Task::new_and_run(
2164 "Unmounting filesystems",
2165 "umount",
2166 ["-R", root_path.as_str()],
2167 )?;
2168 if let Some(luksdev) = luksdev.as_deref() {
2169 Task::new_and_run("Closing root LUKS device", "cryptsetup", ["close", luksdev])?;
2170 }
2171
2172 if let Some(loopback_dev) = loopback {
2173 loopback_dev.close()?;
2174 }
2175
2176 if let Some(state) = Arc::into_inner(state) {
2178 state.consume()?;
2179 } else {
2180 tracing::warn!("Failed to consume state Arc");
2182 }
2183
2184 installation_complete();
2185
2186 Ok(())
2187}
2188
2189#[context("Requiring directory contains only mount points")]
2200fn require_dir_contains_only_mounts(parent_fd: &Dir, dir_name: &str) -> Result<()> {
2201 tracing::trace!("Checking directory {dir_name} for non-mount entries");
2202 let Some(dir_fd) = parent_fd.open_dir_noxdev(dir_name)? else {
2203 tracing::trace!("{dir_name} is a mount point");
2205 return Ok(());
2206 };
2207
2208 if dir_fd.entries()?.next().is_none() {
2209 anyhow::bail!("Found empty directory: {dir_name}");
2210 }
2211
2212 for entry in dir_fd.entries()? {
2213 tracing::trace!("Checking entry in {dir_name}");
2214 let entry = DirEntryUtf8::from_cap_std(entry?);
2215 let entry_name = entry.file_name()?;
2216
2217 if entry_name == LOST_AND_FOUND {
2218 continue;
2219 }
2220
2221 let etype = entry.file_type()?;
2222 if etype == FileType::dir() {
2223 require_dir_contains_only_mounts(&dir_fd, &entry_name)?;
2224 } else {
2225 anyhow::bail!("Found entry in {dir_name}: {entry_name}");
2226 }
2227 }
2228
2229 Ok(())
2230}
2231
2232#[context("Verifying empty rootfs")]
2233fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
2234 for e in rootfs_fd.entries()? {
2235 let e = DirEntryUtf8::from_cap_std(e?);
2236 let name = e.file_name()?;
2237 if name == LOST_AND_FOUND {
2238 continue;
2239 }
2240
2241 let etype = e.file_type()?;
2243 if etype == FileType::dir() {
2244 require_dir_contains_only_mounts(rootfs_fd, &name)?;
2245 } else {
2246 anyhow::bail!("Non-empty root filesystem; found {name:?}");
2247 }
2248 }
2249 Ok(())
2250}
2251
2252fn remove_all_in_dir_no_xdev(d: &Dir, mount_err: bool) -> Result<()> {
2256 for entry in d.entries()? {
2257 let entry = entry?;
2258 let name = entry.file_name();
2259 let etype = entry.file_type()?;
2260 if etype == FileType::dir() {
2261 if let Some(subdir) = d.open_dir_noxdev(&name)? {
2262 remove_all_in_dir_no_xdev(&subdir, mount_err)?;
2263 d.remove_dir(&name)?;
2264 } else if mount_err {
2265 anyhow::bail!("Found unexpected mount point {name:?}");
2266 }
2267 } else {
2268 d.remove_file_optional(&name)?;
2269 }
2270 }
2271 anyhow::Ok(())
2272}
2273
2274#[context("Removing boot directory content except loader dir on ostree")]
2275fn remove_all_except_loader_dirs(bootdir: &Dir, is_ostree: bool) -> Result<()> {
2276 let entries = bootdir
2277 .entries()
2278 .context("Reading boot directory entries")?;
2279
2280 for entry in entries {
2281 let entry = entry.context("Reading directory entry")?;
2282 let file_name = entry.file_name();
2283 let file_name = if let Some(n) = file_name.to_str() {
2284 n
2285 } else {
2286 anyhow::bail!("Invalid non-UTF8 filename: {file_name:?} in /boot");
2287 };
2288
2289 if is_ostree && file_name.starts_with("loader") {
2293 continue;
2294 }
2295
2296 let etype = entry.file_type()?;
2297 if etype == FileType::dir() {
2298 if let Some(subdir) = bootdir.open_dir_noxdev(&file_name)? {
2300 remove_all_in_dir_no_xdev(&subdir, false)
2301 .with_context(|| format!("Removing directory contents: {}", file_name))?;
2302 bootdir.remove_dir(&file_name)?;
2303 }
2304 } else {
2305 bootdir
2306 .remove_file_optional(&file_name)
2307 .with_context(|| format!("Removing file: {}", file_name))?;
2308 }
2309 }
2310 Ok(())
2311}
2312
2313#[context("Removing boot directory content")]
2314fn clean_boot_directories(rootfs: &Dir, rootfs_path: &Utf8Path, is_ostree: bool) -> Result<()> {
2315 let bootdir =
2316 crate::utils::open_dir_remount_rw(rootfs, BOOT.into()).context("Opening /boot")?;
2317
2318 if ARCH_USES_EFI {
2319 crate::bootloader::mount_esp_part(&rootfs, &rootfs_path, is_ostree)?;
2322 }
2323
2324 remove_all_except_loader_dirs(&bootdir, is_ostree).context("Emptying /boot")?;
2326
2327 if ARCH_USES_EFI {
2329 if let Some(efidir) = bootdir
2330 .open_dir_optional(crate::bootloader::EFI_DIR)
2331 .context("Opening /boot/efi")?
2332 {
2333 remove_all_in_dir_no_xdev(&efidir, false).context("Emptying EFI system partition")?;
2334 }
2335 }
2336
2337 Ok(())
2338}
2339
2340struct RootMountInfo {
2341 mount_spec: String,
2342 kargs: Vec<String>,
2343}
2344
2345fn find_root_args_to_inherit(
2348 cmdline: &bytes::Cmdline,
2349 root_info: &Filesystem,
2350) -> Result<RootMountInfo> {
2351 let root = cmdline
2353 .find_utf8("root")?
2354 .and_then(|p| p.value().map(|p| p.to_string()));
2355 let (mount_spec, kargs) = if let Some(root) = root {
2356 let rootflags = cmdline.find(ROOTFLAGS);
2357 let inherit_kargs = cmdline.find_all_starting_with(INITRD_ARG_PREFIX);
2358 (
2359 root,
2360 rootflags
2361 .into_iter()
2362 .chain(inherit_kargs)
2363 .map(|p| utf8::Parameter::try_from(p).map(|p| p.to_string()))
2364 .collect::<Result<Vec<_>, _>>()?,
2365 )
2366 } else {
2367 let uuid = root_info
2368 .uuid
2369 .as_deref()
2370 .ok_or_else(|| anyhow!("No filesystem uuid found in target root"))?;
2371 (format!("UUID={uuid}"), Vec::new())
2372 };
2373
2374 Ok(RootMountInfo { mount_spec, kargs })
2375}
2376
2377fn warn_on_host_root(rootfs_fd: &Dir) -> Result<()> {
2378 const DELAY_SECONDS: u64 = 20;
2380
2381 let host_root_dfd = &Dir::open_ambient_dir("/proc/1/root", cap_std::ambient_authority())?;
2382 let host_root_devstat = rustix::fs::fstatvfs(host_root_dfd)?;
2383 let target_devstat = rustix::fs::fstatvfs(rootfs_fd)?;
2384 if host_root_devstat.f_fsid != target_devstat.f_fsid {
2385 tracing::debug!("Not the host root");
2386 return Ok(());
2387 }
2388 let dashes = "----------------------------";
2389 let timeout = Duration::from_secs(DELAY_SECONDS);
2390 eprintln!("{dashes}");
2391 crate::utils::medium_visibility_warning(
2392 "WARNING: This operation will OVERWRITE THE BOOTED HOST ROOT FILESYSTEM and is NOT REVERSIBLE.",
2393 );
2394 eprintln!("Waiting {timeout:?} to continue; interrupt (Control-C) to cancel.");
2395 eprintln!("{dashes}");
2396
2397 let bar = indicatif::ProgressBar::new_spinner();
2398 bar.enable_steady_tick(Duration::from_millis(100));
2399 std::thread::sleep(timeout);
2400 bar.finish();
2401
2402 Ok(())
2403}
2404
2405pub enum Cleanup {
2406 Skip,
2407 TriggerOnNextBoot,
2408}
2409
2410#[context("Installing to filesystem")]
2412pub(crate) async fn install_to_filesystem(
2413 opts: InstallToFilesystemOpts,
2414 targeting_host_root: bool,
2415 cleanup: Cleanup,
2416) -> Result<()> {
2417 const INSTALL_FILESYSTEM_JOURNAL_ID: &str = "9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3";
2419 let source_image = opts
2420 .source_opts
2421 .source_imgref
2422 .as_ref()
2423 .map(|s| s.as_str())
2424 .unwrap_or("none");
2425 let target_path = opts.filesystem_opts.root_path.as_str();
2426
2427 tracing::info!(
2428 message_id = INSTALL_FILESYSTEM_JOURNAL_ID,
2429 bootc.source_image = source_image,
2430 bootc.target_path = target_path,
2431 bootc.targeting_host_root = if targeting_host_root { "true" } else { "false" },
2432 "Starting filesystem installation from {} to {}",
2433 source_image,
2434 target_path
2435 );
2436
2437 let mut fsopts = opts.filesystem_opts;
2439
2440 if targeting_host_root
2443 && fsopts.root_path.as_str() == ALONGSIDE_ROOT_MOUNT
2444 && !fsopts.root_path.try_exists()?
2445 {
2446 tracing::debug!("Mounting host / to {ALONGSIDE_ROOT_MOUNT}");
2447 std::fs::create_dir(ALONGSIDE_ROOT_MOUNT)?;
2448 bootc_mount::bind_mount_from_pidns(
2449 bootc_mount::PID1,
2450 "/".into(),
2451 ALONGSIDE_ROOT_MOUNT.into(),
2452 true,
2453 )
2454 .context("Mounting host / to {ALONGSIDE_ROOT_MOUNT}")?;
2455 }
2456
2457 let target_root_path = fsopts.root_path.clone();
2458
2459 let target_rootfs_fd =
2461 Dir::open_ambient_dir(&target_root_path, cap_std::ambient_authority())
2462 .with_context(|| format!("Opening target root directory {target_root_path}"))?;
2463
2464 tracing::debug!("Target root filesystem: {target_root_path}");
2465
2466 if let Some(false) = target_rootfs_fd.is_mountpoint(".")? {
2467 anyhow::bail!("Not a mountpoint: {target_root_path}");
2468 }
2469
2470 {
2472 let root_path = &fsopts.root_path;
2473 let st = root_path
2474 .symlink_metadata()
2475 .with_context(|| format!("Querying target filesystem {root_path}"))?;
2476 if !st.is_dir() {
2477 anyhow::bail!("Not a directory: {root_path}");
2478 }
2479 }
2480
2481 let possible_physical_root = fsopts.root_path.join("sysroot");
2484 let possible_ostree_dir = possible_physical_root.join("ostree");
2485 let is_already_ostree = possible_ostree_dir.exists();
2486 if is_already_ostree {
2487 tracing::debug!(
2488 "ostree detected in {possible_ostree_dir}, assuming target is a deployment root and using {possible_physical_root}"
2489 );
2490 fsopts.root_path = possible_physical_root;
2491 };
2492
2493 let rootfs_fd = if is_already_ostree {
2496 let root_path = &fsopts.root_path;
2497 let rootfs_fd = Dir::open_ambient_dir(&fsopts.root_path, cap_std::ambient_authority())
2498 .with_context(|| format!("Opening target root directory {root_path}"))?;
2499
2500 tracing::debug!("Root filesystem: {root_path}");
2501
2502 if let Some(false) = rootfs_fd.is_mountpoint(".")? {
2503 anyhow::bail!("Not a mountpoint: {root_path}");
2504 }
2505 rootfs_fd
2506 } else {
2507 target_rootfs_fd.try_clone()?
2508 };
2509
2510 let inspect = bootc_mount::inspect_filesystem(&fsopts.root_path)?;
2512
2513 let state = prepare_install(
2519 opts.config_opts,
2520 opts.source_opts,
2521 opts.target_opts,
2522 opts.composefs_opts,
2523 Some(inspect.fstype.as_str().try_into()?),
2524 )
2525 .await?;
2526
2527 if !fsopts.acknowledge_destructive {
2529 warn_on_host_root(&target_rootfs_fd)?;
2530 }
2531
2532 match fsopts.replace {
2533 Some(ReplaceMode::Wipe) => {
2534 let rootfs_fd = rootfs_fd.try_clone()?;
2535 println!("Wiping contents of root");
2536 tokio::task::spawn_blocking(move || remove_all_in_dir_no_xdev(&rootfs_fd, true))
2537 .await??;
2538 }
2539 Some(ReplaceMode::Alongside) => {
2540 clean_boot_directories(&target_rootfs_fd, &target_root_path, is_already_ostree)?
2541 }
2542 None => require_empty_rootdir(&rootfs_fd)?,
2543 }
2544
2545 let config_root_mount_spec = state
2550 .install_config
2551 .as_ref()
2552 .and_then(|c| c.root_mount_spec.as_ref());
2553 let root_info = if let Some(s) = fsopts.root_mount_spec.as_ref().or(config_root_mount_spec) {
2554 RootMountInfo {
2555 mount_spec: s.to_string(),
2556 kargs: Vec::new(),
2557 }
2558 } else if targeting_host_root {
2559 let cmdline = bytes::Cmdline::from_proc()?;
2561 find_root_args_to_inherit(&cmdline, &inspect)?
2562 } else {
2563 let uuid = inspect
2566 .uuid
2567 .as_deref()
2568 .ok_or_else(|| anyhow!("No filesystem uuid found in target root"))?;
2569 let kargs = match inspect.fstype.as_str() {
2570 "btrfs" => {
2571 let subvol = crate::utils::find_mount_option(&inspect.options, "subvol");
2572 subvol
2573 .map(|vol| format!("rootflags=subvol={vol}"))
2574 .into_iter()
2575 .collect::<Vec<_>>()
2576 }
2577 _ => Vec::new(),
2578 };
2579 RootMountInfo {
2580 mount_spec: format!("UUID={uuid}"),
2581 kargs,
2582 }
2583 };
2584 tracing::debug!("Root mount: {} {:?}", root_info.mount_spec, root_info.kargs);
2585
2586 let boot_is_mount = {
2587 if let Some(boot_metadata) = target_rootfs_fd.symlink_metadata_optional(BOOT)? {
2588 let root_dev = rootfs_fd.dir_metadata()?.dev();
2589 let boot_dev = boot_metadata.dev();
2590 tracing::debug!("root_dev={root_dev} boot_dev={boot_dev}");
2591 root_dev != boot_dev
2592 } else {
2593 tracing::debug!("No /{BOOT} directory found");
2594 false
2595 }
2596 };
2597 let boot_uuid = if boot_is_mount {
2599 let boot_path = target_root_path.join(BOOT);
2600 tracing::debug!("boot_path={boot_path}");
2601 let u = bootc_mount::inspect_filesystem(&boot_path)
2602 .with_context(|| format!("Inspecting /{BOOT}"))?
2603 .uuid
2604 .ok_or_else(|| anyhow!("No UUID found for /{BOOT}"))?;
2605 Some(u)
2606 } else {
2607 None
2608 };
2609 tracing::debug!("boot UUID: {boot_uuid:?}");
2610
2611 let device_info = {
2614 let dev =
2615 bootc_blockdev::list_dev(Utf8Path::new(&inspect.source))?.require_single_root()?;
2616 tracing::debug!("Backing device: {}", dev.path());
2617 dev
2618 };
2619
2620 let rootarg = format!("root={}", root_info.mount_spec);
2621 let config_boot_mount_spec = state
2623 .install_config
2624 .as_ref()
2625 .and_then(|c| c.boot_mount_spec.as_ref());
2626 let mut boot = if let Some(spec) = fsopts.boot_mount_spec.as_ref().or(config_boot_mount_spec) {
2627 if spec.is_empty() {
2630 None
2631 } else {
2632 Some(MountSpec::new(&spec, "/boot"))
2633 }
2634 } else {
2635 read_boot_fstab_entry(&rootfs_fd)?
2638 .filter(|spec| spec.get_source_uuid().is_some())
2639 .or_else(|| {
2640 boot_uuid
2641 .as_deref()
2642 .map(|boot_uuid| MountSpec::new_uuid_src(boot_uuid, "/boot"))
2643 })
2644 };
2645 if let Some(boot) = boot.as_mut() {
2648 boot.push_option("ro");
2649 }
2650 let bootarg = boot.as_ref().map(|boot| format!("boot={}", &boot.source));
2653
2654 let mut kargs = if root_info.mount_spec.is_empty() {
2657 Vec::new()
2658 } else {
2659 [rootarg]
2660 .into_iter()
2661 .chain(root_info.kargs)
2662 .collect::<Vec<_>>()
2663 };
2664
2665 kargs.push(RW_KARG.to_string());
2666
2667 if let Some(bootarg) = bootarg {
2668 kargs.push(bootarg);
2669 }
2670
2671 let kargs = Cmdline::from(kargs.join(" "));
2672
2673 let skip_finalize =
2674 matches!(fsopts.replace, Some(ReplaceMode::Alongside)) || fsopts.skip_finalize;
2675 let mut rootfs = RootSetup {
2676 #[cfg(feature = "install-to-disk")]
2677 luks_device: None,
2678 device_info,
2679 physical_root_path: fsopts.root_path,
2680 physical_root: rootfs_fd,
2681 target_root_path: Some(target_root_path.clone()),
2682 rootfs_uuid: inspect.uuid.clone(),
2683 boot,
2684 kargs,
2685 skip_finalize,
2686 };
2687
2688 install_to_filesystem_impl(&state, &mut rootfs, cleanup).await?;
2689
2690 drop(rootfs);
2692
2693 installation_complete();
2694
2695 Ok(())
2696}
2697
2698pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) -> Result<()> {
2699 const INSTALL_EXISTING_ROOT_JOURNAL_ID: &str = "7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1";
2701 let source_image = opts
2702 .source_opts
2703 .source_imgref
2704 .as_ref()
2705 .map(|s| s.as_str())
2706 .unwrap_or("none");
2707 let target_path = opts.root_path.as_str();
2708
2709 tracing::info!(
2710 message_id = INSTALL_EXISTING_ROOT_JOURNAL_ID,
2711 bootc.source_image = source_image,
2712 bootc.target_path = target_path,
2713 bootc.cleanup = if opts.cleanup {
2714 "trigger_on_next_boot"
2715 } else {
2716 "skip"
2717 },
2718 "Starting installation to existing root from {} to {}",
2719 source_image,
2720 target_path
2721 );
2722
2723 let cleanup = match opts.cleanup {
2724 true => Cleanup::TriggerOnNextBoot,
2725 false => Cleanup::Skip,
2726 };
2727
2728 let opts = InstallToFilesystemOpts {
2729 filesystem_opts: InstallTargetFilesystemOpts {
2730 root_path: opts.root_path,
2731 root_mount_spec: None,
2732 boot_mount_spec: None,
2733 replace: opts.replace,
2734 skip_finalize: true,
2735 acknowledge_destructive: opts.acknowledge_destructive,
2736 },
2737 source_opts: opts.source_opts,
2738 target_opts: opts.target_opts,
2739 config_opts: opts.config_opts,
2740 composefs_opts: opts.composefs_opts,
2741 };
2742
2743 install_to_filesystem(opts, true, cleanup).await
2744}
2745
2746fn read_boot_fstab_entry(root: &Dir) -> Result<Option<MountSpec>> {
2748 let fstab_path = "etc/fstab";
2749 let fstab = match root.open_optional(fstab_path)? {
2750 Some(f) => f,
2751 None => return Ok(None),
2752 };
2753
2754 let reader = std::io::BufReader::new(fstab);
2755 for line in std::io::BufRead::lines(reader) {
2756 let line = line?;
2757 let line = line.trim();
2758
2759 if line.is_empty() || line.starts_with('#') {
2761 continue;
2762 }
2763
2764 let spec = MountSpec::from_str(line)?;
2766
2767 if spec.target == "/boot" {
2769 return Ok(Some(spec));
2770 }
2771 }
2772
2773 Ok(None)
2774}
2775
2776pub(crate) async fn install_reset(opts: InstallResetOpts) -> Result<()> {
2777 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2778 if !opts.experimental {
2779 anyhow::bail!("This command requires --experimental");
2780 }
2781
2782 let prog: ProgressWriter = opts.progress.try_into()?;
2783
2784 let sysroot = &crate::cli::get_storage().await?;
2785 let ostree = sysroot.get_ostree()?;
2786 let repo = &ostree.repo();
2787 let (booted_ostree, _deployments, host) = crate::status::get_status_require_booted(ostree)?;
2788
2789 let stateroots = list_stateroots(ostree)?;
2790 let target_stateroot = if let Some(s) = opts.stateroot {
2791 s
2792 } else {
2793 let now = chrono::Utc::now();
2794 let r = allocate_new_stateroot(&ostree, &stateroots, now)?;
2795 r.name
2796 };
2797
2798 let booted_stateroot = booted_ostree.stateroot();
2799 assert!(booted_stateroot.as_str() != target_stateroot);
2800 let (fetched, spec) = if let Some(target) = opts.target_opts.imageref()? {
2801 let mut new_spec = host.spec;
2802 new_spec.image = Some(target.into());
2803 let fetched = crate::deploy::pull(
2804 repo,
2805 &new_spec.image.as_ref().unwrap(),
2806 None,
2807 opts.quiet,
2808 prog.clone(),
2809 None,
2810 )
2811 .await?;
2812 (fetched, new_spec)
2813 } else {
2814 let imgstate = host
2815 .status
2816 .booted
2817 .map(|b| b.query_image(repo))
2818 .transpose()?
2819 .flatten()
2820 .ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
2821 (Box::new((*imgstate).into()), host.spec)
2822 };
2823 let spec = crate::deploy::RequiredHostSpec::from_spec(&spec)?;
2824
2825 let mut kargs = crate::bootc_kargs::get_kargs_in_root(rootfs, std::env::consts::ARCH)?;
2828
2829 if !opts.no_root_kargs {
2831 let bootcfg = booted_ostree
2832 .deployment
2833 .bootconfig()
2834 .ok_or_else(|| anyhow!("Missing bootcfg for booted deployment"))?;
2835 if let Some(options) = bootcfg.get("options") {
2836 let options_cmdline = Cmdline::from(options.as_str());
2837 let root_kargs = crate::bootc_kargs::root_args_from_cmdline(&options_cmdline);
2838 kargs.extend(&root_kargs);
2839 }
2840 }
2841
2842 if let Some(user_kargs) = opts.karg.as_ref() {
2844 for karg in user_kargs {
2845 kargs.extend(karg);
2846 }
2847 }
2848
2849 let from = MergeState::Reset {
2850 stateroot: target_stateroot.clone(),
2851 kargs,
2852 };
2853 crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone(), false).await?;
2854
2855 if let Some(boot_spec) = read_boot_fstab_entry(rootfs)? {
2857 let staged_deployment = ostree
2858 .staged_deployment()
2859 .ok_or_else(|| anyhow!("No staged deployment found"))?;
2860 let deployment_path = ostree.deployment_dirpath(&staged_deployment);
2861 let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
2862 let deployment_root = sysroot_dir.open_dir(&deployment_path)?;
2863
2864 crate::lsm::atomic_replace_labeled(
2866 &deployment_root,
2867 "etc/fstab",
2868 0o644.into(),
2869 None,
2870 |w| writeln!(w, "{}", boot_spec.to_fstab()).map_err(Into::into),
2871 )?;
2872
2873 tracing::debug!(
2874 "Copied /boot entry to new stateroot: {}",
2875 boot_spec.to_fstab()
2876 );
2877 }
2878
2879 sysroot.update_mtime()?;
2880
2881 if opts.apply {
2882 crate::reboot::reboot()?;
2883 }
2884 Ok(())
2885}
2886
2887pub(crate) async fn install_finalize(target: &Utf8Path) -> Result<()> {
2889 const INSTALL_FINALIZE_JOURNAL_ID: &str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0";
2891
2892 tracing::info!(
2893 message_id = INSTALL_FINALIZE_JOURNAL_ID,
2894 bootc.target_path = target.as_str(),
2895 "Starting installation finalization for target: {}",
2896 target
2897 );
2898
2899 crate::cli::require_root(false)?;
2900 let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(target)));
2901 sysroot.load(gio::Cancellable::NONE)?;
2902 let deployments = sysroot.deployments();
2903 if deployments.is_empty() {
2905 anyhow::bail!("Failed to find deployment in {target}");
2906 }
2907
2908 tracing::info!(
2910 message_id = INSTALL_FINALIZE_JOURNAL_ID,
2911 bootc.target_path = target.as_str(),
2912 "Successfully finalized installation for target: {}",
2913 target
2914 );
2915
2916 Ok(())
2920}
2921
2922#[cfg(test)]
2923mod tests {
2924 use super::*;
2925
2926 #[test]
2927 fn install_opts_serializable() {
2928 let c: InstallToDiskOpts = serde_json::from_value(serde_json::json!({
2929 "device": "/dev/vda"
2930 }))
2931 .unwrap();
2932 assert_eq!(c.block_opts.device, "/dev/vda");
2933 }
2934
2935 #[test]
2936 fn test_mountspec() {
2937 let mut ms = MountSpec::new("/dev/vda4", "/boot");
2938 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto defaults 0 0");
2939 ms.push_option("ro");
2940 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto ro 0 0");
2941 ms.push_option("relatime");
2942 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto ro,relatime 0 0");
2943 }
2944
2945 #[test]
2946 fn test_gather_root_args() {
2947 let inspect = Filesystem {
2949 source: "/dev/vda4".into(),
2950 target: "/".into(),
2951 fstype: "xfs".into(),
2952 maj_min: "252:4".into(),
2953 options: "rw".into(),
2954 uuid: Some("965eb3c7-5a3f-470d-aaa2-1bcf04334bc6".into()),
2955 children: None,
2956 };
2957 let kargs = bytes::Cmdline::from("");
2958 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
2959 assert_eq!(r.mount_spec, "UUID=965eb3c7-5a3f-470d-aaa2-1bcf04334bc6");
2960
2961 let kargs = bytes::Cmdline::from(
2962 "root=/dev/mapper/root rw someother=karg rd.lvm.lv=root systemd.debug=1",
2963 );
2964
2965 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
2967 assert_eq!(r.mount_spec, "/dev/mapper/root");
2968 assert_eq!(r.kargs.len(), 1);
2969 assert_eq!(r.kargs[0], "rd.lvm.lv=root");
2970
2971 let kargs = bytes::Cmdline::from(
2973 b"root=/dev/mapper/root rw non-utf8=\xff rd.lvm.lv=root systemd.debug=1",
2974 );
2975 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
2976 assert_eq!(r.mount_spec, "/dev/mapper/root");
2977 assert_eq!(r.kargs.len(), 1);
2978 assert_eq!(r.kargs[0], "rd.lvm.lv=root");
2979
2980 let kargs = bytes::Cmdline::from(
2982 b"root=/dev/mapper/ro\xffot rw non-utf8=\xff rd.lvm.lv=root systemd.debug=1",
2983 );
2984 let r = find_root_args_to_inherit(&kargs, &inspect);
2985 assert!(r.is_err());
2986
2987 let kargs = bytes::Cmdline::from(
2989 b"root=/dev/mapper/root rw non-utf8=\xff rd.lvm.lv=ro\xffot systemd.debug=1",
2990 );
2991 let r = find_root_args_to_inherit(&kargs, &inspect);
2992 assert!(r.is_err());
2993 }
2994
2995 #[test]
2998 fn test_remove_all_noxdev() -> Result<()> {
2999 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3000
3001 td.create_dir_all("foo/bar/baz")?;
3002 td.write("foo/bar/baz/test", b"sometest")?;
3003 td.symlink_contents("/absolute-nonexistent-link", "somelink")?;
3004 td.write("toptestfile", b"othertestcontents")?;
3005
3006 remove_all_in_dir_no_xdev(&td, true).unwrap();
3007
3008 assert_eq!(td.entries()?.count(), 0);
3009
3010 Ok(())
3011 }
3012
3013 #[test]
3014 fn test_read_boot_fstab_entry() -> Result<()> {
3015 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3016
3017 assert!(read_boot_fstab_entry(&td)?.is_none());
3019
3020 td.create_dir("etc")?;
3022 td.write("etc/fstab", "UUID=test-uuid / ext4 defaults 0 0\n")?;
3023 assert!(read_boot_fstab_entry(&td)?.is_none());
3024
3025 let fstab_content = "\
3027# /etc/fstab
3028UUID=root-uuid / ext4 defaults 0 0
3029UUID=boot-uuid /boot ext4 ro 0 0
3030UUID=home-uuid /home ext4 defaults 0 0
3031";
3032 td.write("etc/fstab", fstab_content)?;
3033 let boot_spec = read_boot_fstab_entry(&td)?.unwrap();
3034 assert_eq!(boot_spec.source, "UUID=boot-uuid");
3035 assert_eq!(boot_spec.target, "/boot");
3036 assert_eq!(boot_spec.fstype, "ext4");
3037 assert_eq!(boot_spec.options, Some("ro".to_string()));
3038
3039 let fstab_content = "\
3041# /etc/fstab
3042# Created by anaconda
3043UUID=root-uuid / ext4 defaults 0 0
3044# Boot partition
3045UUID=boot-uuid /boot ext4 defaults 0 0
3046";
3047 td.write("etc/fstab", fstab_content)?;
3048 let boot_spec = read_boot_fstab_entry(&td)?.unwrap();
3049 assert_eq!(boot_spec.source, "UUID=boot-uuid");
3050 assert_eq!(boot_spec.target, "/boot");
3051
3052 Ok(())
3053 }
3054
3055 #[test]
3056 fn test_require_dir_contains_only_mounts() -> Result<()> {
3057 {
3059 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3060 td.create_dir("empty")?;
3061 assert!(require_dir_contains_only_mounts(&td, "empty").is_err());
3062 }
3063
3064 {
3066 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3067 td.create_dir_all("var/lost+found")?;
3068 assert!(require_dir_contains_only_mounts(&td, "var").is_ok());
3069 }
3070
3071 {
3073 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3074 td.create_dir("var")?;
3075 td.write("var/test.txt", b"content")?;
3076 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
3077 }
3078
3079 {
3081 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3082 td.create_dir_all("var/lib/containers")?;
3083 td.write("var/lib/containers/storage.db", b"data")?;
3084 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
3085 }
3086
3087 {
3089 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3090 td.create_dir_all("boot/grub2")?;
3091 td.write("boot/grub2/grub.cfg", b"config")?;
3092 assert!(require_dir_contains_only_mounts(&td, "boot").is_err());
3093 }
3094
3095 {
3097 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3098 td.create_dir_all("var/lib/containers")?;
3099 td.create_dir_all("var/log/journal")?;
3100 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
3101 }
3102
3103 {
3105 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3106 td.create_dir_all("var/lost+found")?;
3107 td.write("var/data.txt", b"content")?;
3108 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
3109 }
3110
3111 {
3113 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3114 td.create_dir("var")?;
3115 td.symlink_contents("../usr/lib", "var/lib")?;
3116 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
3117 }
3118
3119 {
3121 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3122 td.create_dir_all("var/lib/containers/storage/overlay")?;
3123 td.write("var/lib/containers/storage/overlay/file.txt", b"data")?;
3124 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
3125 }
3126
3127 Ok(())
3128 }
3129
3130 #[test]
3131 fn test_delete_kargs() -> Result<()> {
3132 let mut cmdline = Cmdline::from("console=tty0 quiet debug nosmt foo=bar foo=baz bar=baz");
3133
3134 let deletions = vec!["foo=bar", "bar", "debug"];
3135
3136 delete_kargs(&mut cmdline, &deletions);
3137
3138 let result = cmdline.to_string();
3139 assert!(!result.contains("foo=bar"));
3140 assert!(!result.contains("bar"));
3141 assert!(!result.contains("debug"));
3142 assert!(result.contains("foo=baz"));
3143
3144 Ok(())
3145 }
3146}