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