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_utils::CommandRunExt;
164use camino::Utf8Path;
165use camino::Utf8PathBuf;
166use canon_json::CanonJsonSerialize;
167use cap_std::fs::{Dir, MetadataExt};
168use cap_std_ext::cap_std;
169use cap_std_ext::cap_std::fs::FileType;
170use cap_std_ext::cap_std::fs_utf8::DirEntry as DirEntryUtf8;
171use cap_std_ext::cap_tempfile::TempDir;
172use cap_std_ext::cmdext::CapStdExtCommandExt;
173use cap_std_ext::prelude::CapStdExtDirExt;
174use clap::ValueEnum;
175use fn_error_context::context;
176use linux_kernel_cmdline::utf8::{Cmdline, CmdlineOwned};
177use ostree::gio;
178use ostree_ext::ostree;
179use ostree_ext::ostree_prepareroot::{ComposefsState, Tristate};
180use ostree_ext::prelude::Cast;
181use ostree_ext::sysroot::{SysrootLock, allocate_new_stateroot, list_stateroots};
182use ostree_ext::{container as ostree_container, ostree_prepareroot};
183#[cfg(feature = "install-to-disk")]
184use rustix::fs::FileTypeExt;
185use rustix::fs::MetadataExt as _;
186use serde::{Deserialize, Serialize};
187
188#[cfg(feature = "install-to-disk")]
189use self::baseline::InstallBlockDeviceOpts;
190use crate::bootc_composefs::status::ComposefsCmdline;
191use crate::bootc_composefs::{
192 boot::setup_composefs_boot,
193 repo::{
194 finalize_fresh_repository_policy, initialize_composefs_repository,
195 pull_install_composefs_repository, tag_pulled_image, validate_repository_policy,
196 },
197 status::get_container_manifest_and_config,
198};
199use crate::bootc_kargs::{INITRD_ARG_PREFIX, ROOTFLAGS_KEY};
200use crate::boundimage::{BoundImage, ResolvedBoundImage};
201use crate::containerenv::ContainerExecutionInfo;
202use crate::deploy::{
203 MergeState, PreparedPullResult, PullProgress, prepare_for_pull, pull_from_prepared,
204 retry_pull_operation,
205};
206use crate::install::config::Filesystem as FilesystemEnum;
207use crate::lsm;
208use crate::progress_jsonl::ProgressWriter;
209use crate::spec::{Bootloader, ImageReference};
210use crate::store::Storage;
211use crate::task::Task;
212use crate::utils::sigpolicy_from_opt;
213use bootc_mount::Filesystem;
214use linux_kernel_cmdline::{bytes, utf8};
215
216pub(crate) const BOOT: &str = "boot";
218#[cfg(feature = "install-to-disk")]
220const RUN_BOOTC: &str = "/run/bootc";
221const ALONGSIDE_ROOT_MOUNT: &str = "/target";
223pub(crate) const DESTRUCTIVE_CLEANUP: &str = "etc/bootc-destructive-cleanup";
225const LOST_AND_FOUND: &str = "lost+found";
227const SELINUXFS: &str = "/sys/fs/selinux";
229pub(crate) const EFIVARFS: &str = "/sys/firmware/efi/efivars";
231pub(crate) const ARCH_USES_EFI: bool = cfg!(any(target_arch = "x86_64", target_arch = "aarch64"));
232
233pub(crate) const EFI_LOADER_INFO: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
234
235const DEFAULT_REPO_CONFIG: &[(&str, &str)] = &[
236 ("sysroot.bootloader", "none"),
238 ("sysroot.bootprefix", "true"),
241 ("sysroot.readonly", "true"),
242];
243
244pub(crate) const RW_KARG: &str = "rw";
246
247#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
248pub(crate) struct InstallTargetOpts {
249 #[clap(long, default_value = "registry")]
253 #[serde(default)]
254 pub(crate) target_transport: String,
255
256 #[clap(long)]
258 pub(crate) target_imgref: Option<String>,
259
260 #[clap(long, hide = true)]
270 #[serde(default)]
271 pub(crate) target_no_signature_verification: bool,
272
273 #[clap(long)]
278 #[serde(default)]
279 pub(crate) enforce_container_sigpolicy: bool,
280
281 #[clap(long)]
284 #[serde(default)]
285 pub(crate) run_fetch_check: bool,
286
287 #[clap(long)]
290 #[serde(default)]
291 pub(crate) skip_fetch_check: bool,
292
293 #[clap(long = "experimental-unified-storage", hide = true)]
299 #[serde(default)]
300 pub(crate) unified_storage_exp: bool,
301}
302
303#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
304pub(crate) struct InstallSourceOpts {
305 #[clap(long)]
312 pub(crate) source_imgref: Option<String>,
313}
314
315#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
316#[serde(rename_all = "kebab-case")]
317pub(crate) enum BoundImagesOpt {
318 #[default]
320 Stored,
321 #[clap(hide = true)]
322 Skip,
324 Pull,
328}
329
330impl std::fmt::Display for BoundImagesOpt {
331 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332 self.to_possible_value().unwrap().get_name().fmt(f)
333 }
334}
335
336#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
337pub(crate) struct InstallConfigOpts {
338 #[clap(long)]
343 #[serde(default)]
344 pub(crate) disable_selinux: bool,
345
346 #[clap(long)]
350 pub(crate) karg: Option<Vec<CmdlineOwned>>,
351
352 #[clap(long)]
356 pub(crate) karg_delete: Option<Vec<String>>,
357
358 #[clap(long)]
366 root_ssh_authorized_keys: Option<Utf8PathBuf>,
367
368 #[clap(long)]
374 #[serde(default)]
375 pub(crate) generic_image: bool,
376
377 #[clap(long)]
379 #[serde(default)]
380 #[arg(default_value_t)]
381 pub(crate) bound_images: BoundImagesOpt,
382
383 #[clap(long)]
385 pub(crate) stateroot: Option<String>,
386
387 #[clap(long)]
389 #[serde(default)]
390 pub(crate) bootupd_skip_boot_uuid: bool,
391
392 #[clap(long)]
394 #[serde(default)]
395 pub(crate) bootloader: Option<Bootloader>,
396}
397
398#[derive(Debug, Default, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
399pub(crate) struct UkiAddonOpts {
400 #[clap(long = "uki-addon")]
406 #[serde(default, rename = "uki_addon")]
407 pub(crate) scoped: Option<Vec<String>>,
408
409 #[clap(long = "global-uki-addon")]
415 #[serde(default, rename = "global_uki_addon")]
416 pub(crate) global: Option<Vec<String>>,
417}
418
419#[derive(Debug, Default, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
420pub(crate) struct InstallComposefsOpts {
421 #[clap(long, default_value_t)]
424 #[serde(default)]
425 pub(crate) composefs_backend: bool,
426
427 #[clap(long, default_value_t)]
435 #[serde(default)]
436 pub(crate) allow_missing_verity: bool,
437
438 #[clap(flatten)]
439 #[serde(flatten)]
440 pub(crate) uki_addon_opts: UkiAddonOpts,
441}
442
443impl InstallComposefsOpts {
444 pub(crate) fn validate(
449 &self,
450 bootloader: Option<&Bootloader>,
451 stateroot: Option<&str>,
452 config_stateroot: Option<&str>,
453 ) -> Result<()> {
454 if self.composefs_backend {
455 let default = ostree_container::deploy::STATEROOT_DEFAULT;
456 if let Some(stateroot) = stateroot.filter(|&s| s != default) {
457 anyhow::bail!(
458 "--stateroot {stateroot} is not supported with the composefs backend, which only uses {default}"
459 );
460 }
461 if let Some(stateroot) = config_stateroot.filter(|&s| s != default) {
462 anyhow::bail!(
463 "stateroot {stateroot} in the install configuration is not supported with the composefs backend, which only uses {default}"
464 );
465 }
466 } else {
467 anyhow::ensure!(
468 !matches!(bootloader, Some(Bootloader::Ukiboot)),
469 "ukiboot requires the composefs backend"
470 );
471 anyhow::ensure!(
472 !self.allow_missing_verity,
473 "--allow-missing-verity requires the composefs backend"
474 );
475 anyhow::ensure!(
476 self.uki_addon_opts.scoped.is_none(),
477 "--uki-addon requires the composefs backend"
478 );
479 anyhow::ensure!(
480 self.uki_addon_opts.global.is_none(),
481 "--global-uki-addon requires the composefs backend"
482 );
483 }
484 Ok(())
485 }
486
487 pub(crate) fn validate_bound_images(
491 &self,
492 bound_images: BoundImagesOpt,
493 root: &Dir,
494 ) -> Result<()> {
495 if !self.composefs_backend || bound_images == BoundImagesOpt::Skip {
496 return Ok(());
497 }
498 let images = crate::boundimage::query_bound_images(root)?;
499 if !images.is_empty() {
500 let images = images
501 .iter()
502 .map(|i| i.image.as_str())
503 .collect::<Vec<_>>()
504 .join(", ");
505 anyhow::bail!(
506 "Logically bound images are not supported with the composefs backend (found {images}); use --bound-images skip to install without them"
507 );
508 }
509 Ok(())
510 }
511}
512
513#[cfg(feature = "install-to-disk")]
514#[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
515pub(crate) struct InstallToDiskOpts {
516 #[clap(flatten)]
517 #[serde(flatten)]
518 pub(crate) block_opts: InstallBlockDeviceOpts,
519
520 #[clap(flatten)]
521 #[serde(flatten)]
522 pub(crate) source_opts: InstallSourceOpts,
523
524 #[clap(flatten)]
525 #[serde(flatten)]
526 pub(crate) target_opts: InstallTargetOpts,
527
528 #[clap(flatten)]
529 #[serde(flatten)]
530 pub(crate) config_opts: InstallConfigOpts,
531
532 #[clap(long)]
534 #[serde(default)]
535 pub(crate) via_loopback: bool,
536
537 #[clap(flatten)]
538 #[serde(flatten)]
539 pub(crate) composefs_opts: InstallComposefsOpts,
540
541 #[clap(long)]
544 #[serde(default)]
545 pub(crate) run_repart: bool,
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq)]
549pub(crate) enum AbootDiskLayout {
550 Android,
551 Ukiboot,
552}
553
554impl From<composefs_ctl::composefs_boot::bootloader::AbootEncoding> for AbootDiskLayout {
555 fn from(encoding: composefs_ctl::composefs_boot::bootloader::AbootEncoding) -> Self {
556 match encoding {
557 composefs_ctl::composefs_boot::bootloader::AbootEncoding::AndroidV2 => Self::Android,
558 composefs_ctl::composefs_boot::bootloader::AbootEncoding::Uki => Self::Ukiboot,
559 }
560 }
561}
562
563#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
564#[serde(rename_all = "kebab-case")]
565pub(crate) enum ReplaceMode {
566 Wipe,
569 Alongside,
577}
578
579impl std::fmt::Display for ReplaceMode {
580 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
581 self.to_possible_value().unwrap().get_name().fmt(f)
582 }
583}
584
585#[derive(Debug, Clone, clap::Args, PartialEq, Eq)]
587pub(crate) struct InstallTargetFilesystemOpts {
588 pub(crate) root_path: Utf8PathBuf,
593
594 #[clap(long)]
598 pub(crate) root_mount_spec: Option<String>,
599
600 #[clap(long)]
605 pub(crate) boot_mount_spec: Option<String>,
606
607 #[clap(long)]
610 pub(crate) replace: Option<ReplaceMode>,
611
612 #[clap(long)]
614 pub(crate) acknowledge_destructive: bool,
615
616 #[clap(long)]
620 pub(crate) skip_finalize: bool,
621}
622
623#[derive(Debug, Clone, clap::Parser, PartialEq, Eq)]
624pub(crate) struct InstallToFilesystemOpts {
625 #[clap(flatten)]
626 pub(crate) filesystem_opts: InstallTargetFilesystemOpts,
627
628 #[clap(flatten)]
629 pub(crate) source_opts: InstallSourceOpts,
630
631 #[clap(flatten)]
632 pub(crate) target_opts: InstallTargetOpts,
633
634 #[clap(flatten)]
635 pub(crate) config_opts: InstallConfigOpts,
636
637 #[clap(flatten)]
638 pub(crate) composefs_opts: InstallComposefsOpts,
639}
640
641#[derive(Debug, Clone, clap::Parser, PartialEq, Eq)]
642pub(crate) struct InstallToExistingRootOpts {
643 #[clap(long, default_value = "alongside")]
645 pub(crate) replace: Option<ReplaceMode>,
646
647 #[clap(flatten)]
648 pub(crate) source_opts: InstallSourceOpts,
649
650 #[clap(flatten)]
651 pub(crate) target_opts: InstallTargetOpts,
652
653 #[clap(flatten)]
654 pub(crate) config_opts: InstallConfigOpts,
655
656 #[clap(long)]
658 pub(crate) acknowledge_destructive: bool,
659
660 #[clap(long)]
663 pub(crate) cleanup: bool,
664
665 #[clap(default_value = ALONGSIDE_ROOT_MOUNT)]
669 pub(crate) root_path: Utf8PathBuf,
670
671 #[clap(flatten)]
672 pub(crate) composefs_opts: InstallComposefsOpts,
673}
674
675#[derive(Debug, clap::Parser, PartialEq, Eq)]
676pub(crate) struct InstallResetOpts {
677 #[clap(long)]
679 pub(crate) experimental: bool,
680
681 #[clap(flatten)]
682 pub(crate) source_opts: InstallSourceOpts,
683
684 #[clap(flatten)]
685 pub(crate) target_opts: InstallTargetOpts,
686
687 #[clap(long)]
691 pub(crate) stateroot: Option<String>,
692
693 #[clap(long)]
695 pub(crate) quiet: bool,
696
697 #[clap(flatten)]
698 pub(crate) progress: crate::cli::ProgressOptions,
699
700 #[clap(long)]
706 pub(crate) apply: bool,
707
708 #[clap(long)]
710 no_root_kargs: bool,
711
712 #[clap(long)]
716 karg: Option<Vec<CmdlineOwned>>,
717}
718
719#[derive(Debug, clap::Parser, PartialEq, Eq)]
720pub(crate) struct InstallPrintConfigurationOpts {
721 #[clap(long)]
725 pub(crate) all: bool,
726}
727
728#[derive(Debug, Clone)]
730pub(crate) struct SourceInfo {
731 pub(crate) imageref: ostree_container::ImageReference,
733 pub(crate) digest: Option<String>,
736 pub(crate) host_image_id: Option<String>,
738 pub(crate) selinux: bool,
740 pub(crate) in_host_mountns: bool,
742}
743
744#[derive(Debug)]
746pub(crate) struct State {
747 pub(crate) source: SourceInfo,
748 pub(crate) selinux_state: SELinuxFinalState,
750 #[allow(dead_code)]
751 pub(crate) config_opts: InstallConfigOpts,
752 pub(crate) target_opts: InstallTargetOpts,
753 pub(crate) target_imgref: ostree_container::OstreeImageReference,
754 #[allow(dead_code)]
755 pub(crate) ostree_prepareroot_config: HashMap<String, String>,
756 pub(crate) install_config: Option<config::InstallConfiguration>,
757 pub(crate) root_ssh_authorized_keys: Option<String>,
759 #[allow(dead_code)]
760 pub(crate) host_is_container: bool,
761 pub(crate) container_root: Dir,
763 pub(crate) tempdir: TempDir,
764
765 #[allow(dead_code)]
767 pub(crate) composefs_required: bool,
768
769 pub(crate) composefs_options: InstallComposefsOpts,
771 pub(crate) composefs_fsverity_supported: bool,
772 pub(crate) allow_missing_verity_explicit: bool,
773 #[cfg(feature = "install-to-disk")]
774 pub(crate) aboot_disk_layout: Option<AbootDiskLayout>,
775}
776
777#[derive(Debug)]
779pub(crate) struct PostFetchState {
780 pub(crate) detected_bootloader: crate::spec::Bootloader,
782}
783
784impl InstallTargetOpts {
785 pub(crate) fn imageref(&self) -> Result<Option<ostree_container::OstreeImageReference>> {
786 let Some(target_imgname) = self.target_imgref.as_deref() else {
787 return Ok(None);
788 };
789 let target_transport =
790 ostree_container::Transport::try_from(self.target_transport.as_str())?;
791 let target_imgref = ostree_container::OstreeImageReference {
792 sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
793 imgref: ostree_container::ImageReference {
794 transport: target_transport,
795 name: target_imgname.to_string(),
796 },
797 };
798 Ok(Some(target_imgref))
799 }
800}
801
802impl State {
803 #[context("Loading SELinux policy")]
804 pub(crate) fn load_policy(&self) -> Result<Option<ostree::SePolicy>> {
805 if !self.selinux_state.enabled() {
806 return Ok(None);
807 }
808 let r = lsm::new_sepolicy_at(&self.container_root)?
810 .ok_or_else(|| anyhow::anyhow!("SELinux enabled, but no policy found in root"))?;
811 tracing::debug!("Loaded SELinux policy: {}", r.csum().unwrap());
813 Ok(Some(r))
814 }
815
816 #[context("Finalizing state")]
817 #[allow(dead_code)]
818 pub(crate) fn consume(self) -> Result<()> {
819 self.tempdir.close()?;
820 if let SELinuxFinalState::Enabled(Some(guard)) = self.selinux_state {
822 guard.consume()?;
823 }
824 Ok(())
825 }
826
827 pub(crate) fn require_no_kargs_for_immutable_boot(&self) -> Result<()> {
829 if self
830 .config_opts
831 .karg
832 .as_ref()
833 .map(|v| !v.is_empty())
834 .unwrap_or_default()
835 {
836 anyhow::bail!(
837 "Cannot use externally specified kernel arguments with a prebuilt boot image"
838 );
839 }
840 Ok(())
841 }
842
843 fn stateroot(&self) -> &str {
844 self.config_opts
846 .stateroot
847 .as_deref()
848 .or_else(|| {
849 self.install_config
850 .as_ref()
851 .and_then(|c| c.stateroot.as_deref())
852 })
853 .unwrap_or(ostree_ext::container::deploy::STATEROOT_DEFAULT)
854 }
855}
856
857#[derive(Debug, Clone)]
868pub(crate) struct MountSpec {
869 pub(crate) source: String,
870 pub(crate) target: String,
871 pub(crate) fstype: String,
872 pub(crate) options: Option<String>,
873}
874
875impl MountSpec {
876 const AUTO: &'static str = "auto";
877
878 pub(crate) fn new(src: &str, target: &str) -> Self {
879 MountSpec {
880 source: src.to_string(),
881 target: target.to_string(),
882 fstype: Self::AUTO.to_string(),
883 options: None,
884 }
885 }
886
887 pub(crate) fn new_uuid_src(uuid: &str, target: &str) -> Self {
889 Self::new(&format!("UUID={uuid}"), target)
890 }
891
892 pub(crate) fn get_source_uuid(&self) -> Option<&str> {
893 if let Some((t, rest)) = self.source.split_once('=') {
894 if t.eq_ignore_ascii_case("uuid") {
895 return Some(rest);
896 }
897 }
898 None
899 }
900
901 pub(crate) fn to_fstab(&self) -> String {
902 let options = self.options.as_deref().unwrap_or("defaults");
903 format!(
904 "{} {} {} {} 0 0",
905 self.source, self.target, self.fstype, options
906 )
907 }
908
909 pub(crate) fn push_option(&mut self, opt: &str) {
911 let options = self.options.get_or_insert_with(Default::default);
912 if !options.is_empty() {
913 options.push(',');
914 }
915 options.push_str(opt);
916 }
917}
918
919impl FromStr for MountSpec {
920 type Err = anyhow::Error;
921
922 fn from_str(s: &str) -> Result<Self> {
923 let mut parts = s.split_ascii_whitespace().fuse();
924 let source = parts.next().unwrap_or_default();
925 if source.is_empty() {
926 tracing::debug!("Empty mount specification");
927 return Ok(Self {
928 source: String::new(),
929 target: String::new(),
930 fstype: Self::AUTO.into(),
931 options: None,
932 });
933 }
934 let target = parts
935 .next()
936 .ok_or_else(|| anyhow!("Missing target in mount specification {s}"))?;
937 let fstype = parts.next().unwrap_or(Self::AUTO);
938 let options = parts.next().map(ToOwned::to_owned);
939 Ok(Self {
940 source: source.to_string(),
941 fstype: fstype.to_string(),
942 target: target.to_string(),
943 options,
944 })
945 }
946}
947
948impl SourceInfo {
949 pub(crate) fn composefs_fetch_reference(&self) -> ostree_container::ImageReference {
952 composefs_fetch_reference(&self.imageref, self.host_image_id.as_deref())
953 }
954
955 #[context("Gathering source info from container env")]
958 pub(crate) fn from_container(
959 root: &Dir,
960 container_info: &ContainerExecutionInfo,
961 ) -> Result<Self> {
962 if !container_info.engine.starts_with("podman") {
963 anyhow::bail!("Currently this command only supports being executed via podman");
964 }
965 if container_info.imageid.is_empty() {
966 anyhow::bail!("Invalid empty imageid");
967 }
968 let imageref = ostree_container::ImageReference {
969 transport: ostree_container::Transport::ContainerStorage,
970 name: container_info.image.clone(),
971 };
972 tracing::debug!("Finding digest for image ID {}", container_info.imageid);
973 let digest = crate::podman::imageid_to_digest(&container_info.imageid)?;
974 Self::new(
975 imageref,
976 Some(digest),
977 Some(container_info.imageid.clone()),
978 root,
979 true,
980 )
981 }
982
983 #[context("Creating source info from a given imageref")]
984 pub(crate) fn from_imageref(imageref: &str, root: &Dir) -> Result<Self> {
985 let imageref = ostree_container::ImageReference::try_from(imageref)?;
986 Self::new(imageref, None, None, root, false)
987 }
988
989 fn have_selinux_from_repo(root: &Dir) -> Result<bool> {
990 let cancellable = ostree::gio::Cancellable::NONE;
991
992 let commit = Command::new("ostree")
993 .args(["--repo=/ostree/repo", "rev-parse", "--single"])
994 .run_get_string()?;
995 let repo = ostree::Repo::open_at_dir(root.as_fd(), "ostree/repo")?;
996 let root = repo
997 .read_commit(commit.trim(), cancellable)
998 .context("Reading commit")?
999 .0;
1000 let root = root.downcast_ref::<ostree::RepoFile>().unwrap();
1001 let xattrs = root.xattrs(cancellable)?;
1002 Ok(crate::lsm::xattrs_have_selinux(&xattrs))
1003 }
1004
1005 fn new(
1007 imageref: ostree_container::ImageReference,
1008 digest: Option<String>,
1009 host_image_id: Option<String>,
1010 root: &Dir,
1011 in_host_mountns: bool,
1012 ) -> Result<Self> {
1013 let selinux = if Path::new("/ostree/repo").try_exists()? {
1014 Self::have_selinux_from_repo(root)?
1015 } else {
1016 lsm::have_selinux_policy(root)?
1017 };
1018 Ok(Self {
1019 imageref,
1020 digest,
1021 host_image_id,
1022 selinux,
1023 in_host_mountns,
1024 })
1025 }
1026}
1027
1028fn composefs_fetch_reference(
1029 imageref: &ostree_container::ImageReference,
1030 host_image_id: Option<&str>,
1031) -> ostree_container::ImageReference {
1032 host_image_id
1033 .map(|config_id| ostree_container::ImageReference {
1034 transport: ostree_container::Transport::ContainerStorage,
1035 name: config_id.to_owned(),
1036 })
1037 .unwrap_or_else(|| imageref.clone())
1038}
1039
1040pub(crate) fn print_configuration(opts: InstallPrintConfigurationOpts) -> Result<()> {
1041 let mut install_config = config::load_config()?.unwrap_or_default();
1042 if !opts.all {
1043 install_config.filter_to_external();
1044 }
1045 let stdout = std::io::stdout().lock();
1046 anyhow::Ok(install_config.to_canon_json_writer(stdout)?)
1047}
1048
1049#[context("Creating ostree deployment")]
1050async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result<(Storage, bool)> {
1051 let sepolicy = state.load_policy()?;
1052 let sepolicy = sepolicy.as_ref();
1053 let rootfs_dir = &root_setup.physical_root;
1055 let cancellable = gio::Cancellable::NONE;
1056
1057 let stateroot = state.stateroot();
1058
1059 let has_ostree = rootfs_dir.try_exists("ostree/repo")?;
1060 if !has_ostree {
1061 Task::new("Initializing ostree layout", "ostree")
1062 .args(["admin", "init-fs", "--modern", "."])
1063 .cwd(rootfs_dir)?
1064 .run()?;
1065 } else {
1066 println!("Reusing extant ostree layout");
1067
1068 let path = ".".into();
1069 let _ = crate::utils::open_dir_remount_rw(rootfs_dir, path)
1070 .context("remounting target as read-write")?;
1071 crate::utils::remove_immutability(rootfs_dir, path)?;
1072 }
1073
1074 crate::lsm::ensure_dir_labeled(rootfs_dir, "", Some("/".into()), 0o755.into(), sepolicy)?;
1077
1078 if has_ostree && root_setup.boot.is_some() {
1081 if let Some(boot) = &root_setup.boot {
1082 let source_boot = &boot.source;
1083 let target_boot = root_setup.physical_root_path.join(BOOT);
1084 tracing::debug!("Mount {source_boot} to {target_boot} on ostree");
1085 bootc_mount::mount(source_boot, &target_boot)?;
1086 }
1087 }
1088
1089 if rootfs_dir.try_exists(BOOT)? {
1091 crate::lsm::ensure_dir_labeled(rootfs_dir, BOOT, None, 0o755.into(), sepolicy)?;
1092 }
1093
1094 let ostree_opts = state
1096 .install_config
1097 .as_ref()
1098 .and_then(|c| c.ostree.as_ref())
1099 .into_iter()
1100 .flat_map(|o| o.to_config_tuples());
1101
1102 let repo_config: Vec<_> = DEFAULT_REPO_CONFIG
1103 .iter()
1104 .copied()
1105 .chain(ostree_opts)
1106 .collect();
1107
1108 for (k, v) in repo_config.iter() {
1109 Command::new("ostree")
1110 .args(["config", "--repo", "ostree/repo", "set", k, v])
1111 .cwd_dir(rootfs_dir.try_clone()?)
1112 .run_capture_stderr()?;
1113 }
1114
1115 let sysroot = {
1116 let path = format!(
1117 "/proc/{}/fd/{}",
1118 process::id(),
1119 rootfs_dir.as_fd().as_raw_fd()
1120 );
1121 ostree::Sysroot::new(Some(&gio::File::for_path(path)))
1122 };
1123 sysroot.load(cancellable)?;
1124 let repo = &sysroot.repo();
1125
1126 let repo_verity_state = ostree_ext::fsverity::is_verity_enabled(&repo)?;
1127 let prepare_root_composefs = state
1128 .ostree_prepareroot_config
1129 .get("composefs.enabled")
1130 .map(|v| ComposefsState::from_str(&v))
1131 .transpose()?
1132 .unwrap_or(ComposefsState::default());
1133 if prepare_root_composefs.requires_fsverity() || repo_verity_state.desired == Tristate::Enabled
1134 {
1135 ostree_ext::fsverity::ensure_verity(repo).await?;
1136 }
1137
1138 if let Some(booted) = sysroot.booted_deployment() {
1139 if stateroot == booted.stateroot() {
1140 anyhow::bail!("Cannot redeploy over booted stateroot {stateroot}");
1141 }
1142 }
1143
1144 let sysroot_dir = crate::utils::sysroot_dir(&sysroot)?;
1145
1146 let stateroot_path = format!("ostree/deploy/{stateroot}");
1151 if !sysroot_dir.try_exists(stateroot_path)? {
1152 sysroot
1153 .init_osname(stateroot, cancellable)
1154 .context("initializing stateroot")?;
1155 }
1156
1157 state.tempdir.create_dir("temp-run")?;
1158 let temp_run = state.tempdir.open_dir("temp-run")?;
1159
1160 if let Some(policy) = sepolicy {
1163 let ostree_dir = rootfs_dir.open_dir("ostree")?;
1164 crate::lsm::ensure_dir_labeled(
1165 &ostree_dir,
1166 ".",
1167 Some("/usr".into()),
1168 0o755.into(),
1169 Some(policy),
1170 )?;
1171 }
1172
1173 sysroot.load(cancellable)?;
1174 let sysroot = SysrootLock::new_from_sysroot(&sysroot).await?;
1175 let storage = Storage::new_ostree(sysroot, &temp_run)?;
1176
1177 Ok((storage, has_ostree))
1178}
1179
1180async fn pull_ostree_install_once(
1181 repo: &ostree::Repo,
1182 imgref: &ImageReference,
1183 target_imgref: &ostree_container::OstreeImageReference,
1184 progress: PullProgress,
1185) -> Result<Box<crate::deploy::ImageState>> {
1186 let prepared = prepare_for_pull(repo, imgref, Some(target_imgref), None).await?;
1187 pull_ostree_install_from_prepared(repo, imgref, prepared, progress).await
1188}
1189
1190async fn pull_ostree_install_from_prepared(
1191 repo: &ostree::Repo,
1192 imgref: &ImageReference,
1193 prepared: PreparedPullResult,
1194 progress: PullProgress,
1195) -> Result<Box<crate::deploy::ImageState>> {
1196 match prepared {
1197 PreparedPullResult::AlreadyPresent(existing) => Ok(existing),
1198 PreparedPullResult::Ready(image_meta) => {
1199 crate::deploy::check_disk_space_ostree(repo, &image_meta, imgref)?;
1200 pull_from_prepared(imgref, progress, *image_meta).await
1201 }
1202 }
1203}
1204
1205#[context("Creating ostree deployment")]
1206async fn install_container(
1207 state: &State,
1208 root_setup: &RootSetup,
1209 sysroot: &ostree::Sysroot,
1210 storage: &Storage,
1211 has_ostree: bool,
1212) -> Result<(ostree::Deployment, InstallAleph)> {
1213 let sepolicy = state.load_policy()?;
1214 let sepolicy = sepolicy.as_ref();
1215 let stateroot = state.stateroot();
1216
1217 let (src_imageref, proxy_cfg) = if !state.source.in_host_mountns {
1219 (state.source.imageref.clone(), None)
1220 } else {
1221 let src_imageref = {
1222 let digest = state
1224 .source
1225 .digest
1226 .as_ref()
1227 .ok_or_else(|| anyhow::anyhow!("Missing container image digest"))?;
1228 let spec = crate::utils::digested_pullspec(&state.source.imageref.name, digest);
1229 ostree_container::ImageReference {
1230 transport: ostree_container::Transport::ContainerStorage,
1231 name: spec,
1232 }
1233 };
1234
1235 let proxy_cfg = crate::deploy::new_proxy_config();
1236 (src_imageref, Some(proxy_cfg))
1237 };
1238 let src_imageref = ostree_container::OstreeImageReference {
1239 sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
1242 imgref: src_imageref,
1243 };
1244
1245 let spec_imgref = ImageReference::from(src_imageref.clone());
1248 let repo = &sysroot.repo();
1249 repo.set_disable_fsync(true);
1250
1251 let use_unified = state.target_opts.unified_storage_exp;
1255 let progress = PullProgress::new(false, ProgressWriter::default());
1256
1257 let pulled_image = if use_unified {
1258 tracing::info!("Using unified storage path for installation");
1259 let prepared = crate::deploy::prepare_for_pull_unified(
1260 repo,
1261 &spec_imgref,
1262 Some(&state.target_imgref),
1263 storage,
1264 None,
1265 )
1266 .await?;
1267 pull_ostree_install_from_prepared(repo, &spec_imgref, prepared, progress.clone()).await?
1268 } else {
1269 let operation =
1270 || pull_ostree_install_once(repo, &spec_imgref, &state.target_imgref, progress.clone());
1271 if spec_imgref.transport == "registry" {
1272 retry_pull_operation(&progress, operation).await?
1273 } else {
1274 operation().await?
1275 }
1276 };
1277
1278 repo.set_disable_fsync(false);
1279
1280 let merged_ostree_root = sysroot
1283 .repo()
1284 .read_commit(pulled_image.ostree_commit.as_str(), gio::Cancellable::NONE)?
1285 .0;
1286 let kargsd = crate::bootc_kargs::get_kargs_from_ostree_root(
1287 &sysroot.repo(),
1288 merged_ostree_root.downcast_ref().unwrap(),
1289 std::env::consts::ARCH,
1290 )?;
1291
1292 if ostree_ext::bootabletree::commit_has_aboot_img(&merged_ostree_root, None)? {
1295 tracing::debug!("Setting bootloader to aboot");
1296 Command::new("ostree")
1297 .args([
1298 "config",
1299 "--repo",
1300 "ostree/repo",
1301 "set",
1302 "sysroot.bootloader",
1303 "aboot",
1304 ])
1305 .cwd_dir(root_setup.physical_root.try_clone()?)
1306 .run_capture_stderr()
1307 .context("Setting bootloader config to aboot")?;
1308 sysroot.repo().reload_config(None::<&gio::Cancellable>)?;
1309 }
1310
1311 let install_config_kargs = state.install_config.as_ref().and_then(|c| c.kargs.as_ref());
1313 let install_config_karg_deletes = state
1314 .install_config
1315 .as_ref()
1316 .and_then(|c| c.karg_deletes.as_ref());
1317
1318 let mut kargs = Cmdline::new();
1324 let mut karg_deletes = Vec::<&str>::new();
1325
1326 kargs.extend(&root_setup.kargs);
1327
1328 if let Some(install_config_kargs) = install_config_kargs {
1329 for karg in install_config_kargs {
1330 kargs.extend(&Cmdline::from(karg.as_str()));
1331 }
1332 }
1333
1334 kargs.extend(&kargsd);
1335
1336 if let Some(install_config_karg_deletes) = install_config_karg_deletes {
1338 for karg_delete in install_config_karg_deletes {
1339 karg_deletes.push(karg_delete);
1340 }
1341 }
1342 if let Some(deletes) = state.config_opts.karg_delete.as_ref() {
1343 for karg_delete in deletes {
1344 karg_deletes.push(karg_delete);
1345 }
1346 }
1347 delete_kargs(&mut kargs, &karg_deletes);
1348
1349 if let Some(cli_kargs) = state.config_opts.karg.as_ref() {
1350 for karg in cli_kargs {
1351 kargs.extend(karg);
1352 }
1353 }
1354
1355 let kargs_strs: Vec<&str> = kargs.iter_str().collect();
1357
1358 let mut options = ostree_container::deploy::DeployOpts::default();
1359 options.kargs = Some(kargs_strs.as_slice());
1360 options.target_imgref = Some(&state.target_imgref);
1361 options.proxy_cfg = proxy_cfg;
1362 options.skip_completion = true; options.no_clean = has_ostree;
1364 let imgstate = crate::utils::async_task_with_spinner(
1365 "Deploying container image",
1366 ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)),
1367 )
1368 .await?;
1369
1370 let deployment = sysroot
1371 .deployments()
1372 .into_iter()
1373 .next()
1374 .ok_or_else(|| anyhow::anyhow!("Failed to find deployment"))?;
1375 let path = sysroot.deployment_dirpath(&deployment);
1377 let root = root_setup
1378 .physical_root
1379 .open_dir(path.as_str())
1380 .context("Opening deployment dir")?;
1381
1382 if let Some(policy) = sepolicy {
1386 let deployment_root_meta = root.dir_metadata()?;
1387 let deployment_root_devino = (deployment_root_meta.dev(), deployment_root_meta.ino());
1388 for d in ["ostree", BOOT] {
1389 let mut pathbuf = Utf8PathBuf::from(d);
1390 crate::lsm::ensure_dir_labeled_recurse(
1391 &root_setup.physical_root,
1392 &mut pathbuf,
1393 policy,
1394 Some(deployment_root_devino),
1395 )
1396 .with_context(|| format!("Recursive SELinux relabeling of {d}"))?;
1397 }
1398
1399 if let Some(cfs_super) = root.open_optional(ostree_prepareroot::COMPOSEFS_IMAGE)? {
1400 let label = crate::lsm::require_label(policy, "/usr".into(), 0o644)?;
1401 crate::lsm::set_security_selinux(cfs_super.as_fd(), label.as_bytes())?;
1402 } else {
1403 tracing::warn!(
1404 "Missing {}; composefs is not enabled?",
1405 ostree_prepareroot::COMPOSEFS_IMAGE
1406 );
1407 }
1408 }
1409
1410 if let Some(boot) = root_setup.boot.as_ref() {
1414 if !boot.source.is_empty() {
1415 crate::lsm::atomic_replace_labeled(&root, "etc/fstab", 0o644.into(), sepolicy, |w| {
1416 writeln!(w, "{}", boot.to_fstab()).map_err(Into::into)
1417 })?;
1418 }
1419 }
1420
1421 if let Some(contents) = state.root_ssh_authorized_keys.as_deref() {
1422 osconfig::inject_root_ssh_authorized_keys(&root, &root, sepolicy, contents)?;
1423 }
1424
1425 let aleph = InstallAleph::new(
1426 &src_imageref,
1427 &state.target_imgref,
1428 &imgstate,
1429 &state.selinux_state,
1430 )?;
1431 Ok((deployment, aleph))
1432}
1433
1434pub(crate) fn delete_kargs(existing: &mut Cmdline, deletes: &Vec<&str>) {
1435 for delete in deletes {
1436 if let Some(param) = utf8::Parameter::parse(&delete) {
1437 if param.value().is_some() {
1438 existing.remove_exact(¶m);
1439 } else {
1440 existing.remove(¶m.key());
1441 }
1442 }
1443 }
1444}
1445
1446pub(crate) fn run_in_host_mountns(cmd: &str) -> Result<Command> {
1448 let mut c = Command::new(bootc_utils::reexec::executable_path()?);
1449 c.lifecycle_bind()
1450 .args(["exec-in-host-mount-namespace", cmd]);
1451 Ok(c)
1452}
1453
1454#[context("Re-exec in host mountns")]
1455pub(crate) fn exec_in_host_mountns(args: &[std::ffi::OsString]) -> Result<()> {
1456 let (cmd, args) = args
1457 .split_first()
1458 .ok_or_else(|| anyhow::anyhow!("Missing command"))?;
1459 tracing::trace!("{cmd:?} {args:?}");
1460 let pid1mountns = std::fs::File::open("/proc/1/ns/mnt").context("open pid1 mountns")?;
1461 rustix::thread::move_into_link_name_space(
1462 pid1mountns.as_fd(),
1463 Some(rustix::thread::LinkNameSpaceType::Mount),
1464 )
1465 .context("setns")?;
1466 rustix::process::chdir("/").context("chdir")?;
1467 if !Utf8Path::new("/usr").try_exists().context("/usr")?
1470 && Utf8Path::new("/root/usr")
1471 .try_exists()
1472 .context("/root/usr")?
1473 {
1474 tracing::debug!("Using supermin workaround");
1475 rustix::process::chroot("/root").context("chroot")?;
1476 }
1477 Err(Command::new(cmd).args(args).arg0(bootc_utils::NAME).exec()).context("exec")?
1478}
1479
1480#[derive(Debug)]
1481pub(crate) struct RootSetup {
1482 #[cfg(feature = "install-to-disk")]
1483 luks_device: Option<String>,
1484 pub(crate) device_info: bootc_blockdev::Device,
1485 pub(crate) physical_root_path: Utf8PathBuf,
1488 pub(crate) physical_root: Dir,
1490 pub(crate) target_root_path: Option<Utf8PathBuf>,
1492 pub(crate) rootfs_uuid: Option<String>,
1493 pub(crate) aboot_disk_layout: Option<AbootDiskLayout>,
1494 pub(crate) disk_install: bool,
1495 skip_finalize: bool,
1497 boot: Option<MountSpec>,
1498 pub(crate) kargs: CmdlineOwned,
1499}
1500
1501fn require_boot_uuid(spec: &MountSpec) -> Result<&str> {
1502 spec.get_source_uuid()
1503 .ok_or_else(|| anyhow!("/boot is not specified via UUID= (this is currently required)"))
1504}
1505
1506impl RootSetup {
1507 pub(crate) fn get_boot_uuid(&self) -> Result<Option<&str>> {
1510 self.boot.as_ref().map(require_boot_uuid).transpose()
1511 }
1512
1513 pub(crate) fn boot_mount_spec(&self) -> Option<&MountSpec> {
1515 self.boot.as_ref()
1516 }
1517
1518 #[cfg(feature = "install-to-disk")]
1520 fn into_storage(self) -> (Utf8PathBuf, Option<String>) {
1521 (self.physical_root_path, self.luks_device)
1522 }
1523}
1524
1525#[derive(Debug)]
1526#[allow(dead_code)]
1527pub(crate) enum SELinuxFinalState {
1528 ForceTargetDisabled,
1530 Enabled(Option<crate::lsm::SetEnforceGuard>),
1532 HostDisabled,
1534 Disabled,
1536}
1537
1538impl SELinuxFinalState {
1539 pub(crate) fn enabled(&self) -> bool {
1541 match self {
1542 SELinuxFinalState::ForceTargetDisabled | SELinuxFinalState::Disabled => false,
1543 SELinuxFinalState::Enabled(_) | SELinuxFinalState::HostDisabled => true,
1544 }
1545 }
1546
1547 pub(crate) fn to_aleph(&self) -> &'static str {
1550 match self {
1551 SELinuxFinalState::ForceTargetDisabled => "force-target-disabled",
1552 SELinuxFinalState::Enabled(_) => "enabled",
1553 SELinuxFinalState::HostDisabled => "host-disabled",
1554 SELinuxFinalState::Disabled => "disabled",
1555 }
1556 }
1557}
1558
1559pub(crate) fn reexecute_self_for_selinux_if_needed(
1564 srcdata: &SourceInfo,
1565 override_disable_selinux: bool,
1566 reexec_env: &[(&str, &str)],
1567) -> Result<SELinuxFinalState> {
1568 if srcdata.selinux {
1570 let host_selinux = crate::lsm::host_selinux_enabled()?;
1571 tracing::debug!("Target has SELinux, host={host_selinux}");
1572 let r = if override_disable_selinux {
1573 println!("notice: Target has SELinux enabled, overriding to disable");
1574 SELinuxFinalState::ForceTargetDisabled
1575 } else if host_selinux {
1576 setup_sys_mount("selinuxfs", SELINUXFS)?;
1582 let g = crate::lsm::selinux_ensure_install_or_setenforce(reexec_env)?;
1584 SELinuxFinalState::Enabled(g)
1585 } else {
1586 SELinuxFinalState::HostDisabled
1587 };
1588 Ok(r)
1589 } else {
1590 Ok(SELinuxFinalState::Disabled)
1591 }
1592}
1593
1594pub(crate) fn finalize_filesystem(
1597 fsname: &str,
1598 root: &Dir,
1599 path: impl AsRef<Utf8Path>,
1600) -> Result<()> {
1601 let path = path.as_ref();
1602 Task::new(format!("Trimming {fsname}"), "fstrim")
1604 .args(["--quiet-unsupported", "-v", path.as_str()])
1605 .cwd(root)?
1606 .run()?;
1607 Task::new(format!("Finalizing filesystem {fsname}"), "mount")
1610 .cwd(root)?
1611 .args(["-o", "remount,ro", path.as_str()])
1612 .run()?;
1613 let fsdir = root.open_dir(path.as_str())?;
1617 let st = rustix::fs::fstatfs(fsdir.as_fd())?;
1618 if st.f_type == libc::MSDOS_SUPER_MAGIC {
1619 tracing::debug!("Filesystem {fsname} is VFAT, skipping fsfreeze");
1620 } else {
1621 for a in ["-f", "-u"] {
1622 Command::new("fsfreeze")
1623 .cwd_dir(root.try_clone()?)
1624 .args([a, path.as_str()])
1625 .run_capture_stderr()?;
1626 }
1627 }
1628 Ok(())
1629}
1630
1631fn require_host_pidns() -> Result<()> {
1633 if rustix::process::getpid().is_init() {
1634 anyhow::bail!("This command must be run with the podman --pid=host flag")
1635 }
1636 tracing::trace!("OK: we're not pid 1");
1637 Ok(())
1638}
1639
1640fn require_host_userns() -> Result<()> {
1643 let proc1 = "/proc/1";
1644 let pid1_uid = Path::new(proc1)
1645 .metadata()
1646 .with_context(|| format!("Querying {proc1}"))?
1647 .uid();
1648 ensure!(
1651 pid1_uid == 0,
1652 "{proc1} is owned by {pid1_uid}, not zero; this command must be run in the root user namespace (e.g. not rootless podman)"
1653 );
1654 tracing::trace!("OK: we're in a matching user namespace with pid1");
1655 Ok(())
1656}
1657
1658pub(crate) fn setup_tmp_mount() -> Result<()> {
1663 let st = rustix::fs::statfs("/tmp")?;
1664 if st.f_type == libc::TMPFS_MAGIC {
1665 tracing::trace!("Already have tmpfs /tmp")
1666 } else {
1667 Command::new("mount")
1670 .args(["tmpfs", "-t", "tmpfs", "/tmp"])
1671 .run_capture_stderr()?;
1672 }
1673 Ok(())
1674}
1675
1676#[context("Ensuring sys mount {fspath} {fstype}")]
1679pub(crate) fn setup_sys_mount(fstype: &str, fspath: &str) -> Result<()> {
1680 tracing::debug!("Setting up sys mounts");
1681 let rootfs = format!("/proc/1/root/{fspath}");
1682 if !Path::new(rootfs.as_str()).try_exists()? {
1684 return Ok(());
1685 }
1686
1687 if std::fs::read_dir(rootfs)?.next().is_none() {
1689 return Ok(());
1690 }
1691
1692 if Path::new(fspath).try_exists()? && std::fs::read_dir(fspath)?.next().is_some() {
1696 return Ok(());
1697 }
1698
1699 Command::new("mount")
1701 .args(["-t", fstype, fstype, fspath])
1702 .run_capture_stderr()?;
1703
1704 Ok(())
1705}
1706
1707#[context("Verifying fetch")]
1709async fn verify_target_fetch(
1710 tmpdir: &Dir,
1711 imgref: &ostree_container::OstreeImageReference,
1712) -> Result<()> {
1713 let tmpdir = &TempDir::new_in(&tmpdir)?;
1714 let tmprepo = &ostree::Repo::create_at_dir(tmpdir.as_fd(), ".", ostree::RepoMode::Bare, None)
1715 .context("Init tmp repo")?;
1716
1717 tracing::trace!("Verifying fetch for {imgref}");
1718 let mut imp =
1719 ostree_container::store::ImageImporter::new(tmprepo, imgref, Default::default()).await?;
1720 use ostree_container::store::PrepareResult;
1721 let prep = match imp.prepare().await? {
1722 PrepareResult::AlreadyPresent(_) => unreachable!(),
1724 PrepareResult::Ready(r) => r,
1725 };
1726 tracing::debug!("Fetched manifest with digest {}", prep.manifest_digest);
1727 Ok(())
1728}
1729
1730const ROOT_SSH_AUTHORIZED_KEYS_ENV: &str = "_BOOTC_ROOT_SSH_AUTHORIZED_KEYS";
1732
1733async fn prepare_install(
1735 mut config_opts: InstallConfigOpts,
1736 source_opts: InstallSourceOpts,
1737 mut target_opts: InstallTargetOpts,
1738 mut composefs_options: InstallComposefsOpts,
1739 target_fs: Option<FilesystemEnum>,
1740 disk_install: bool,
1741) -> Result<Arc<State>> {
1742 tracing::trace!("Preparing install");
1743
1744 let allow_missing_verity_explicit = composefs_options.allow_missing_verity;
1745 let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
1746 .context("Opening /")?;
1747
1748 let host_is_container = crate::containerenv::is_container(&rootfs);
1749 let external_source = source_opts.source_imgref.is_some();
1750 let (source, target_rootfs) = match source_opts.source_imgref {
1751 None => {
1752 ensure!(
1753 host_is_container,
1754 "Either --source-imgref must be defined or this command must be executed inside a podman container."
1755 );
1756
1757 crate::cli::require_root(true)?;
1758
1759 require_host_pidns()?;
1760 require_host_userns()?;
1763 let container_info = crate::containerenv::get_container_execution_info(&rootfs)?;
1764 match container_info.rootless.as_deref() {
1766 Some("1") => anyhow::bail!(
1767 "Cannot install from rootless podman; this command must be run as root"
1768 ),
1769 Some(o) => tracing::debug!("rootless={o}"),
1770 None => tracing::debug!(
1772 "notice: Did not find rootless= entry in {}",
1773 crate::containerenv::PATH,
1774 ),
1775 };
1776 tracing::trace!("Read container engine info {:?}", container_info);
1777
1778 let source = SourceInfo::from_container(&rootfs, &container_info)?;
1779 (source, Some(rootfs.try_clone()?))
1780 }
1781 Some(source) => {
1782 crate::cli::require_root(false)?;
1783 let source = SourceInfo::from_imageref(&source, &rootfs)?;
1784 (source, None)
1785 }
1786 };
1787
1788 let install_config = config::load_config()?;
1791 if let Some(ref config) = install_config {
1792 tracing::debug!("Loaded install configuration");
1793 if !config_opts.bootupd_skip_boot_uuid {
1796 config_opts.bootupd_skip_boot_uuid = config
1797 .bootupd
1798 .as_ref()
1799 .and_then(|b| b.skip_boot_uuid)
1800 .unwrap_or(false);
1801 }
1802
1803 if config_opts.bootloader.is_none() {
1804 config_opts.bootloader = config.bootloader.clone();
1805 }
1806
1807 if !target_opts.enforce_container_sigpolicy {
1808 target_opts.enforce_container_sigpolicy =
1809 config.enforce_container_sigpolicy.unwrap_or(false);
1810 }
1811 } else {
1812 tracing::debug!("No install configuration found");
1813 }
1814
1815 if target_opts.target_no_signature_verification {
1818 tracing::debug!(
1820 "Use of --target-no-signature-verification flag which is enabled by default"
1821 );
1822 }
1823 let target_sigverify = sigpolicy_from_opt(target_opts.enforce_container_sigpolicy);
1824 let target_imgname = target_opts
1825 .target_imgref
1826 .as_deref()
1827 .unwrap_or(source.imageref.name.as_str());
1828 let target_transport =
1829 ostree_container::Transport::try_from(target_opts.target_transport.as_str())?;
1830 let target_imgref = ostree_container::OstreeImageReference {
1831 sigverify: target_sigverify,
1832 imgref: ostree_container::ImageReference {
1833 transport: target_transport,
1834 name: target_imgname.to_string(),
1835 },
1836 };
1837 tracing::debug!("Target image reference: {target_imgref}");
1838
1839 let kernel = target_rootfs
1840 .as_ref()
1841 .map(|rootfs| crate::kernel::find_kernel(rootfs, Some(&composefs_options.uki_addon_opts)))
1842 .transpose()?
1843 .flatten();
1844 let composefs_required = kernel.as_ref().is_some_and(|k| k.k_type.is_unified());
1845 #[cfg(feature = "install-to-disk")]
1846 let aboot_disk_layout = if disk_install {
1847 kernel.as_ref().and_then(|k| match k.k_type {
1848 crate::kernel::KernelType::Aboot { encoding, .. } => {
1849 Some(AbootDiskLayout::from(encoding))
1850 }
1851 _ => None,
1852 })
1853 } else {
1854 None
1855 };
1856
1857 tracing::debug!("Composefs required: {composefs_required}");
1858
1859 let ostree_prepareroot_config = ostree_prepareroot::load_config_from_root(&rootfs)?
1864 .map(|kf| -> Result<HashMap<String, String>> {
1865 let mut r = HashMap::new();
1866 for grp in kf.groups() {
1867 for key in kf.keys(&grp)? {
1868 let key = key.as_str();
1869 let value = kf.value(&grp, key)?;
1870 r.insert(format!("{grp}.{key}"), value.to_string());
1871 }
1872 }
1873 Ok(r)
1874 })
1875 .transpose()?;
1876
1877 let composefs_default = crate::bootc_composefs::image::defaults_to_composefs_backend(
1880 &rootfs,
1881 ostree_prepareroot_config.is_some(),
1882 )?;
1883 tracing::debug!("Composefs default: {composefs_default}");
1884 composefs_options.composefs_backend |= composefs_required || composefs_default;
1885 let config_stateroot = install_config
1888 .as_ref()
1889 .and_then(|c| c.stateroot.as_deref())
1890 .filter(|_| config_opts.stateroot.is_none())
1891 .filter(|_| composefs_required || composefs_default);
1892 composefs_options.validate(
1893 config_opts.bootloader.as_ref(),
1894 config_opts.stateroot.as_deref(),
1895 config_stateroot,
1896 )?;
1897
1898 composefs_options.validate_bound_images(config_opts.bound_images, &rootfs)?;
1899
1900 if !composefs_options.composefs_backend
1901 && (composefs_options.uki_addon_opts.scoped.is_some()
1902 || composefs_options.uki_addon_opts.global.is_some())
1903 {
1904 anyhow::bail!("UKI Addons are only supported on composefs backends");
1905 }
1906
1907 let root_ssh_authorized_keys = config_opts
1912 .root_ssh_authorized_keys
1913 .as_ref()
1914 .map(|p| -> Result<String> {
1915 use std::env::VarError;
1916 match std::env::var(ROOT_SSH_AUTHORIZED_KEYS_ENV) {
1917 Ok(v) => Ok(v),
1919 Err(VarError::NotPresent) => {
1920 std::fs::read_to_string(p).with_context(|| format!("Reading {p}"))
1921 }
1922 Err(e) => Err(e).with_context(|| format!("Parsing {ROOT_SSH_AUTHORIZED_KEYS_ENV}")),
1923 }
1924 })
1925 .transpose()?;
1926 let reexec_env: Vec<(&str, &str)> = root_ssh_authorized_keys
1927 .as_deref()
1928 .map(|v| (ROOT_SSH_AUTHORIZED_KEYS_ENV, v))
1929 .into_iter()
1930 .collect();
1931
1932 bootc_mount::ensure_mirrored_host_mount("/dev")?;
1934 bootc_mount::ensure_mirrored_host_mount("/var/lib/containers")?;
1937 bootc_mount::ensure_mirrored_host_mount("/var/tmp")?;
1940 bootc_mount::ensure_mirrored_host_mount("/run/udev")?;
1943 setup_tmp_mount()?;
1945 let tempdir = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
1948 osbuild::adjust_for_bootc_image_builder(&rootfs, &tempdir)?;
1950
1951 if target_opts.run_fetch_check {
1952 verify_target_fetch(&tempdir, &target_imgref).await?;
1953 }
1954
1955 if !external_source && std::env::var_os("BOOTC_SKIP_UNSHARE").is_none() {
1958 super::cli::ensure_self_unshared_mount_namespace(&reexec_env)?;
1959 }
1960
1961 setup_sys_mount("efivarfs", EFIVARFS)?;
1962
1963 let selinux_state =
1965 reexecute_self_for_selinux_if_needed(&source, config_opts.disable_selinux, &reexec_env)?;
1966 tracing::debug!("SELinux state: {selinux_state:?}");
1967
1968 println!("Installing image: {:#}", &target_imgref);
1969 if let Some(digest) = source.digest.as_deref() {
1970 println!("Digest: {digest}");
1971 }
1972
1973 let root_filesystem = target_fs.or(install_config
1976 .as_ref()
1977 .and_then(|c| c.filesystem_root())
1978 .and_then(|r| r.fstype));
1979
1980 let mut immutable_boot = false;
1981
1982 if let Some(root_filesystem) = root_filesystem {
1990 if let Some(kernel) = kernel.as_ref().filter(|k| k.k_type.is_unified()) {
1991 let allow_missing_fsverity = match kernel.k_type.cmdline() {
1992 Some(cmdline) => ComposefsCmdline::find_in_cmdline(cmdline)?
1993 .is_some_and(|cfs_cmdline| cfs_cmdline.allow_missing_fsverity),
1994 None => false,
1995 };
1996
1997 if !allow_missing_fsverity {
1998 anyhow::ensure!(
1999 root_filesystem.supports_fsverity(),
2000 "Specified filesystem {root_filesystem} does not support fs-verity"
2001 );
2002 }
2003
2004 composefs_options.allow_missing_verity = allow_missing_fsverity;
2005 immutable_boot = true;
2006 }
2007
2008 if composefs_options.composefs_backend
2010 && !composefs_options.allow_missing_verity
2011 && !immutable_boot
2012 {
2013 composefs_options.allow_missing_verity = !root_filesystem.supports_fsverity();
2014 }
2015 }
2016
2017 tracing::info!(
2018 root_filesystem = root_filesystem
2019 .map(|f| f.to_string())
2020 .unwrap_or("None".into()),
2021 allow_missing_fsverity = composefs_options.allow_missing_verity,
2022 immutable_boot = immutable_boot,
2023 "ComposeFS install prep",
2024 );
2025
2026 if let Some(crate::spec::Bootloader::None) = config_opts.bootloader {
2027 if cfg!(target_arch = "s390x") {
2028 anyhow::bail!("Bootloader set to none is not supported for the s390x architecture");
2029 }
2030 }
2031
2032 let ostree_prepareroot_config = match ostree_prepareroot_config {
2035 Some(c) => c,
2036 None if composefs_options.composefs_backend => HashMap::new(),
2037 None => anyhow::bail!(
2038 "Failed to find {} in /usr/lib or /etc",
2039 ostree_prepareroot::CONF_PATH
2040 ),
2041 };
2042
2043 let state = Arc::new(State {
2047 selinux_state,
2048 source,
2049 config_opts,
2050 target_opts,
2051 target_imgref,
2052 install_config,
2053 ostree_prepareroot_config,
2054 root_ssh_authorized_keys,
2055 container_root: rootfs,
2056 tempdir,
2057 host_is_container,
2058 composefs_required,
2059 composefs_options,
2060 composefs_fsverity_supported: root_filesystem
2062 .map(|fs| fs.supports_fsverity())
2063 .unwrap_or(true),
2064 allow_missing_verity_explicit,
2065 #[cfg(feature = "install-to-disk")]
2066 aboot_disk_layout,
2067 });
2068
2069 Ok(state)
2070}
2071
2072impl PostFetchState {
2073 pub(crate) fn new(
2074 state: &State,
2075 d: &Dir,
2076 image_bootloader: Option<Bootloader>,
2077 ) -> Result<Self> {
2078 let detected_bootloader =
2081 select_bootloader(state.config_opts.bootloader, image_bootloader, d)?;
2082 println!("Bootloader: {detected_bootloader}");
2083 let r = Self {
2084 detected_bootloader,
2085 };
2086 Ok(r)
2087 }
2088}
2089
2090fn select_bootloader(
2091 configured: Option<Bootloader>,
2092 image: Option<Bootloader>,
2093 root: &Dir,
2094) -> Result<Bootloader> {
2095 if let Some(bootloader) = configured.or(image) {
2096 return Ok(bootloader);
2097 }
2098 if crate::bootloader::supports_bootupd(root)? {
2099 Ok(Bootloader::Grub)
2100 } else {
2101 Ok(Bootloader::Systemd)
2102 }
2103}
2104
2105async fn install_with_sysroot(
2110 state: &State,
2111 rootfs: &RootSetup,
2112 storage: &Storage,
2113 boot_uuid: &str,
2114 bound_images: BoundImages,
2115 has_ostree: bool,
2116) -> Result<()> {
2117 let ostree = storage.get_ostree()?;
2118 let c_storage = storage.get_ensure_imgstore()?;
2119
2120 let (deployment, aleph) = install_container(state, rootfs, ostree, storage, has_ostree).await?;
2123 aleph.write_to(&rootfs.physical_root)?;
2125
2126 let deployment_path = ostree.deployment_dirpath(&deployment);
2127
2128 let deployment_dir = rootfs
2129 .physical_root
2130 .open_dir(&deployment_path)
2131 .context("Opening deployment dir")?;
2132 let postfetch = PostFetchState::new(state, &deployment_dir, None)?;
2133
2134 if cfg!(target_arch = "s390x") {
2135 crate::bootloader::install_via_zipl(&rootfs.device_info.require_single_root()?, boot_uuid)?;
2138 } else {
2139 match postfetch.detected_bootloader {
2140 Bootloader::Grub => {
2141 let root_path = rootfs
2142 .target_root_path
2143 .clone()
2144 .unwrap_or(rootfs.physical_root_path.clone());
2145 let chroot_target = root_path.join(deployment_path.as_str());
2146 let bind_boot_path = root_path.join(BOOT);
2147 crate::bootloader::install_via_bootupd(
2148 &rootfs.device_info,
2149 &root_path,
2150 &state.config_opts,
2151 Some(chroot_target.as_path()),
2152 Some(bind_boot_path.as_path()),
2153 )?;
2154 }
2155 Bootloader::Systemd | Bootloader::GrubCC | Bootloader::Ukiboot => {
2156 anyhow::bail!("bootupd is required for ostree-based installs");
2157 }
2158 Bootloader::None => {
2159 tracing::debug!("Skip bootloader installation due set to None");
2160 }
2161 }
2162 }
2163 tracing::debug!("Installed bootloader");
2164
2165 tracing::debug!("Performing post-deployment operations");
2166
2167 match bound_images {
2168 BoundImages::Skip => {}
2169 BoundImages::Resolved(resolved_bound_images) => {
2170 for image in resolved_bound_images {
2172 let image = image.image.as_str();
2173 c_storage.pull_from_host_storage(image).await?;
2174 }
2175 }
2176 BoundImages::Unresolved(bound_images) => {
2177 crate::boundimage::pull_images_impl(c_storage, bound_images)
2178 .await
2179 .context("pulling bound images")?;
2180 }
2181 }
2182
2183 Ok(())
2184}
2185
2186enum BoundImages {
2187 Skip,
2188 Resolved(Vec<ResolvedBoundImage>),
2189 Unresolved(Vec<BoundImage>),
2190}
2191
2192impl BoundImages {
2193 async fn from_state(state: &State) -> Result<Self> {
2194 let bound_images = match state.config_opts.bound_images {
2195 BoundImagesOpt::Skip => BoundImages::Skip,
2196 others => {
2197 let queried_images = crate::boundimage::query_bound_images(&state.container_root)?;
2198 match others {
2199 BoundImagesOpt::Stored => {
2200 let mut r = Vec::with_capacity(queried_images.len());
2202 for image in queried_images {
2203 let resolved = ResolvedBoundImage::from_image(&image).await?;
2204 tracing::debug!("Resolved {}: {}", resolved.image, resolved.digest);
2205 r.push(resolved)
2206 }
2207 BoundImages::Resolved(r)
2208 }
2209 BoundImagesOpt::Pull => {
2210 BoundImages::Unresolved(queried_images)
2212 }
2213 BoundImagesOpt::Skip => anyhow::bail!("unreachable error"),
2214 }
2215 }
2216 };
2217
2218 Ok(bound_images)
2219 }
2220}
2221
2222async fn ostree_install(state: &State, rootfs: &RootSetup, cleanup: Cleanup) -> Result<()> {
2223 let boot_uuid = rootfs
2225 .get_boot_uuid()?
2226 .or(rootfs.rootfs_uuid.as_deref())
2227 .ok_or_else(|| anyhow!("No uuid for boot/root"))?;
2228 tracing::debug!("boot uuid={boot_uuid}");
2229
2230 let bound_images = BoundImages::from_state(state).await?;
2231
2232 {
2235 let (sysroot, has_ostree) = initialize_ostree_root(state, rootfs).await?;
2236
2237 install_with_sysroot(
2238 state,
2239 rootfs,
2240 &sysroot,
2241 &boot_uuid,
2242 bound_images,
2243 has_ostree,
2244 )
2245 .await?;
2246 let ostree = sysroot.get_ostree()?;
2247
2248 if matches!(cleanup, Cleanup::TriggerOnNextBoot) {
2249 let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
2250 tracing::debug!("Writing {DESTRUCTIVE_CLEANUP}");
2251 sysroot_dir.atomic_write(DESTRUCTIVE_CLEANUP, b"")?;
2252 }
2253
2254 sysroot.ensure_imgstore_labeled()?;
2257
2258 };
2261
2262 install_finalize(&rootfs.physical_root_path).await?;
2264
2265 Ok(())
2266}
2267
2268async fn install_to_filesystem_impl(
2269 state: &State,
2270 rootfs: &mut RootSetup,
2271 cleanup: Cleanup,
2272) -> Result<()> {
2273 if matches!(state.selinux_state, SELinuxFinalState::ForceTargetDisabled) {
2274 rootfs.kargs.extend(&Cmdline::from("selinux=0"));
2275 }
2276 let rootfs = &*rootfs;
2278
2279 match rootfs.device_info.pttype.as_deref() {
2280 Some("dos") => crate::utils::medium_visibility_warning(
2281 "Installing to `dos` format partitions is not recommended",
2282 ),
2283 Some("gpt") => {
2284 }
2286 Some(o) => {
2287 crate::utils::medium_visibility_warning(&format!("Unknown partition table type {o}"))
2288 }
2289 None => {
2290 }
2292 }
2293
2294 if state.composefs_options.composefs_backend {
2295 let fetch_ref = state.source.composefs_fetch_reference();
2296 let (manifest, _) = get_container_manifest_and_config(&fetch_ref).await?;
2297 let provisional_relaxed = !state.composefs_fsverity_supported;
2301 let fetch_imgref = Some(fetch_ref.clone().into());
2302 let initialized = initialize_composefs_repository(state, rootfs, provisional_relaxed)?;
2303 crate::deploy::check_disk_space_composefs(
2304 &initialized.repo,
2305 &manifest.manifest,
2306 &ImageReference {
2307 image: fetch_ref.name.clone(),
2308 transport: fetch_ref.transport.to_string(),
2309 signature: None,
2310 },
2311 )?;
2312 let pull_result = pull_install_composefs_repository(
2313 state,
2314 rootfs,
2315 &initialized.repo,
2316 state.target_opts.unified_storage_exp,
2317 fetch_imgref.as_ref(),
2318 )
2319 .await?;
2320 let image_fsverity_policy = crate::bootc_composefs::repo::inspect_image_fsverity_policy(
2321 &initialized.repo,
2322 &pull_result,
2323 )?;
2324
2325 let requested_relaxed = crate::bootc_composefs::repo::final_repository_policy(
2326 image_fsverity_policy,
2327 state.composefs_options.allow_missing_verity,
2328 );
2329 if !initialized.created {
2330 validate_repository_policy(
2331 initialized.repository_insecure,
2332 requested_relaxed,
2333 state.allow_missing_verity_explicit,
2334 )?;
2335 } else if !requested_relaxed && provisional_relaxed {
2336 anyhow::bail!(
2337 "Initial boot artifact requires fs-verity, but the target filesystem does not support it"
2338 );
2339 }
2340 drop(initialized.repo);
2343 if initialized.created && requested_relaxed && !provisional_relaxed {
2344 finalize_fresh_repository_policy(
2345 &rootfs.physical_root,
2346 crate::bootc_composefs::repo::composefs_repository_config(false),
2347 )?;
2348 }
2349 let allow_missing_verity = requested_relaxed;
2350 tag_pulled_image(&rootfs.physical_root, &pull_result)?;
2351
2352 setup_composefs_boot(rootfs, state, &pull_result, allow_missing_verity).await?;
2353
2354 if let Some(policy) = state.load_policy()? {
2357 tracing::info!("Labeling composefs objects as /usr");
2358 crate::lsm::relabel_recurse(
2359 &rootfs.physical_root,
2360 "composefs",
2361 Some("/usr".into()),
2362 &policy,
2363 )
2364 .context("SELinux labeling of composefs objects")?;
2365 }
2366 } else {
2367 ostree_install(state, rootfs, cleanup).await?;
2368
2369 if cfg!(target_arch = "s390x") {
2373 Command::new("ostree")
2374 .args([
2375 "config",
2376 "--repo",
2377 "ostree/repo",
2378 "set",
2379 "sysroot.bootloader",
2380 "zipl",
2381 ])
2382 .cwd_dir(rootfs.physical_root.try_clone()?)
2383 .run_capture_stderr()
2384 .context("Setting bootloader config to zipl")?;
2385 }
2386 }
2387
2388 if let Some(policy) = state.load_policy()? {
2392 tracing::info!("Performing final SELinux relabeling of physical root");
2393 let mut path = Utf8PathBuf::from("");
2394 crate::lsm::ensure_dir_labeled_recurse(&rootfs.physical_root, &mut path, &policy, None)
2395 .context("Final SELinux relabeling of physical root")?;
2396 } else {
2397 tracing::debug!("Skipping final SELinux relabel (SELinux is disabled)");
2398 }
2399
2400 if !rootfs.skip_finalize {
2402 let bootfs = rootfs.boot.as_ref().map(|_| ("boot", BOOT));
2403 for (fsname, fs) in std::iter::once(("root", ".")).chain(bootfs) {
2404 finalize_filesystem(fsname, &rootfs.physical_root, fs)?;
2405 }
2406 }
2407
2408 Ok(())
2409}
2410
2411fn installation_complete() {
2412 println!("Installation complete!");
2413}
2414
2415#[context("Installing to disk")]
2417#[cfg(feature = "install-to-disk")]
2418pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
2419 const INSTALL_DISK_JOURNAL_ID: &str = "8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2";
2421 let source_image = opts
2422 .source_opts
2423 .source_imgref
2424 .as_ref()
2425 .map(|s| s.as_str())
2426 .unwrap_or("none");
2427 let target_device = opts.block_opts.device.as_str();
2428
2429 tracing::info!(
2430 message_id = INSTALL_DISK_JOURNAL_ID,
2431 bootc.source_image = source_image,
2432 bootc.target_device = target_device,
2433 bootc.via_loopback = if opts.via_loopback { "true" } else { "false" },
2434 "Starting disk installation from {} to {}",
2435 source_image,
2436 target_device
2437 );
2438
2439 let mut block_opts = opts.block_opts;
2440 let target_blockdev_meta = block_opts
2441 .device
2442 .metadata()
2443 .with_context(|| format!("Querying {}", &block_opts.device))?;
2444 if opts.via_loopback {
2445 if !opts.config_opts.generic_image {
2446 crate::utils::medium_visibility_warning(
2447 "Automatically enabling --generic-image when installing via loopback",
2448 );
2449 opts.config_opts.generic_image = true;
2450 }
2451 if !target_blockdev_meta.file_type().is_file() {
2452 anyhow::bail!(
2453 "Not a regular file (to be used via loopback): {}",
2454 block_opts.device
2455 );
2456 }
2457 } else if !target_blockdev_meta.file_type().is_block_device() {
2458 anyhow::bail!("Not a block device: {}", block_opts.device);
2459 }
2460
2461 let state = prepare_install(
2462 opts.config_opts,
2463 opts.source_opts,
2464 opts.target_opts,
2465 opts.composefs_opts,
2466 block_opts.filesystem,
2467 true,
2468 )
2469 .await?;
2470
2471 let (mut rootfs, loopback) = {
2473 let loopback_dev = if opts.via_loopback {
2474 let loopback_dev =
2475 bootc_blockdev::LoopbackDevice::new(block_opts.device.as_std_path())?;
2476 block_opts.device = loopback_dev.path().into();
2477 Some(loopback_dev)
2478 } else {
2479 None
2480 };
2481
2482 let state = state.clone();
2483 let rootfs = tokio::task::spawn_blocking(move || {
2484 baseline::install_create_rootfs(&state, block_opts, opts.run_repart)
2485 })
2486 .await??;
2487 (rootfs, loopback_dev)
2488 };
2489
2490 install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip).await?;
2491
2492 let (root_path, luksdev) = rootfs.into_storage();
2494 Task::new_and_run(
2495 "Unmounting filesystems",
2496 "umount",
2497 ["-R", root_path.as_str()],
2498 )?;
2499 if let Some(luksdev) = luksdev.as_deref() {
2500 Task::new_and_run("Closing root LUKS device", "cryptsetup", ["close", luksdev])?;
2501 }
2502
2503 if let Some(loopback_dev) = loopback {
2504 loopback_dev.close()?;
2505 }
2506
2507 if let Some(state) = Arc::into_inner(state) {
2509 state.consume()?;
2510 } else {
2511 tracing::warn!("Failed to consume state Arc");
2513 }
2514
2515 installation_complete();
2516
2517 Ok(())
2518}
2519
2520#[context("Requiring directory contains only mount points")]
2531fn require_dir_contains_only_mounts(parent_fd: &Dir, dir_name: &str) -> Result<()> {
2532 tracing::trace!("Checking directory {dir_name} for non-mount entries");
2533 let Some(dir_fd) = parent_fd.open_dir_noxdev(dir_name)? else {
2534 tracing::trace!("{dir_name} is a mount point");
2536 return Ok(());
2537 };
2538
2539 if dir_fd.entries()?.next().is_none() {
2540 anyhow::bail!("Found empty directory: {dir_name}");
2541 }
2542
2543 for entry in dir_fd.entries()? {
2544 tracing::trace!("Checking entry in {dir_name}");
2545 let entry = DirEntryUtf8::from_cap_std(entry?);
2546 let entry_name = entry.file_name()?;
2547
2548 if entry_name == LOST_AND_FOUND {
2549 continue;
2550 }
2551
2552 let etype = entry.file_type()?;
2553 if etype == FileType::dir() {
2554 require_dir_contains_only_mounts(&dir_fd, &entry_name)?;
2555 } else {
2556 anyhow::bail!("Found entry in {dir_name}: {entry_name}");
2557 }
2558 }
2559
2560 Ok(())
2561}
2562
2563#[context("Verifying empty rootfs")]
2564fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
2565 for e in rootfs_fd.entries()? {
2566 let e = DirEntryUtf8::from_cap_std(e?);
2567 let name = e.file_name()?;
2568 if name == LOST_AND_FOUND {
2569 continue;
2570 }
2571
2572 let etype = e.file_type()?;
2574 if etype == FileType::dir() {
2575 require_dir_contains_only_mounts(rootfs_fd, &name)?;
2576 } else {
2577 anyhow::bail!("Non-empty root filesystem; found {name:?}");
2578 }
2579 }
2580 Ok(())
2581}
2582
2583fn remove_all_in_dir_no_xdev(d: &Dir, mount_err: bool) -> Result<()> {
2587 for entry in d.entries()? {
2588 let entry = entry?;
2589 let name = entry.file_name();
2590 let etype = entry.file_type()?;
2591 if etype == FileType::dir() {
2592 remove_dir_no_xdev(d, &name, mount_err)?;
2593 } else {
2594 d.remove_file_optional(&name)?;
2595 }
2596 }
2597 anyhow::Ok(())
2598}
2599
2600fn remove_dir_no_xdev(d: &Dir, name: impl AsRef<Path>, mount_err: bool) -> Result<()> {
2603 let name = name.as_ref();
2604 if let Some(subdir) = d.open_dir_noxdev(name)? {
2605 remove_all_in_dir_no_xdev(&subdir, mount_err)?;
2606 d.remove_dir(name)?;
2607 } else if mount_err {
2608 anyhow::bail!("Found unexpected mount point {name:?}");
2609 }
2610 Ok(())
2611}
2612
2613#[context("Removing boot directory content except loader dir on ostree")]
2614fn remove_all_except_loader_dirs(bootdir: &Dir, is_ostree: bool) -> Result<()> {
2615 let entries = bootdir
2616 .entries()
2617 .context("Reading boot directory entries")?;
2618
2619 for entry in entries {
2620 let entry = entry.context("Reading directory entry")?;
2621 let file_name = entry.file_name();
2622 let file_name = if let Some(n) = file_name.to_str() {
2623 n
2624 } else {
2625 anyhow::bail!("Invalid non-UTF8 filename: {file_name:?} in /boot");
2626 };
2627
2628 if is_ostree && file_name.starts_with("loader") {
2632 continue;
2633 }
2634
2635 let etype = entry.file_type()?;
2636 if etype == FileType::dir() {
2637 remove_dir_no_xdev(bootdir, file_name, false)
2638 .with_context(|| format!("Removing directory: {file_name}"))?;
2639 } else {
2640 bootdir
2641 .remove_file_optional(&file_name)
2642 .with_context(|| format!("Removing file: {}", file_name))?;
2643 }
2644 }
2645 Ok(())
2646}
2647
2648#[context("Removing bootloader content from EFI system partition")]
2654fn clean_esp_bootloader_dirs(efidir: &Dir) -> Result<()> {
2655 for name in ["EFI", "loader"] {
2656 if efidir.try_exists(name)? {
2657 remove_dir_no_xdev(efidir, name, false)
2658 .with_context(|| format!("Removing directory: {name}"))?;
2659 }
2660 }
2661 Ok(())
2662}
2663
2664#[context("Removing boot directory content")]
2665fn clean_boot_directories(rootfs: &Dir, rootfs_path: &Utf8Path, is_ostree: bool) -> Result<()> {
2666 let bootdir =
2667 crate::utils::open_dir_remount_rw(rootfs, BOOT.into()).context("Opening /boot")?;
2668
2669 if ARCH_USES_EFI {
2670 crate::bootloader::mount_esp_part(&rootfs, &rootfs_path, is_ostree)?;
2673 }
2674
2675 remove_all_except_loader_dirs(&bootdir, is_ostree).context("Emptying /boot")?;
2677
2678 if ARCH_USES_EFI {
2679 if let Some(efidir) = bootdir
2680 .open_dir_optional(crate::bootloader::EFI_DIR)
2681 .context("Opening /boot/efi")?
2682 {
2683 clean_esp_bootloader_dirs(&efidir)?;
2684 }
2685 }
2686
2687 Ok(())
2688}
2689
2690struct RootMountInfo {
2691 mount_spec: String,
2692 kargs: Vec<String>,
2693}
2694
2695fn find_root_args_to_inherit(
2698 cmdline: &bytes::Cmdline,
2699 root_info: &Filesystem,
2700) -> Result<RootMountInfo> {
2701 let root = cmdline
2703 .find_utf8("root")?
2704 .and_then(|p| p.value().map(|p| p.to_string()));
2705 let (mount_spec, kargs) = if let Some(root) = root {
2706 let rootflags = cmdline.find(ROOTFLAGS_KEY);
2707 let inherit_kargs = cmdline.find_all_starting_with(INITRD_ARG_PREFIX);
2708 (
2709 root,
2710 rootflags
2711 .into_iter()
2712 .chain(inherit_kargs)
2713 .map(|p| utf8::Parameter::try_from(p).map(|p| p.to_string()))
2714 .collect::<Result<Vec<_>, _>>()?,
2715 )
2716 } else {
2717 let uuid = root_info
2718 .uuid
2719 .as_deref()
2720 .ok_or_else(|| anyhow!("No filesystem uuid found in target root"))?;
2721 (format!("UUID={uuid}"), Vec::new())
2722 };
2723
2724 Ok(RootMountInfo { mount_spec, kargs })
2725}
2726
2727fn warn_on_host_root(rootfs_fd: &Dir) -> Result<()> {
2728 const DELAY_SECONDS: u64 = 20;
2730
2731 let host_root_dfd = &Dir::open_ambient_dir("/proc/1/root", cap_std::ambient_authority())?;
2732 let host_root_devstat = rustix::fs::fstatvfs(host_root_dfd)?;
2733 let target_devstat = rustix::fs::fstatvfs(rootfs_fd)?;
2734 if host_root_devstat.f_fsid != target_devstat.f_fsid {
2735 tracing::debug!("Not the host root");
2736 return Ok(());
2737 }
2738 let dashes = "----------------------------";
2739 let timeout = Duration::from_secs(DELAY_SECONDS);
2740 eprintln!("{dashes}");
2741 crate::utils::medium_visibility_warning(
2742 "WARNING: This operation will OVERWRITE THE BOOTED HOST ROOT FILESYSTEM and is NOT REVERSIBLE.",
2743 );
2744 eprintln!("Waiting {timeout:?} to continue; interrupt (Control-C) to cancel.");
2745 eprintln!("{dashes}");
2746
2747 let bar = indicatif::ProgressBar::new_spinner();
2748 bar.enable_steady_tick(Duration::from_millis(100));
2749 std::thread::sleep(timeout);
2750 bar.finish();
2751
2752 Ok(())
2753}
2754
2755pub enum Cleanup {
2756 Skip,
2757 TriggerOnNextBoot,
2758}
2759
2760#[context("Installing to filesystem")]
2762pub(crate) async fn install_to_filesystem(
2763 opts: InstallToFilesystemOpts,
2764 targeting_host_root: bool,
2765 cleanup: Cleanup,
2766) -> Result<()> {
2767 const INSTALL_FILESYSTEM_JOURNAL_ID: &str = "9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3";
2769 let source_image = opts
2770 .source_opts
2771 .source_imgref
2772 .as_ref()
2773 .map(|s| s.as_str())
2774 .unwrap_or("none");
2775 let target_path = opts.filesystem_opts.root_path.as_str();
2776
2777 tracing::info!(
2778 message_id = INSTALL_FILESYSTEM_JOURNAL_ID,
2779 bootc.source_image = source_image,
2780 bootc.target_path = target_path,
2781 bootc.targeting_host_root = if targeting_host_root { "true" } else { "false" },
2782 "Starting filesystem installation from {} to {}",
2783 source_image,
2784 target_path
2785 );
2786
2787 let mut fsopts = opts.filesystem_opts;
2789
2790 if targeting_host_root
2793 && fsopts.root_path.as_str() == ALONGSIDE_ROOT_MOUNT
2794 && !fsopts.root_path.try_exists()?
2795 {
2796 tracing::debug!("Mounting host / to {ALONGSIDE_ROOT_MOUNT}");
2797 std::fs::create_dir(ALONGSIDE_ROOT_MOUNT)?;
2798 bootc_mount::bind_mount_from_pidns(
2799 bootc_mount::PID1,
2800 "/".into(),
2801 ALONGSIDE_ROOT_MOUNT.into(),
2802 true,
2803 )
2804 .context("Mounting host / to {ALONGSIDE_ROOT_MOUNT}")?;
2805 }
2806
2807 let target_root_path = fsopts.root_path.clone();
2808
2809 let target_rootfs_fd =
2811 Dir::open_ambient_dir(&target_root_path, cap_std::ambient_authority())
2812 .with_context(|| format!("Opening target root directory {target_root_path}"))?;
2813
2814 tracing::debug!("Target root filesystem: {target_root_path}");
2815
2816 if let Some(false) = target_rootfs_fd.is_mountpoint(".")? {
2817 anyhow::bail!("Not a mountpoint: {target_root_path}");
2818 }
2819
2820 {
2822 let root_path = &fsopts.root_path;
2823 let st = root_path
2824 .symlink_metadata()
2825 .with_context(|| format!("Querying target filesystem {root_path}"))?;
2826 if !st.is_dir() {
2827 anyhow::bail!("Not a directory: {root_path}");
2828 }
2829 }
2830
2831 let possible_physical_root = fsopts.root_path.join("sysroot");
2834 let possible_ostree_dir = possible_physical_root.join("ostree");
2835 let is_already_ostree = possible_ostree_dir.exists();
2836 if is_already_ostree {
2837 tracing::debug!(
2838 "ostree detected in {possible_ostree_dir}, assuming target is a deployment root and using {possible_physical_root}"
2839 );
2840 fsopts.root_path = possible_physical_root;
2841 };
2842
2843 let rootfs_fd = if is_already_ostree {
2846 let root_path = &fsopts.root_path;
2847 let rootfs_fd = Dir::open_ambient_dir(&fsopts.root_path, cap_std::ambient_authority())
2848 .with_context(|| format!("Opening target root directory {root_path}"))?;
2849
2850 tracing::debug!("Root filesystem: {root_path}");
2851
2852 if let Some(false) = rootfs_fd.is_mountpoint(".")? {
2853 anyhow::bail!("Not a mountpoint: {root_path}");
2854 }
2855 rootfs_fd
2856 } else {
2857 target_rootfs_fd.try_clone()?
2858 };
2859
2860 let inspect = bootc_mount::inspect_filesystem(&fsopts.root_path)?;
2862
2863 let state = prepare_install(
2869 opts.config_opts,
2870 opts.source_opts,
2871 opts.target_opts,
2872 opts.composefs_opts,
2873 Some(inspect.fstype.as_str().try_into()?),
2874 false,
2875 )
2876 .await?;
2877
2878 if !fsopts.acknowledge_destructive {
2880 warn_on_host_root(&target_rootfs_fd)?;
2881 }
2882
2883 match fsopts.replace {
2884 Some(ReplaceMode::Wipe) => {
2885 let rootfs_fd = rootfs_fd.try_clone()?;
2886 println!("Wiping contents of root");
2887 tokio::task::spawn_blocking(move || remove_all_in_dir_no_xdev(&rootfs_fd, true))
2888 .await??;
2889 }
2890 Some(ReplaceMode::Alongside) => {
2891 clean_boot_directories(&target_rootfs_fd, &target_root_path, is_already_ostree)?
2892 }
2893 None => require_empty_rootdir(&rootfs_fd)?,
2894 }
2895
2896 let config_root_mount_spec = state
2901 .install_config
2902 .as_ref()
2903 .and_then(|c| c.root_mount_spec.as_ref());
2904 let root_info = if let Some(s) = fsopts.root_mount_spec.as_ref().or(config_root_mount_spec) {
2905 RootMountInfo {
2906 mount_spec: s.to_string(),
2907 kargs: Vec::new(),
2908 }
2909 } else if targeting_host_root {
2910 let cmdline = bytes::Cmdline::from_proc()?;
2912 find_root_args_to_inherit(&cmdline, &inspect)?
2913 } else {
2914 let uuid = inspect
2917 .uuid
2918 .as_deref()
2919 .ok_or_else(|| anyhow!("No filesystem uuid found in target root"))?;
2920 let kargs = match inspect.fstype.as_str() {
2921 "btrfs" => {
2922 let subvol = crate::utils::find_mount_option(&inspect.options, "subvol");
2923 subvol
2924 .map(|vol| format!("rootflags=subvol={vol}"))
2925 .into_iter()
2926 .collect::<Vec<_>>()
2927 }
2928 _ => Vec::new(),
2929 };
2930 RootMountInfo {
2931 mount_spec: format!("UUID={uuid}"),
2932 kargs,
2933 }
2934 };
2935 tracing::debug!("Root mount: {} {:?}", root_info.mount_spec, root_info.kargs);
2936
2937 let boot_is_mount = {
2938 if let Some(boot_metadata) = target_rootfs_fd.symlink_metadata_optional(BOOT)? {
2939 let root_dev = rootfs_fd.dir_metadata()?.dev();
2940 let boot_dev = boot_metadata.dev();
2941 tracing::debug!("root_dev={root_dev} boot_dev={boot_dev}");
2942 root_dev != boot_dev
2943 } else {
2944 tracing::debug!("No /{BOOT} directory found");
2945 false
2946 }
2947 };
2948 let boot_uuid = if boot_is_mount {
2950 let boot_path = target_root_path.join(BOOT);
2951 tracing::debug!("boot_path={boot_path}");
2952 let u = bootc_mount::inspect_filesystem(&boot_path)
2953 .with_context(|| format!("Inspecting /{BOOT}"))?
2954 .uuid
2955 .ok_or_else(|| anyhow!("No UUID found for /{BOOT}"))?;
2956 Some(u)
2957 } else {
2958 None
2959 };
2960 tracing::debug!("boot UUID: {boot_uuid:?}");
2961
2962 let device_info = {
2965 let dev = bootc_blockdev::list_dev(Utf8Path::new(&inspect.source))?;
2966 tracing::debug!("Target filesystem backing device: {}", dev.path());
2967 dev
2968 };
2969
2970 let rootarg = format!("root={}", root_info.mount_spec);
2971 let config_boot_mount_spec = state
2973 .install_config
2974 .as_ref()
2975 .and_then(|c| c.boot_mount_spec.as_ref());
2976 let mut boot = if let Some(spec) = fsopts.boot_mount_spec.as_ref().or(config_boot_mount_spec) {
2977 if spec.is_empty() {
2980 None
2981 } else {
2982 Some(MountSpec::new(&spec, &format!("/{BOOT}")))
2983 }
2984 } else {
2985 read_boot_fstab_entry(&rootfs_fd)?
2988 .filter(|spec| spec.get_source_uuid().is_some())
2989 .or_else(|| {
2990 boot_uuid
2991 .as_deref()
2992 .map(|boot_uuid| MountSpec::new_uuid_src(boot_uuid, &format!("/{BOOT}")))
2993 })
2994 };
2995 if let Some(boot) = boot.as_mut() {
2998 boot.push_option("ro");
2999 }
3000 let bootarg = boot.as_ref().map(|boot| format!("boot={}", &boot.source));
3003
3004 let mut kargs = if root_info.mount_spec.is_empty() {
3007 Vec::new()
3008 } else {
3009 [rootarg]
3010 .into_iter()
3011 .chain(root_info.kargs)
3012 .collect::<Vec<_>>()
3013 };
3014
3015 kargs.push(RW_KARG.to_string());
3016
3017 if let Some(bootarg) = bootarg {
3018 kargs.push(bootarg);
3019 }
3020
3021 let kargs = Cmdline::from(kargs.join(" "));
3022
3023 let skip_finalize =
3024 matches!(fsopts.replace, Some(ReplaceMode::Alongside)) || fsopts.skip_finalize;
3025 let mut rootfs = RootSetup {
3026 #[cfg(feature = "install-to-disk")]
3027 luks_device: None,
3028 device_info,
3029 physical_root_path: fsopts.root_path,
3030 physical_root: rootfs_fd,
3031 target_root_path: Some(target_root_path.clone()),
3032 rootfs_uuid: inspect.uuid.clone(),
3033 aboot_disk_layout: None,
3034 disk_install: false,
3035 boot,
3036 kargs,
3037 skip_finalize,
3038 };
3039
3040 install_to_filesystem_impl(&state, &mut rootfs, cleanup).await?;
3041
3042 drop(rootfs);
3044
3045 installation_complete();
3046
3047 Ok(())
3048}
3049
3050pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) -> Result<()> {
3051 const INSTALL_EXISTING_ROOT_JOURNAL_ID: &str = "7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1";
3053 let source_image = opts
3054 .source_opts
3055 .source_imgref
3056 .as_ref()
3057 .map(|s| s.as_str())
3058 .unwrap_or("none");
3059 let target_path = opts.root_path.as_str();
3060
3061 tracing::info!(
3062 message_id = INSTALL_EXISTING_ROOT_JOURNAL_ID,
3063 bootc.source_image = source_image,
3064 bootc.target_path = target_path,
3065 bootc.cleanup = if opts.cleanup {
3066 "trigger_on_next_boot"
3067 } else {
3068 "skip"
3069 },
3070 "Starting installation to existing root from {} to {}",
3071 source_image,
3072 target_path
3073 );
3074
3075 let cleanup = match opts.cleanup {
3076 true => Cleanup::TriggerOnNextBoot,
3077 false => Cleanup::Skip,
3078 };
3079
3080 let opts = InstallToFilesystemOpts {
3081 filesystem_opts: InstallTargetFilesystemOpts {
3082 root_path: opts.root_path,
3083 root_mount_spec: None,
3084 boot_mount_spec: None,
3085 replace: opts.replace,
3086 skip_finalize: true,
3087 acknowledge_destructive: opts.acknowledge_destructive,
3088 },
3089 source_opts: opts.source_opts,
3090 target_opts: opts.target_opts,
3091 config_opts: opts.config_opts,
3092 composefs_opts: opts.composefs_opts,
3093 };
3094
3095 install_to_filesystem(opts, true, cleanup).await
3096}
3097
3098fn read_boot_fstab_entry(root: &Dir) -> Result<Option<MountSpec>> {
3100 let fstab_path = "etc/fstab";
3101 let fstab = match root.open_optional(fstab_path)? {
3102 Some(f) => f,
3103 None => return Ok(None),
3104 };
3105
3106 let reader = std::io::BufReader::new(fstab);
3107 for line in std::io::BufRead::lines(reader) {
3108 let line = line?;
3109 let line = line.trim();
3110
3111 if line.is_empty() || line.starts_with('#') {
3113 continue;
3114 }
3115
3116 let spec = MountSpec::from_str(line)?;
3118
3119 if spec.target == format!("/{BOOT}") {
3121 return Ok(Some(spec));
3122 }
3123 }
3124
3125 Ok(None)
3126}
3127
3128pub(crate) async fn install_reset(opts: InstallResetOpts) -> Result<()> {
3129 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
3130 if !opts.experimental {
3131 anyhow::bail!("This command requires --experimental");
3132 }
3133
3134 let prog: ProgressWriter = opts.progress.try_into()?;
3135
3136 let sysroot = &crate::cli::get_storage().await?;
3137 let ostree = sysroot.get_ostree()?;
3138 let repo = &ostree.repo();
3139 let (booted_ostree, _deployments, host) = crate::status::get_status_require_booted(ostree)?;
3140
3141 let stateroots = list_stateroots(ostree)?;
3142 let target_stateroot = if let Some(s) = opts.stateroot {
3143 s
3144 } else {
3145 let now = chrono::Utc::now();
3146 let r = allocate_new_stateroot(&ostree, &stateroots, now)?;
3147 r.name
3148 };
3149
3150 let booted_stateroot = booted_ostree.stateroot();
3151 assert!(booted_stateroot.as_str() != target_stateroot);
3152 let (fetched, spec) = if let Some(target) = opts.target_opts.imageref()? {
3153 let mut new_spec = host.spec;
3154 new_spec.image = Some(target.into());
3155 let fetched = crate::deploy::pull(
3156 repo,
3157 &new_spec.image.as_ref().unwrap(),
3158 None,
3159 opts.quiet,
3160 prog.clone(),
3161 None,
3162 )
3163 .await?;
3164 (fetched, new_spec)
3165 } else {
3166 let imgstate = host
3167 .status
3168 .booted
3169 .map(|b| b.query_image(repo))
3170 .transpose()?
3171 .flatten()
3172 .ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
3173 (Box::new((*imgstate).into()), host.spec)
3174 };
3175 let spec = crate::deploy::RequiredHostSpec::from_spec(&spec)?;
3176
3177 let mut kargs = crate::bootc_kargs::get_kargs_in_root(rootfs, std::env::consts::ARCH)?;
3180
3181 if !opts.no_root_kargs {
3183 let bootcfg = booted_ostree
3184 .deployment
3185 .bootconfig()
3186 .ok_or_else(|| anyhow!("Missing bootcfg for booted deployment"))?;
3187 if let Some(options) = bootcfg.get("options") {
3188 let options_cmdline = Cmdline::from(options.as_str());
3189 let root_kargs = crate::bootc_kargs::root_args_from_cmdline(&options_cmdline);
3190 kargs.extend(&root_kargs);
3191 }
3192 }
3193
3194 if let Some(user_kargs) = opts.karg.as_ref() {
3196 for karg in user_kargs {
3197 kargs.extend(karg);
3198 }
3199 }
3200
3201 let from = MergeState::Reset {
3202 stateroot: target_stateroot.clone(),
3203 kargs,
3204 };
3205 crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone(), false).await?;
3206
3207 if let Some(boot_spec) = read_boot_fstab_entry(rootfs)? {
3209 let staged_deployment = ostree
3210 .staged_deployment()
3211 .ok_or_else(|| anyhow!("No staged deployment found"))?;
3212 let deployment_path = ostree.deployment_dirpath(&staged_deployment);
3213 let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
3214 let deployment_root = sysroot_dir.open_dir(&deployment_path)?;
3215
3216 crate::lsm::atomic_replace_labeled(
3218 &deployment_root,
3219 "etc/fstab",
3220 0o644.into(),
3221 None,
3222 |w| writeln!(w, "{}", boot_spec.to_fstab()).map_err(Into::into),
3223 )?;
3224
3225 tracing::debug!(
3226 "Copied /boot entry to new stateroot: {}",
3227 boot_spec.to_fstab()
3228 );
3229 }
3230
3231 sysroot.update_mtime()?;
3232
3233 if opts.apply {
3234 crate::reboot::reboot()?;
3235 }
3236 Ok(())
3237}
3238
3239pub(crate) async fn install_finalize(target: &Utf8Path) -> Result<()> {
3241 const INSTALL_FINALIZE_JOURNAL_ID: &str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0";
3243
3244 tracing::info!(
3245 message_id = INSTALL_FINALIZE_JOURNAL_ID,
3246 bootc.target_path = target.as_str(),
3247 "Starting installation finalization for target: {}",
3248 target
3249 );
3250
3251 crate::cli::require_root(false)?;
3252 let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(target)));
3253 sysroot.load(gio::Cancellable::NONE)?;
3254 let deployments = sysroot.deployments();
3255 if deployments.is_empty() {
3257 anyhow::bail!("Failed to find deployment in {target}");
3258 }
3259
3260 tracing::info!(
3262 message_id = INSTALL_FINALIZE_JOURNAL_ID,
3263 bootc.target_path = target.as_str(),
3264 "Successfully finalized installation for target: {}",
3265 target
3266 );
3267
3268 Ok(())
3272}
3273
3274#[cfg(test)]
3275mod tests {
3276 use super::*;
3277
3278 #[test]
3279 fn test_composefs_opts_validate() {
3280 let addon = || Some(vec!["addon".to_string()]);
3281 let cases = [
3283 (false, false, None, None, None, true),
3284 (false, false, None, Some(Bootloader::None), None, true),
3285 (false, true, None, None, None, false),
3286 (false, false, addon(), None, None, false),
3287 (false, false, None, None, Some("myroot"), true),
3288 (true, false, None, None, None, true),
3289 (true, true, addon(), Some(Bootloader::Systemd), None, true),
3290 (true, false, None, Some(Bootloader::None), None, true),
3291 (true, false, None, Some(Bootloader::Ukiboot), None, true),
3292 (false, false, None, Some(Bootloader::Ukiboot), None, false),
3293 (true, false, None, None, Some("default"), true),
3294 (true, false, None, None, Some("myroot"), false),
3295 ];
3296 for (composefs_backend, allow_missing_verity, uki_addon, bootloader, stateroot, valid) in
3297 cases
3298 {
3299 let opts = InstallComposefsOpts {
3300 composefs_backend,
3301 allow_missing_verity,
3302 uki_addon_opts: UkiAddonOpts {
3303 scoped: uki_addon,
3304 global: None,
3305 },
3306 };
3307 assert_eq!(
3308 opts.validate(bootloader.as_ref(), stateroot, None).is_ok(),
3309 valid,
3310 "{opts:?} {bootloader:?} {stateroot:?}"
3311 );
3312 }
3313
3314 let opts = InstallComposefsOpts {
3315 composefs_backend: true,
3316 ..Default::default()
3317 };
3318 assert_eq!(
3319 opts.validate(None, Some("myroot"), None)
3320 .unwrap_err()
3321 .to_string(),
3322 "--stateroot myroot is not supported with the composefs backend, which only uses default"
3323 );
3324
3325 assert!(opts.validate(None, None, Some("default")).is_ok());
3326 assert!(
3327 InstallComposefsOpts::default()
3328 .validate(None, None, Some("myroot"))
3329 .is_ok()
3330 );
3331 assert_eq!(
3332 opts.validate(None, None, Some("myroot"))
3333 .unwrap_err()
3334 .to_string(),
3335 "stateroot myroot in the install configuration is not supported with the composefs backend, which only uses default"
3336 );
3337 }
3338
3339 #[test]
3340 fn test_select_bootloader() -> Result<()> {
3341 let root = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
3342 assert_eq!(select_bootloader(None, None, &root)?, Bootloader::Systemd);
3343 assert_eq!(
3344 select_bootloader(None, Some(Bootloader::None), &root)?,
3345 Bootloader::None
3346 );
3347 assert_eq!(
3348 select_bootloader(None, Some(Bootloader::Ukiboot), &root)?,
3349 Bootloader::Ukiboot
3350 );
3351 assert_eq!(
3352 select_bootloader(Some(Bootloader::Grub), Some(Bootloader::None), &root)?,
3353 Bootloader::Grub
3354 );
3355 Ok(())
3356 }
3357
3358 #[test]
3359 #[cfg(feature = "install-to-disk")]
3360 fn install_opts_serializable() {
3361 let c: InstallToDiskOpts = serde_json::from_value(serde_json::json!({
3362 "device": "/dev/vda"
3363 }))
3364 .unwrap();
3365 assert_eq!(c.block_opts.device, "/dev/vda");
3366 }
3367
3368 #[test]
3369 fn test_composefs_opts_validate_bound_images() -> Result<()> {
3370 use crate::boundimage::BOUND_IMAGE_DIR;
3371 let unbound = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3372 unbound.create_dir_all(BOUND_IMAGE_DIR)?;
3373 let bound = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3374 bound.create_dir_all("usr/share/containers/systemd")?;
3375 bound.write(
3376 "usr/share/containers/systemd/app.image",
3377 "[Image]\nImage=quay.io/example/app:latest\n",
3378 )?;
3379 bound.create_dir_all(BOUND_IMAGE_DIR)?;
3380 bound.symlink_contents(
3381 "/usr/share/containers/systemd/app.image",
3382 format!("{BOUND_IMAGE_DIR}/app.image"),
3383 )?;
3384
3385 let cases = [
3387 (false, BoundImagesOpt::Stored, true, true),
3388 (true, BoundImagesOpt::Stored, false, true),
3389 (true, BoundImagesOpt::Stored, true, false),
3390 (true, BoundImagesOpt::Pull, true, false),
3391 (true, BoundImagesOpt::Skip, true, true),
3392 ];
3393 for (composefs_backend, bound_images, binds, valid) in cases {
3394 let opts = InstallComposefsOpts {
3395 composefs_backend,
3396 ..Default::default()
3397 };
3398 let root = if binds { &bound } else { &unbound };
3399 assert_eq!(
3400 opts.validate_bound_images(bound_images, root).is_ok(),
3401 valid,
3402 "{opts:?} {bound_images:?} {binds}"
3403 );
3404 }
3405
3406 let opts = InstallComposefsOpts {
3407 composefs_backend: true,
3408 ..Default::default()
3409 };
3410 assert_eq!(
3411 opts.validate_bound_images(BoundImagesOpt::Stored, &bound)
3412 .unwrap_err()
3413 .to_string(),
3414 "Logically bound images are not supported with the composefs backend (found quay.io/example/app:latest); use --bound-images skip to install without them"
3415 );
3416 Ok(())
3417 }
3418
3419 #[test]
3420 fn source_fetch_reference_uses_config_id_for_host_images() {
3421 let cases = [
3422 (
3423 "containers-storage:localhost/os:latest",
3424 Some("sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"),
3425 "containers-storage:sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
3426 ),
3427 (
3428 "docker://quay.io/example/os:latest",
3429 None,
3430 "docker://quay.io/example/os:latest",
3431 ),
3432 ("oci:/tmp/image", None, "oci:/tmp/image"),
3433 ];
3434 for (original, config_id, expected) in cases {
3435 let original = original.parse().unwrap();
3436 let fetched = composefs_fetch_reference(&original, config_id);
3437 assert_eq!(fetched.to_string(), expected);
3438 }
3439 }
3440
3441 #[test]
3442 fn test_mountspec() {
3443 let mut ms = MountSpec::new("/dev/vda4", "/boot");
3444 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto defaults 0 0");
3445 ms.push_option("ro");
3446 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto ro 0 0");
3447 ms.push_option("relatime");
3448 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto ro,relatime 0 0");
3449 }
3450
3451 #[test]
3452 fn test_gather_root_args() {
3453 let inspect = Filesystem {
3455 source: "/dev/vda4".into(),
3456 target: "/".into(),
3457 fstype: "xfs".into(),
3458 maj_min: "252:4".into(),
3459 options: "rw".into(),
3460 uuid: Some("965eb3c7-5a3f-470d-aaa2-1bcf04334bc6".into()),
3461 children: None,
3462 };
3463 let kargs = bytes::Cmdline::from("");
3464 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
3465 assert_eq!(r.mount_spec, "UUID=965eb3c7-5a3f-470d-aaa2-1bcf04334bc6");
3466
3467 let kargs = bytes::Cmdline::from(
3468 "root=/dev/mapper/root rw someother=karg rd.lvm.lv=root systemd.debug=1",
3469 );
3470
3471 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
3473 assert_eq!(r.mount_spec, "/dev/mapper/root");
3474 assert_eq!(r.kargs.len(), 1);
3475 assert_eq!(r.kargs[0], "rd.lvm.lv=root");
3476
3477 let kargs = bytes::Cmdline::from(
3479 b"root=/dev/mapper/root rw non-utf8=\xff rd.lvm.lv=root systemd.debug=1",
3480 );
3481 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
3482 assert_eq!(r.mount_spec, "/dev/mapper/root");
3483 assert_eq!(r.kargs.len(), 1);
3484 assert_eq!(r.kargs[0], "rd.lvm.lv=root");
3485
3486 let kargs = bytes::Cmdline::from(
3488 b"root=/dev/mapper/ro\xffot rw non-utf8=\xff rd.lvm.lv=root systemd.debug=1",
3489 );
3490 let r = find_root_args_to_inherit(&kargs, &inspect);
3491 assert!(r.is_err());
3492
3493 let kargs = bytes::Cmdline::from(
3495 b"root=/dev/mapper/root rw non-utf8=\xff rd.lvm.lv=ro\xffot systemd.debug=1",
3496 );
3497 let r = find_root_args_to_inherit(&kargs, &inspect);
3498 assert!(r.is_err());
3499 }
3500
3501 #[test]
3504 fn test_remove_all_noxdev() -> Result<()> {
3505 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3506
3507 td.create_dir_all("foo/bar/baz")?;
3508 td.write("foo/bar/baz/test", b"sometest")?;
3509 td.symlink_contents("/absolute-nonexistent-link", "somelink")?;
3510 td.write("toptestfile", b"othertestcontents")?;
3511
3512 remove_all_in_dir_no_xdev(&td, true).unwrap();
3513
3514 assert_eq!(td.entries()?.count(), 0);
3515
3516 Ok(())
3517 }
3518
3519 #[test]
3520 fn test_clean_esp_bootloader_dirs() -> Result<()> {
3521 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3522
3523 td.create_dir_all("EFI/BOOT")?;
3524 td.write("EFI/BOOT/BOOTAA64.EFI", b"shim")?;
3525 td.create_dir_all("loader/entries")?;
3526 td.write("loader/entries/foo.conf", b"entry")?;
3527 td.create_dir_all("m1n1")?;
3529 td.write("m1n1/boot.bin", b"m1n1")?;
3530 td.write("ubootefi.var", b"efivars")?;
3531
3532 clean_esp_bootloader_dirs(&td)?;
3533 assert!(!td.exists("EFI"));
3534 assert!(!td.exists("loader"));
3535 assert_eq!(td.read("m1n1/boot.bin")?, b"m1n1");
3536 assert_eq!(td.read("ubootefi.var")?, b"efivars");
3537
3538 clean_esp_bootloader_dirs(&td)?;
3540 assert_eq!(td.entries()?.count(), 2);
3541
3542 Ok(())
3543 }
3544
3545 #[test]
3546 fn test_read_boot_fstab_entry() -> Result<()> {
3547 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3548
3549 assert!(read_boot_fstab_entry(&td)?.is_none());
3551
3552 td.create_dir("etc")?;
3554 td.write("etc/fstab", "UUID=test-uuid / ext4 defaults 0 0\n")?;
3555 assert!(read_boot_fstab_entry(&td)?.is_none());
3556
3557 let fstab_content = "\
3559# /etc/fstab
3560UUID=root-uuid / ext4 defaults 0 0
3561UUID=boot-uuid /boot ext4 ro 0 0
3562UUID=home-uuid /home ext4 defaults 0 0
3563";
3564 td.write("etc/fstab", fstab_content)?;
3565 let boot_spec = read_boot_fstab_entry(&td)?.unwrap();
3566 assert_eq!(boot_spec.source, "UUID=boot-uuid");
3567 assert_eq!(boot_spec.target, "/boot");
3568 assert_eq!(boot_spec.fstype, "ext4");
3569 assert_eq!(boot_spec.options, Some("ro".to_string()));
3570
3571 let fstab_content = "\
3573# /etc/fstab
3574# Created by anaconda
3575UUID=root-uuid / ext4 defaults 0 0
3576# Boot partition
3577UUID=boot-uuid /boot ext4 defaults 0 0
3578";
3579 td.write("etc/fstab", fstab_content)?;
3580 let boot_spec = read_boot_fstab_entry(&td)?.unwrap();
3581 assert_eq!(boot_spec.source, "UUID=boot-uuid");
3582 assert_eq!(boot_spec.target, "/boot");
3583
3584 Ok(())
3585 }
3586
3587 #[test]
3588 fn test_require_dir_contains_only_mounts() -> Result<()> {
3589 {
3591 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3592 td.create_dir("empty")?;
3593 assert!(require_dir_contains_only_mounts(&td, "empty").is_err());
3594 }
3595
3596 {
3598 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3599 td.create_dir_all("var/lost+found")?;
3600 assert!(require_dir_contains_only_mounts(&td, "var").is_ok());
3601 }
3602
3603 {
3605 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3606 td.create_dir("var")?;
3607 td.write("var/test.txt", b"content")?;
3608 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
3609 }
3610
3611 {
3613 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3614 td.create_dir_all("var/lib/containers")?;
3615 td.write("var/lib/containers/storage.db", b"data")?;
3616 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
3617 }
3618
3619 {
3621 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3622 td.create_dir_all("boot/grub2")?;
3623 td.write("boot/grub2/grub.cfg", b"config")?;
3624 assert!(require_dir_contains_only_mounts(&td, "boot").is_err());
3625 }
3626
3627 {
3629 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3630 td.create_dir_all("var/lib/containers")?;
3631 td.create_dir_all("var/log/journal")?;
3632 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
3633 }
3634
3635 {
3637 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3638 td.create_dir_all("var/lost+found")?;
3639 td.write("var/data.txt", b"content")?;
3640 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
3641 }
3642
3643 {
3645 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3646 td.create_dir("var")?;
3647 td.symlink_contents("../usr/lib", "var/lib")?;
3648 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
3649 }
3650
3651 {
3653 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
3654 td.create_dir_all("var/lib/containers/storage/overlay")?;
3655 td.write("var/lib/containers/storage/overlay/file.txt", b"data")?;
3656 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
3657 }
3658
3659 Ok(())
3660 }
3661
3662 #[test]
3663 fn test_delete_kargs() -> Result<()> {
3664 let mut cmdline = Cmdline::from("console=tty0 quiet debug nosmt foo=bar foo=baz bar=baz");
3665
3666 let deletions = vec!["foo=bar", "bar", "debug"];
3667
3668 delete_kargs(&mut cmdline, &deletions);
3669
3670 let result = cmdline.to_string();
3671 assert!(!result.contains("foo=bar"));
3672 assert!(!result.contains("bar"));
3673 assert!(!result.contains("debug"));
3674 assert!(result.contains("foo=baz"));
3675
3676 Ok(())
3677 }
3678}