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