1mod aleph;
10#[cfg(feature = "install-to-disk")]
11pub(crate) mod baseline;
12pub(crate) mod completion;
13pub(crate) mod config;
14mod osbuild;
15pub(crate) mod osconfig;
16
17use std::collections::HashMap;
18use std::io::Write;
19use std::os::fd::{AsFd, AsRawFd};
20use std::os::unix::process::CommandExt;
21use std::path::Path;
22use std::process;
23use std::process::Command;
24use std::str::FromStr;
25use std::sync::Arc;
26use std::time::Duration;
27
28use aleph::InstallAleph;
29use anyhow::{anyhow, ensure, Context, Result};
30use bootc_kernel_cmdline::utf8::{Cmdline, CmdlineOwned};
31use bootc_utils::CommandRunExt;
32use camino::Utf8Path;
33use camino::Utf8PathBuf;
34use canon_json::CanonJsonSerialize;
35use cap_std::fs::{Dir, MetadataExt};
36use cap_std_ext::cap_std;
37use cap_std_ext::cap_std::fs::FileType;
38use cap_std_ext::cap_std::fs_utf8::DirEntry as DirEntryUtf8;
39use cap_std_ext::cap_tempfile::TempDir;
40use cap_std_ext::cmdext::CapStdExtCommandExt;
41use cap_std_ext::prelude::CapStdExtDirExt;
42use clap::ValueEnum;
43use fn_error_context::context;
44use ostree::gio;
45use ostree_ext::ostree;
46use ostree_ext::ostree_prepareroot::{ComposefsState, Tristate};
47use ostree_ext::prelude::Cast;
48use ostree_ext::sysroot::{allocate_new_stateroot, list_stateroots, SysrootLock};
49use ostree_ext::{container as ostree_container, ostree_prepareroot};
50#[cfg(feature = "install-to-disk")]
51use rustix::fs::FileTypeExt;
52use rustix::fs::MetadataExt as _;
53use serde::{Deserialize, Serialize};
54
55#[cfg(feature = "install-to-disk")]
56use self::baseline::InstallBlockDeviceOpts;
57use crate::bootc_composefs::{boot::setup_composefs_boot, repo::initialize_composefs_repository};
58use crate::boundimage::{BoundImage, ResolvedBoundImage};
59use crate::containerenv::ContainerExecutionInfo;
60use crate::deploy::{
61 prepare_for_pull, pull_from_prepared, MergeState, PreparedImportMeta, PreparedPullResult,
62};
63use crate::lsm;
64use crate::progress_jsonl::ProgressWriter;
65use crate::spec::{Bootloader, ImageReference};
66use crate::store::Storage;
67use crate::task::Task;
68use crate::utils::sigpolicy_from_opt;
69use bootc_kernel_cmdline::{bytes, utf8, INITRD_ARG_PREFIX, ROOTFLAGS};
70use bootc_mount::Filesystem;
71use composefs::fsverity::FsVerityHashValue;
72
73pub(crate) const BOOT: &str = "boot";
75#[cfg(feature = "install-to-disk")]
77const RUN_BOOTC: &str = "/run/bootc";
78const ALONGSIDE_ROOT_MOUNT: &str = "/target";
80pub(crate) const DESTRUCTIVE_CLEANUP: &str = "etc/bootc-destructive-cleanup";
82const LOST_AND_FOUND: &str = "lost+found";
84const OSTREE_COMPOSEFS_SUPER: &str = ".ostree.cfs";
86const SELINUXFS: &str = "/sys/fs/selinux";
88pub(crate) const EFIVARFS: &str = "/sys/firmware/efi/efivars";
90pub(crate) const ARCH_USES_EFI: bool = cfg!(any(target_arch = "x86_64", target_arch = "aarch64"));
91
92pub(crate) const EFI_LOADER_INFO: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
93
94const DEFAULT_REPO_CONFIG: &[(&str, &str)] = &[
95 ("sysroot.bootloader", "none"),
97 ("sysroot.bootprefix", "true"),
100 ("sysroot.readonly", "true"),
101];
102
103pub(crate) const RW_KARG: &str = "rw";
105
106#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107pub(crate) struct InstallTargetOpts {
108 #[clap(long, default_value = "registry")]
112 #[serde(default)]
113 pub(crate) target_transport: String,
114
115 #[clap(long)]
117 pub(crate) target_imgref: Option<String>,
118
119 #[clap(long, hide = true)]
129 #[serde(default)]
130 pub(crate) target_no_signature_verification: bool,
131
132 #[clap(long)]
136 #[serde(default)]
137 pub(crate) enforce_container_sigpolicy: bool,
138
139 #[clap(long)]
142 #[serde(default)]
143 pub(crate) run_fetch_check: bool,
144
145 #[clap(long)]
148 #[serde(default)]
149 pub(crate) skip_fetch_check: bool,
150
151 #[clap(long = "experimental-unified-storage", hide = true)]
157 #[serde(default)]
158 pub(crate) unified_storage_exp: bool,
159}
160
161#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
162pub(crate) struct InstallSourceOpts {
163 #[clap(long)]
170 pub(crate) source_imgref: Option<String>,
171}
172
173#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
174#[serde(rename_all = "kebab-case")]
175pub(crate) enum BoundImagesOpt {
176 #[default]
178 Stored,
179 #[clap(hide = true)]
180 Skip,
182 Pull,
186}
187
188impl std::fmt::Display for BoundImagesOpt {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 self.to_possible_value().unwrap().get_name().fmt(f)
191 }
192}
193
194#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
195pub(crate) struct InstallConfigOpts {
196 #[clap(long)]
201 #[serde(default)]
202 pub(crate) disable_selinux: bool,
203
204 #[clap(long)]
208 pub(crate) karg: Option<Vec<CmdlineOwned>>,
209
210 #[clap(long)]
218 root_ssh_authorized_keys: Option<Utf8PathBuf>,
219
220 #[clap(long)]
226 #[serde(default)]
227 pub(crate) generic_image: bool,
228
229 #[clap(long)]
231 #[serde(default)]
232 #[arg(default_value_t)]
233 pub(crate) bound_images: BoundImagesOpt,
234
235 #[clap(long)]
237 pub(crate) stateroot: Option<String>,
238}
239
240#[derive(Debug, Default, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
241pub(crate) struct InstallComposefsOpts {
242 #[clap(long, default_value_t)]
244 #[serde(default)]
245 pub(crate) composefs_backend: bool,
246
247 #[clap(long, default_value_t)]
249 #[serde(default)]
250 pub(crate) insecure: bool,
251
252 #[clap(long)]
254 #[serde(default)]
255 pub(crate) bootloader: Option<Bootloader>,
256
257 #[clap(long)]
260 #[serde(default)]
261 pub(crate) uki_addon: Option<Vec<String>>,
262}
263
264#[cfg(feature = "install-to-disk")]
265#[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
266pub(crate) struct InstallToDiskOpts {
267 #[clap(flatten)]
268 #[serde(flatten)]
269 pub(crate) block_opts: InstallBlockDeviceOpts,
270
271 #[clap(flatten)]
272 #[serde(flatten)]
273 pub(crate) source_opts: InstallSourceOpts,
274
275 #[clap(flatten)]
276 #[serde(flatten)]
277 pub(crate) target_opts: InstallTargetOpts,
278
279 #[clap(flatten)]
280 #[serde(flatten)]
281 pub(crate) config_opts: InstallConfigOpts,
282
283 #[clap(long)]
285 #[serde(default)]
286 pub(crate) via_loopback: bool,
287
288 #[clap(flatten)]
289 #[serde(flatten)]
290 pub(crate) composefs_opts: InstallComposefsOpts,
291}
292
293#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
294#[serde(rename_all = "kebab-case")]
295pub(crate) enum ReplaceMode {
296 Wipe,
299 Alongside,
307}
308
309impl std::fmt::Display for ReplaceMode {
310 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311 self.to_possible_value().unwrap().get_name().fmt(f)
312 }
313}
314
315#[derive(Debug, Clone, clap::Args, PartialEq, Eq)]
317pub(crate) struct InstallTargetFilesystemOpts {
318 pub(crate) root_path: Utf8PathBuf,
323
324 #[clap(long)]
328 pub(crate) root_mount_spec: Option<String>,
329
330 #[clap(long)]
335 pub(crate) boot_mount_spec: Option<String>,
336
337 #[clap(long)]
340 pub(crate) replace: Option<ReplaceMode>,
341
342 #[clap(long)]
344 pub(crate) acknowledge_destructive: bool,
345
346 #[clap(long)]
350 pub(crate) skip_finalize: bool,
351}
352
353#[derive(Debug, Clone, clap::Parser, PartialEq, Eq)]
354pub(crate) struct InstallToFilesystemOpts {
355 #[clap(flatten)]
356 pub(crate) filesystem_opts: InstallTargetFilesystemOpts,
357
358 #[clap(flatten)]
359 pub(crate) source_opts: InstallSourceOpts,
360
361 #[clap(flatten)]
362 pub(crate) target_opts: InstallTargetOpts,
363
364 #[clap(flatten)]
365 pub(crate) config_opts: InstallConfigOpts,
366
367 #[clap(flatten)]
368 pub(crate) composefs_opts: InstallComposefsOpts,
369}
370
371#[derive(Debug, Clone, clap::Parser, PartialEq, Eq)]
372pub(crate) struct InstallToExistingRootOpts {
373 #[clap(long, default_value = "alongside")]
375 pub(crate) replace: Option<ReplaceMode>,
376
377 #[clap(flatten)]
378 pub(crate) source_opts: InstallSourceOpts,
379
380 #[clap(flatten)]
381 pub(crate) target_opts: InstallTargetOpts,
382
383 #[clap(flatten)]
384 pub(crate) config_opts: InstallConfigOpts,
385
386 #[clap(long)]
388 pub(crate) acknowledge_destructive: bool,
389
390 #[clap(long)]
393 pub(crate) cleanup: bool,
394
395 #[clap(default_value = ALONGSIDE_ROOT_MOUNT)]
399 pub(crate) root_path: Utf8PathBuf,
400
401 #[clap(flatten)]
402 pub(crate) composefs_opts: InstallComposefsOpts,
403}
404
405#[derive(Debug, clap::Parser, PartialEq, Eq)]
406pub(crate) struct InstallResetOpts {
407 #[clap(long)]
409 pub(crate) experimental: bool,
410
411 #[clap(flatten)]
412 pub(crate) source_opts: InstallSourceOpts,
413
414 #[clap(flatten)]
415 pub(crate) target_opts: InstallTargetOpts,
416
417 #[clap(long)]
421 pub(crate) stateroot: Option<String>,
422
423 #[clap(long)]
425 pub(crate) quiet: bool,
426
427 #[clap(flatten)]
428 pub(crate) progress: crate::cli::ProgressOptions,
429
430 #[clap(long)]
436 pub(crate) apply: bool,
437
438 #[clap(long)]
440 no_root_kargs: bool,
441
442 #[clap(long)]
446 karg: Option<Vec<CmdlineOwned>>,
447}
448
449#[derive(Debug, clap::Parser, PartialEq, Eq)]
450pub(crate) struct InstallPrintConfigurationOpts {
451 #[clap(long)]
455 pub(crate) all: bool,
456}
457
458#[derive(Debug, Clone)]
460pub(crate) struct SourceInfo {
461 pub(crate) imageref: ostree_container::ImageReference,
463 pub(crate) digest: Option<String>,
465 pub(crate) selinux: bool,
467 pub(crate) in_host_mountns: bool,
469}
470
471#[derive(Debug)]
473pub(crate) struct State {
474 pub(crate) source: SourceInfo,
475 pub(crate) selinux_state: SELinuxFinalState,
477 #[allow(dead_code)]
478 pub(crate) config_opts: InstallConfigOpts,
479 pub(crate) target_opts: InstallTargetOpts,
480 pub(crate) target_imgref: ostree_container::OstreeImageReference,
481 #[allow(dead_code)]
482 pub(crate) prepareroot_config: HashMap<String, String>,
483 pub(crate) install_config: Option<config::InstallConfiguration>,
484 pub(crate) root_ssh_authorized_keys: Option<String>,
486 #[allow(dead_code)]
487 pub(crate) host_is_container: bool,
488 pub(crate) container_root: Dir,
490 pub(crate) tempdir: TempDir,
491
492 #[allow(dead_code)]
494 pub(crate) composefs_required: bool,
495
496 pub(crate) composefs_options: InstallComposefsOpts,
498}
499
500#[derive(Debug)]
502pub(crate) struct PostFetchState {
503 pub(crate) detected_bootloader: crate::spec::Bootloader,
505}
506
507impl InstallTargetOpts {
508 pub(crate) fn imageref(&self) -> Result<Option<ostree_container::OstreeImageReference>> {
509 let Some(target_imgname) = self.target_imgref.as_deref() else {
510 return Ok(None);
511 };
512 let target_transport =
513 ostree_container::Transport::try_from(self.target_transport.as_str())?;
514 let target_imgref = ostree_container::OstreeImageReference {
515 sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
516 imgref: ostree_container::ImageReference {
517 transport: target_transport,
518 name: target_imgname.to_string(),
519 },
520 };
521 Ok(Some(target_imgref))
522 }
523}
524
525impl State {
526 #[context("Loading SELinux policy")]
527 pub(crate) fn load_policy(&self) -> Result<Option<ostree::SePolicy>> {
528 if !self.selinux_state.enabled() {
529 return Ok(None);
530 }
531 let r = lsm::new_sepolicy_at(&self.container_root)?
533 .ok_or_else(|| anyhow::anyhow!("SELinux enabled, but no policy found in root"))?;
534 tracing::debug!("Loaded SELinux policy: {}", r.csum().unwrap());
536 Ok(Some(r))
537 }
538
539 #[context("Finalizing state")]
540 #[allow(dead_code)]
541 pub(crate) fn consume(self) -> Result<()> {
542 self.tempdir.close()?;
543 if let SELinuxFinalState::Enabled(Some(guard)) = self.selinux_state {
545 guard.consume()?;
546 }
547 Ok(())
548 }
549
550 pub(crate) fn require_no_kargs_for_uki(&self) -> Result<()> {
552 if self
553 .config_opts
554 .karg
555 .as_ref()
556 .map(|v| !v.is_empty())
557 .unwrap_or_default()
558 {
559 anyhow::bail!("Cannot use externally specified kernel arguments with UKI");
560 }
561 Ok(())
562 }
563
564 fn stateroot(&self) -> &str {
565 self.config_opts
566 .stateroot
567 .as_deref()
568 .unwrap_or(ostree_ext::container::deploy::STATEROOT_DEFAULT)
569 }
570}
571
572#[derive(Debug, Clone)]
583pub(crate) struct MountSpec {
584 pub(crate) source: String,
585 pub(crate) target: String,
586 pub(crate) fstype: String,
587 pub(crate) options: Option<String>,
588}
589
590impl MountSpec {
591 const AUTO: &'static str = "auto";
592
593 pub(crate) fn new(src: &str, target: &str) -> Self {
594 MountSpec {
595 source: src.to_string(),
596 target: target.to_string(),
597 fstype: Self::AUTO.to_string(),
598 options: None,
599 }
600 }
601
602 pub(crate) fn new_uuid_src(uuid: &str, target: &str) -> Self {
604 Self::new(&format!("UUID={uuid}"), target)
605 }
606
607 pub(crate) fn get_source_uuid(&self) -> Option<&str> {
608 if let Some((t, rest)) = self.source.split_once('=') {
609 if t.eq_ignore_ascii_case("uuid") {
610 return Some(rest);
611 }
612 }
613 None
614 }
615
616 pub(crate) fn to_fstab(&self) -> String {
617 let options = self.options.as_deref().unwrap_or("defaults");
618 format!(
619 "{} {} {} {} 0 0",
620 self.source, self.target, self.fstype, options
621 )
622 }
623
624 pub(crate) fn push_option(&mut self, opt: &str) {
626 let options = self.options.get_or_insert_with(Default::default);
627 if !options.is_empty() {
628 options.push(',');
629 }
630 options.push_str(opt);
631 }
632}
633
634impl FromStr for MountSpec {
635 type Err = anyhow::Error;
636
637 fn from_str(s: &str) -> Result<Self> {
638 let mut parts = s.split_ascii_whitespace().fuse();
639 let source = parts.next().unwrap_or_default();
640 if source.is_empty() {
641 tracing::debug!("Empty mount specification");
642 return Ok(Self {
643 source: String::new(),
644 target: String::new(),
645 fstype: Self::AUTO.into(),
646 options: None,
647 });
648 }
649 let target = parts
650 .next()
651 .ok_or_else(|| anyhow!("Missing target in mount specification {s}"))?;
652 let fstype = parts.next().unwrap_or(Self::AUTO);
653 let options = parts.next().map(ToOwned::to_owned);
654 Ok(Self {
655 source: source.to_string(),
656 fstype: fstype.to_string(),
657 target: target.to_string(),
658 options,
659 })
660 }
661}
662
663#[cfg(feature = "install-to-disk")]
664impl InstallToDiskOpts {
665 pub(crate) fn validate(&self) -> Result<()> {
666 if !self.composefs_opts.composefs_backend {
667 if self.composefs_opts.insecure != false {
669 anyhow::bail!("--insecure must not be provided without --composefs-backend");
670 }
671 }
672
673 Ok(())
674 }
675}
676
677impl SourceInfo {
678 #[context("Gathering source info from container env")]
681 pub(crate) fn from_container(
682 root: &Dir,
683 container_info: &ContainerExecutionInfo,
684 ) -> Result<Self> {
685 if !container_info.engine.starts_with("podman") {
686 anyhow::bail!("Currently this command only supports being executed via podman");
687 }
688 if container_info.imageid.is_empty() {
689 anyhow::bail!("Invalid empty imageid");
690 }
691 let imageref = ostree_container::ImageReference {
692 transport: ostree_container::Transport::ContainerStorage,
693 name: container_info.image.clone(),
694 };
695 tracing::debug!("Finding digest for image ID {}", container_info.imageid);
696 let digest = crate::podman::imageid_to_digest(&container_info.imageid)?;
697
698 Self::new(imageref, Some(digest), root, true)
699 }
700
701 #[context("Creating source info from a given imageref")]
702 pub(crate) fn from_imageref(imageref: &str, root: &Dir) -> Result<Self> {
703 let imageref = ostree_container::ImageReference::try_from(imageref)?;
704 Self::new(imageref, None, root, false)
705 }
706
707 fn have_selinux_from_repo(root: &Dir) -> Result<bool> {
708 let cancellable = ostree::gio::Cancellable::NONE;
709
710 let commit = Command::new("ostree")
711 .args(["--repo=/ostree/repo", "rev-parse", "--single"])
712 .run_get_string()?;
713 let repo = ostree::Repo::open_at_dir(root.as_fd(), "ostree/repo")?;
714 let root = repo
715 .read_commit(commit.trim(), cancellable)
716 .context("Reading commit")?
717 .0;
718 let root = root.downcast_ref::<ostree::RepoFile>().unwrap();
719 let xattrs = root.xattrs(cancellable)?;
720 Ok(crate::lsm::xattrs_have_selinux(&xattrs))
721 }
722
723 fn new(
725 imageref: ostree_container::ImageReference,
726 digest: Option<String>,
727 root: &Dir,
728 in_host_mountns: bool,
729 ) -> Result<Self> {
730 let selinux = if Path::new("/ostree/repo").try_exists()? {
731 Self::have_selinux_from_repo(root)?
732 } else {
733 lsm::have_selinux_policy(root)?
734 };
735 Ok(Self {
736 imageref,
737 digest,
738 selinux,
739 in_host_mountns,
740 })
741 }
742}
743
744pub(crate) fn print_configuration(opts: InstallPrintConfigurationOpts) -> Result<()> {
745 let mut install_config = config::load_config()?.unwrap_or_default();
746 if !opts.all {
747 install_config.filter_to_external();
748 }
749 let stdout = std::io::stdout().lock();
750 anyhow::Ok(install_config.to_canon_json_writer(stdout)?)
751}
752
753#[context("Creating ostree deployment")]
754async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result<(Storage, bool)> {
755 let sepolicy = state.load_policy()?;
756 let sepolicy = sepolicy.as_ref();
757 let rootfs_dir = &root_setup.physical_root;
759 let cancellable = gio::Cancellable::NONE;
760
761 let stateroot = state.stateroot();
762
763 let has_ostree = rootfs_dir.try_exists("ostree/repo")?;
764 if !has_ostree {
765 Task::new("Initializing ostree layout", "ostree")
766 .args(["admin", "init-fs", "--modern", "."])
767 .cwd(rootfs_dir)?
768 .run()?;
769 } else {
770 println!("Reusing extant ostree layout");
771
772 let path = ".".into();
773 let _ = crate::utils::open_dir_remount_rw(rootfs_dir, path)
774 .context("remounting target as read-write")?;
775 crate::utils::remove_immutability(rootfs_dir, path)?;
776 }
777
778 crate::lsm::ensure_dir_labeled(rootfs_dir, "", Some("/".into()), 0o755.into(), sepolicy)?;
781
782 if has_ostree && root_setup.boot.is_some() {
785 if let Some(boot) = &root_setup.boot {
786 let source_boot = &boot.source;
787 let target_boot = root_setup.physical_root_path.join(BOOT);
788 tracing::debug!("Mount {source_boot} to {target_boot} on ostree");
789 bootc_mount::mount(source_boot, &target_boot)?;
790 }
791 }
792
793 if rootfs_dir.try_exists("boot")? {
795 crate::lsm::ensure_dir_labeled(rootfs_dir, "boot", None, 0o755.into(), sepolicy)?;
796 }
797
798 for (k, v) in DEFAULT_REPO_CONFIG.iter() {
799 Command::new("ostree")
800 .args(["config", "--repo", "ostree/repo", "set", k, v])
801 .cwd_dir(rootfs_dir.try_clone()?)
802 .run_capture_stderr()?;
803 }
804
805 let sysroot = {
806 let path = format!(
807 "/proc/{}/fd/{}",
808 process::id(),
809 rootfs_dir.as_fd().as_raw_fd()
810 );
811 ostree::Sysroot::new(Some(&gio::File::for_path(path)))
812 };
813 sysroot.load(cancellable)?;
814 let repo = &sysroot.repo();
815
816 let repo_verity_state = ostree_ext::fsverity::is_verity_enabled(&repo)?;
817 let prepare_root_composefs = state
818 .prepareroot_config
819 .get("composefs.enabled")
820 .map(|v| ComposefsState::from_str(&v))
821 .transpose()?
822 .unwrap_or(ComposefsState::default());
823 if prepare_root_composefs.requires_fsverity() || repo_verity_state.desired == Tristate::Enabled
824 {
825 ostree_ext::fsverity::ensure_verity(repo).await?;
826 }
827
828 if let Some(booted) = sysroot.booted_deployment() {
829 if stateroot == booted.stateroot() {
830 anyhow::bail!("Cannot redeploy over booted stateroot {stateroot}");
831 }
832 }
833
834 let sysroot_dir = crate::utils::sysroot_dir(&sysroot)?;
835
836 let stateroot_path = format!("ostree/deploy/{stateroot}");
841 if !sysroot_dir.try_exists(stateroot_path)? {
842 sysroot
843 .init_osname(stateroot, cancellable)
844 .context("initializing stateroot")?;
845 }
846
847 state.tempdir.create_dir("temp-run")?;
848 let temp_run = state.tempdir.open_dir("temp-run")?;
849
850 if let Some(policy) = sepolicy {
853 let ostree_dir = rootfs_dir.open_dir("ostree")?;
854 crate::lsm::ensure_dir_labeled(
855 &ostree_dir,
856 ".",
857 Some("/usr".into()),
858 0o755.into(),
859 Some(policy),
860 )?;
861 }
862
863 sysroot.load(cancellable)?;
864 let sysroot = SysrootLock::new_from_sysroot(&sysroot).await?;
865 let storage = Storage::new_ostree(sysroot, &temp_run)?;
866
867 Ok((storage, has_ostree))
868}
869
870fn check_disk_space(
871 repo_fd: impl AsFd,
872 image_meta: &PreparedImportMeta,
873 imgref: &ImageReference,
874) -> Result<()> {
875 let stat = rustix::fs::fstatvfs(repo_fd)?;
876 let bytes_avail: u64 = stat.f_bsize * stat.f_bavail;
877 tracing::trace!("bytes_avail: {bytes_avail}");
878
879 if image_meta.bytes_to_fetch > bytes_avail {
880 anyhow::bail!(
881 "Insufficient free space for {image} (available: {bytes_avail} required: {bytes_to_fetch})",
882 bytes_avail = ostree_ext::glib::format_size(bytes_avail),
883 bytes_to_fetch = ostree_ext::glib::format_size(image_meta.bytes_to_fetch),
884 image = imgref.image,
885 );
886 }
887
888 Ok(())
889}
890
891#[context("Creating ostree deployment")]
892async fn install_container(
893 state: &State,
894 root_setup: &RootSetup,
895 sysroot: &ostree::Sysroot,
896 storage: &Storage,
897 has_ostree: bool,
898) -> Result<(ostree::Deployment, InstallAleph)> {
899 let sepolicy = state.load_policy()?;
900 let sepolicy = sepolicy.as_ref();
901 let stateroot = state.stateroot();
902
903 let (src_imageref, proxy_cfg) = if !state.source.in_host_mountns {
905 (state.source.imageref.clone(), None)
906 } else {
907 let src_imageref = {
908 let digest = state
910 .source
911 .digest
912 .as_ref()
913 .ok_or_else(|| anyhow::anyhow!("Missing container image digest"))?;
914 let spec = crate::utils::digested_pullspec(&state.source.imageref.name, digest);
915 ostree_container::ImageReference {
916 transport: ostree_container::Transport::ContainerStorage,
917 name: spec,
918 }
919 };
920
921 let proxy_cfg = ostree_container::store::ImageProxyConfig::default();
922 (src_imageref, Some(proxy_cfg))
923 };
924 let src_imageref = ostree_container::OstreeImageReference {
925 sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
928 imgref: src_imageref,
929 };
930
931 let spec_imgref = ImageReference::from(src_imageref.clone());
934 let repo = &sysroot.repo();
935 repo.set_disable_fsync(true);
936
937 let use_unified = state.target_opts.unified_storage_exp;
941
942 let prepared = if use_unified {
943 tracing::info!("Using unified storage path for installation");
944 crate::deploy::prepare_for_pull_unified(
945 repo,
946 &spec_imgref,
947 Some(&state.target_imgref),
948 storage,
949 )
950 .await?
951 } else {
952 prepare_for_pull(repo, &spec_imgref, Some(&state.target_imgref)).await?
953 };
954
955 let pulled_image = match prepared {
956 PreparedPullResult::AlreadyPresent(existing) => existing,
957 PreparedPullResult::Ready(image_meta) => {
958 check_disk_space(root_setup.physical_root.as_fd(), &image_meta, &spec_imgref)?;
959 pull_from_prepared(&spec_imgref, false, ProgressWriter::default(), *image_meta).await?
960 }
961 };
962
963 repo.set_disable_fsync(false);
964
965 let merged_ostree_root = sysroot
968 .repo()
969 .read_commit(pulled_image.ostree_commit.as_str(), gio::Cancellable::NONE)?
970 .0;
971 let kargsd = crate::bootc_kargs::get_kargs_from_ostree_root(
972 &sysroot.repo(),
973 merged_ostree_root.downcast_ref().unwrap(),
974 std::env::consts::ARCH,
975 )?;
976
977 if ostree_ext::bootabletree::commit_has_aboot_img(&merged_ostree_root, None)? {
980 tracing::debug!("Setting bootloader to aboot");
981 Command::new("ostree")
982 .args([
983 "config",
984 "--repo",
985 "ostree/repo",
986 "set",
987 "sysroot.bootloader",
988 "aboot",
989 ])
990 .cwd_dir(root_setup.physical_root.try_clone()?)
991 .run_capture_stderr()
992 .context("Setting bootloader config to aboot")?;
993 sysroot.repo().reload_config(None::<&gio::Cancellable>)?;
994 }
995
996 let install_config_kargs = state.install_config.as_ref().and_then(|c| c.kargs.as_ref());
998
999 let mut kargs = Cmdline::new();
1005
1006 kargs.extend(&root_setup.kargs);
1007
1008 if let Some(install_config_kargs) = install_config_kargs {
1009 for karg in install_config_kargs {
1010 kargs.extend(&Cmdline::from(karg.as_str()));
1011 }
1012 }
1013
1014 kargs.extend(&kargsd);
1015
1016 if let Some(cli_kargs) = state.config_opts.karg.as_ref() {
1017 for karg in cli_kargs {
1018 kargs.extend(karg);
1019 }
1020 }
1021
1022 let kargs_strs: Vec<&str> = kargs.iter_str().collect();
1024
1025 let mut options = ostree_container::deploy::DeployOpts::default();
1026 options.kargs = Some(kargs_strs.as_slice());
1027 options.target_imgref = Some(&state.target_imgref);
1028 options.proxy_cfg = proxy_cfg;
1029 options.skip_completion = true; options.no_clean = has_ostree;
1031 let imgstate = crate::utils::async_task_with_spinner(
1032 "Deploying container image",
1033 ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)),
1034 )
1035 .await?;
1036
1037 let deployment = sysroot
1038 .deployments()
1039 .into_iter()
1040 .next()
1041 .ok_or_else(|| anyhow::anyhow!("Failed to find deployment"))?;
1042 let path = sysroot.deployment_dirpath(&deployment);
1044 let root = root_setup
1045 .physical_root
1046 .open_dir(path.as_str())
1047 .context("Opening deployment dir")?;
1048
1049 if let Some(policy) = sepolicy {
1053 let deployment_root_meta = root.dir_metadata()?;
1054 let deployment_root_devino = (deployment_root_meta.dev(), deployment_root_meta.ino());
1055 for d in ["ostree", "boot"] {
1056 let mut pathbuf = Utf8PathBuf::from(d);
1057 crate::lsm::ensure_dir_labeled_recurse(
1058 &root_setup.physical_root,
1059 &mut pathbuf,
1060 policy,
1061 Some(deployment_root_devino),
1062 )
1063 .with_context(|| format!("Recursive SELinux relabeling of {d}"))?;
1064 }
1065
1066 if let Some(cfs_super) = root.open_optional(OSTREE_COMPOSEFS_SUPER)? {
1067 let label = crate::lsm::require_label(policy, "/usr".into(), 0o644)?;
1068 crate::lsm::set_security_selinux(cfs_super.as_fd(), label.as_bytes())?;
1069 } else {
1070 tracing::warn!("Missing {OSTREE_COMPOSEFS_SUPER}; composefs is not enabled?");
1071 }
1072 }
1073
1074 if let Some(boot) = root_setup.boot.as_ref() {
1078 if !boot.source.is_empty() {
1079 crate::lsm::atomic_replace_labeled(&root, "etc/fstab", 0o644.into(), sepolicy, |w| {
1080 writeln!(w, "{}", boot.to_fstab()).map_err(Into::into)
1081 })?;
1082 }
1083 }
1084
1085 if let Some(contents) = state.root_ssh_authorized_keys.as_deref() {
1086 osconfig::inject_root_ssh_authorized_keys(&root, sepolicy, contents)?;
1087 }
1088
1089 let aleph = InstallAleph::new(&src_imageref, &imgstate, &state.selinux_state)?;
1090 Ok((deployment, aleph))
1091}
1092
1093pub(crate) fn run_in_host_mountns(cmd: &str) -> Result<Command> {
1095 let mut c = Command::new(bootc_utils::reexec::executable_path()?);
1096 c.lifecycle_bind()
1097 .args(["exec-in-host-mount-namespace", cmd]);
1098 Ok(c)
1099}
1100
1101#[context("Re-exec in host mountns")]
1102pub(crate) fn exec_in_host_mountns(args: &[std::ffi::OsString]) -> Result<()> {
1103 let (cmd, args) = args
1104 .split_first()
1105 .ok_or_else(|| anyhow::anyhow!("Missing command"))?;
1106 tracing::trace!("{cmd:?} {args:?}");
1107 let pid1mountns = std::fs::File::open("/proc/1/ns/mnt").context("open pid1 mountns")?;
1108 rustix::thread::move_into_link_name_space(
1109 pid1mountns.as_fd(),
1110 Some(rustix::thread::LinkNameSpaceType::Mount),
1111 )
1112 .context("setns")?;
1113 rustix::process::chdir("/").context("chdir")?;
1114 if !Utf8Path::new("/usr").try_exists().context("/usr")?
1117 && Utf8Path::new("/root/usr")
1118 .try_exists()
1119 .context("/root/usr")?
1120 {
1121 tracing::debug!("Using supermin workaround");
1122 rustix::process::chroot("/root").context("chroot")?;
1123 }
1124 Err(Command::new(cmd).args(args).arg0(bootc_utils::NAME).exec()).context("exec")?
1125}
1126
1127pub(crate) struct RootSetup {
1128 #[cfg(feature = "install-to-disk")]
1129 luks_device: Option<String>,
1130 pub(crate) device_info: bootc_blockdev::PartitionTable,
1131 pub(crate) physical_root_path: Utf8PathBuf,
1134 pub(crate) physical_root: Dir,
1136 pub(crate) target_root_path: Option<Utf8PathBuf>,
1138 pub(crate) rootfs_uuid: Option<String>,
1139 skip_finalize: bool,
1141 boot: Option<MountSpec>,
1142 pub(crate) kargs: CmdlineOwned,
1143}
1144
1145fn require_boot_uuid(spec: &MountSpec) -> Result<&str> {
1146 spec.get_source_uuid()
1147 .ok_or_else(|| anyhow!("/boot is not specified via UUID= (this is currently required)"))
1148}
1149
1150impl RootSetup {
1151 pub(crate) fn get_boot_uuid(&self) -> Result<Option<&str>> {
1154 self.boot.as_ref().map(require_boot_uuid).transpose()
1155 }
1156
1157 #[cfg(feature = "install-to-disk")]
1159 fn into_storage(self) -> (Utf8PathBuf, Option<String>) {
1160 (self.physical_root_path, self.luks_device)
1161 }
1162}
1163
1164#[derive(Debug)]
1165#[allow(dead_code)]
1166pub(crate) enum SELinuxFinalState {
1167 ForceTargetDisabled,
1169 Enabled(Option<crate::lsm::SetEnforceGuard>),
1171 HostDisabled,
1173 Disabled,
1175}
1176
1177impl SELinuxFinalState {
1178 pub(crate) fn enabled(&self) -> bool {
1180 match self {
1181 SELinuxFinalState::ForceTargetDisabled | SELinuxFinalState::Disabled => false,
1182 SELinuxFinalState::Enabled(_) | SELinuxFinalState::HostDisabled => true,
1183 }
1184 }
1185
1186 pub(crate) fn to_aleph(&self) -> &'static str {
1189 match self {
1190 SELinuxFinalState::ForceTargetDisabled => "force-target-disabled",
1191 SELinuxFinalState::Enabled(_) => "enabled",
1192 SELinuxFinalState::HostDisabled => "host-disabled",
1193 SELinuxFinalState::Disabled => "disabled",
1194 }
1195 }
1196}
1197
1198pub(crate) fn reexecute_self_for_selinux_if_needed(
1203 srcdata: &SourceInfo,
1204 override_disable_selinux: bool,
1205) -> Result<SELinuxFinalState> {
1206 if srcdata.selinux {
1208 let host_selinux = crate::lsm::selinux_enabled()?;
1209 tracing::debug!("Target has SELinux, host={host_selinux}");
1210 let r = if override_disable_selinux {
1211 println!("notice: Target has SELinux enabled, overriding to disable");
1212 SELinuxFinalState::ForceTargetDisabled
1213 } else if host_selinux {
1214 setup_sys_mount("selinuxfs", SELINUXFS)?;
1220 let g = crate::lsm::selinux_ensure_install_or_setenforce()?;
1222 SELinuxFinalState::Enabled(g)
1223 } else {
1224 SELinuxFinalState::HostDisabled
1225 };
1226 Ok(r)
1227 } else {
1228 Ok(SELinuxFinalState::Disabled)
1229 }
1230}
1231
1232pub(crate) fn finalize_filesystem(
1235 fsname: &str,
1236 root: &Dir,
1237 path: impl AsRef<Utf8Path>,
1238) -> Result<()> {
1239 let path = path.as_ref();
1240 Task::new(format!("Trimming {fsname}"), "fstrim")
1242 .args(["--quiet-unsupported", "-v", path.as_str()])
1243 .cwd(root)?
1244 .run()?;
1245 Task::new(format!("Finalizing filesystem {fsname}"), "mount")
1248 .cwd(root)?
1249 .args(["-o", "remount,ro", path.as_str()])
1250 .run()?;
1251 for a in ["-f", "-u"] {
1253 Command::new("fsfreeze")
1254 .cwd_dir(root.try_clone()?)
1255 .args([a, path.as_str()])
1256 .run_capture_stderr()?;
1257 }
1258 Ok(())
1259}
1260
1261fn require_host_pidns() -> Result<()> {
1263 if rustix::process::getpid().is_init() {
1264 anyhow::bail!("This command must be run with the podman --pid=host flag")
1265 }
1266 tracing::trace!("OK: we're not pid 1");
1267 Ok(())
1268}
1269
1270fn require_host_userns() -> Result<()> {
1273 let proc1 = "/proc/1";
1274 let pid1_uid = Path::new(proc1)
1275 .metadata()
1276 .with_context(|| format!("Querying {proc1}"))?
1277 .uid();
1278 ensure!(pid1_uid == 0, "{proc1} is owned by {pid1_uid}, not zero; this command must be run in the root user namespace (e.g. not rootless podman)");
1281 tracing::trace!("OK: we're in a matching user namespace with pid1");
1282 Ok(())
1283}
1284
1285pub(crate) fn setup_tmp_mount() -> Result<()> {
1290 let st = rustix::fs::statfs("/tmp")?;
1291 if st.f_type == libc::TMPFS_MAGIC {
1292 tracing::trace!("Already have tmpfs /tmp")
1293 } else {
1294 Command::new("mount")
1297 .args(["tmpfs", "-t", "tmpfs", "/tmp"])
1298 .run_capture_stderr()?;
1299 }
1300 Ok(())
1301}
1302
1303#[context("Ensuring sys mount {fspath} {fstype}")]
1306pub(crate) fn setup_sys_mount(fstype: &str, fspath: &str) -> Result<()> {
1307 tracing::debug!("Setting up sys mounts");
1308 let rootfs = format!("/proc/1/root/{fspath}");
1309 if !Path::new(rootfs.as_str()).try_exists()? {
1311 return Ok(());
1312 }
1313
1314 if std::fs::read_dir(rootfs)?.next().is_none() {
1316 return Ok(());
1317 }
1318
1319 if Path::new(fspath).try_exists()? && std::fs::read_dir(fspath)?.next().is_some() {
1323 return Ok(());
1324 }
1325
1326 Command::new("mount")
1328 .args(["-t", fstype, fstype, fspath])
1329 .run_capture_stderr()?;
1330
1331 Ok(())
1332}
1333
1334#[context("Verifying fetch")]
1336async fn verify_target_fetch(
1337 tmpdir: &Dir,
1338 imgref: &ostree_container::OstreeImageReference,
1339) -> Result<()> {
1340 let tmpdir = &TempDir::new_in(&tmpdir)?;
1341 let tmprepo = &ostree::Repo::create_at_dir(tmpdir.as_fd(), ".", ostree::RepoMode::Bare, None)
1342 .context("Init tmp repo")?;
1343
1344 tracing::trace!("Verifying fetch for {imgref}");
1345 let mut imp =
1346 ostree_container::store::ImageImporter::new(tmprepo, imgref, Default::default()).await?;
1347 use ostree_container::store::PrepareResult;
1348 let prep = match imp.prepare().await? {
1349 PrepareResult::AlreadyPresent(_) => unreachable!(),
1351 PrepareResult::Ready(r) => r,
1352 };
1353 tracing::debug!("Fetched manifest with digest {}", prep.manifest_digest);
1354 Ok(())
1355}
1356
1357async fn prepare_install(
1359 config_opts: InstallConfigOpts,
1360 source_opts: InstallSourceOpts,
1361 target_opts: InstallTargetOpts,
1362 mut composefs_options: InstallComposefsOpts,
1363) -> Result<Arc<State>> {
1364 tracing::trace!("Preparing install");
1365 let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
1366 .context("Opening /")?;
1367
1368 let host_is_container = crate::containerenv::is_container(&rootfs);
1369 let external_source = source_opts.source_imgref.is_some();
1370 let (source, target_rootfs) = match source_opts.source_imgref {
1371 None => {
1372 ensure!(host_is_container, "Either --source-imgref must be defined or this command must be executed inside a podman container.");
1373
1374 crate::cli::require_root(true)?;
1375
1376 require_host_pidns()?;
1377 require_host_userns()?;
1380 let container_info = crate::containerenv::get_container_execution_info(&rootfs)?;
1381 match container_info.rootless.as_deref() {
1383 Some("1") => anyhow::bail!(
1384 "Cannot install from rootless podman; this command must be run as root"
1385 ),
1386 Some(o) => tracing::debug!("rootless={o}"),
1387 None => tracing::debug!(
1389 "notice: Did not find rootless= entry in {}",
1390 crate::containerenv::PATH,
1391 ),
1392 };
1393 tracing::trace!("Read container engine info {:?}", container_info);
1394
1395 let source = SourceInfo::from_container(&rootfs, &container_info)?;
1396 (source, Some(rootfs.try_clone()?))
1397 }
1398 Some(source) => {
1399 crate::cli::require_root(false)?;
1400 let source = SourceInfo::from_imageref(&source, &rootfs)?;
1401 (source, None)
1402 }
1403 };
1404
1405 if target_opts.target_no_signature_verification {
1408 tracing::debug!(
1410 "Use of --target-no-signature-verification flag which is enabled by default"
1411 );
1412 }
1413 let target_sigverify = sigpolicy_from_opt(target_opts.enforce_container_sigpolicy);
1414 let target_imgname = target_opts
1415 .target_imgref
1416 .as_deref()
1417 .unwrap_or(source.imageref.name.as_str());
1418 let target_transport =
1419 ostree_container::Transport::try_from(target_opts.target_transport.as_str())?;
1420 let target_imgref = ostree_container::OstreeImageReference {
1421 sigverify: target_sigverify,
1422 imgref: ostree_container::ImageReference {
1423 transport: target_transport,
1424 name: target_imgname.to_string(),
1425 },
1426 };
1427 tracing::debug!("Target image reference: {target_imgref}");
1428
1429 let composefs_required = if let Some(root) = target_rootfs.as_ref() {
1430 crate::kernel::find_kernel(root)?
1431 .map(|k| k.unified)
1432 .unwrap_or(false)
1433 } else {
1434 false
1435 };
1436
1437 tracing::debug!("Composefs required: {composefs_required}");
1438
1439 if composefs_required {
1440 composefs_options.composefs_backend = true;
1441 }
1442
1443 bootc_mount::ensure_mirrored_host_mount("/dev")?;
1445 bootc_mount::ensure_mirrored_host_mount("/var/lib/containers")?;
1448 bootc_mount::ensure_mirrored_host_mount("/var/tmp")?;
1451 setup_tmp_mount()?;
1453 let tempdir = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
1456 osbuild::adjust_for_bootc_image_builder(&rootfs, &tempdir)?;
1458
1459 if target_opts.run_fetch_check {
1460 verify_target_fetch(&tempdir, &target_imgref).await?;
1461 }
1462
1463 if !external_source && std::env::var_os("BOOTC_SKIP_UNSHARE").is_none() {
1466 super::cli::ensure_self_unshared_mount_namespace()?;
1467 }
1468
1469 setup_sys_mount("efivarfs", EFIVARFS)?;
1470
1471 let selinux_state = reexecute_self_for_selinux_if_needed(&source, config_opts.disable_selinux)?;
1473 tracing::debug!("SELinux state: {selinux_state:?}");
1474
1475 println!("Installing image: {:#}", &target_imgref);
1476 if let Some(digest) = source.digest.as_deref() {
1477 println!("Digest: {digest}");
1478 }
1479
1480 let install_config = config::load_config()?;
1481 if install_config.is_some() {
1482 tracing::debug!("Loaded install configuration");
1483 } else {
1484 tracing::debug!("No install configuration found");
1485 }
1486
1487 let prepareroot_config = {
1489 let kf = ostree_prepareroot::require_config_from_root(&rootfs)?;
1490 let mut r = HashMap::new();
1491 for grp in kf.groups() {
1492 for key in kf.keys(&grp)? {
1493 let key = key.as_str();
1494 let value = kf.value(&grp, key)?;
1495 r.insert(format!("{grp}.{key}"), value.to_string());
1496 }
1497 }
1498 r
1499 };
1500
1501 let root_ssh_authorized_keys = config_opts
1504 .root_ssh_authorized_keys
1505 .as_ref()
1506 .map(|p| std::fs::read_to_string(p).with_context(|| format!("Reading {p}")))
1507 .transpose()?;
1508
1509 let state = Arc::new(State {
1513 selinux_state,
1514 source,
1515 config_opts,
1516 target_opts,
1517 target_imgref,
1518 install_config,
1519 prepareroot_config,
1520 root_ssh_authorized_keys,
1521 container_root: rootfs,
1522 tempdir,
1523 host_is_container,
1524 composefs_required,
1525 composefs_options,
1526 });
1527
1528 Ok(state)
1529}
1530
1531impl PostFetchState {
1532 pub(crate) fn new(state: &State, d: &Dir) -> Result<Self> {
1533 let detected_bootloader = {
1536 if let Some(bootloader) = state.composefs_options.bootloader.clone() {
1537 bootloader
1538 } else {
1539 if crate::bootloader::supports_bootupd(d)? {
1540 crate::spec::Bootloader::Grub
1541 } else {
1542 crate::spec::Bootloader::Systemd
1543 }
1544 }
1545 };
1546 println!("Bootloader: {detected_bootloader}");
1547 let r = Self {
1548 detected_bootloader,
1549 };
1550 Ok(r)
1551 }
1552}
1553
1554async fn install_with_sysroot(
1559 state: &State,
1560 rootfs: &RootSetup,
1561 storage: &Storage,
1562 boot_uuid: &str,
1563 bound_images: BoundImages,
1564 has_ostree: bool,
1565) -> Result<()> {
1566 let ostree = storage.get_ostree()?;
1567 let c_storage = storage.get_ensure_imgstore()?;
1568
1569 let (deployment, aleph) = install_container(state, rootfs, ostree, storage, has_ostree).await?;
1572 aleph.write_to(&rootfs.physical_root)?;
1574
1575 let deployment_path = ostree.deployment_dirpath(&deployment);
1576
1577 let deployment_dir = rootfs
1578 .physical_root
1579 .open_dir(&deployment_path)
1580 .context("Opening deployment dir")?;
1581 let postfetch = PostFetchState::new(state, &deployment_dir)?;
1582
1583 if cfg!(target_arch = "s390x") {
1584 crate::bootloader::install_via_zipl(&rootfs.device_info, boot_uuid)?;
1586 } else {
1587 match postfetch.detected_bootloader {
1588 Bootloader::Grub => {
1589 crate::bootloader::install_via_bootupd(
1590 &rootfs.device_info,
1591 &rootfs
1592 .target_root_path
1593 .clone()
1594 .unwrap_or(rootfs.physical_root_path.clone()),
1595 &state.config_opts,
1596 Some(&deployment_path.as_str()),
1597 )?;
1598 }
1599 Bootloader::Systemd => {
1600 anyhow::bail!("bootupd is required for ostree-based installs");
1601 }
1602 }
1603 }
1604 tracing::debug!("Installed bootloader");
1605
1606 tracing::debug!("Performing post-deployment operations");
1607
1608 match bound_images {
1609 BoundImages::Skip => {}
1610 BoundImages::Resolved(resolved_bound_images) => {
1611 for image in resolved_bound_images {
1613 let image = image.image.as_str();
1614 c_storage.pull_from_host_storage(image).await?;
1615 }
1616 }
1617 BoundImages::Unresolved(bound_images) => {
1618 crate::boundimage::pull_images_impl(c_storage, bound_images)
1619 .await
1620 .context("pulling bound images")?;
1621 }
1622 }
1623
1624 Ok(())
1625}
1626
1627enum BoundImages {
1628 Skip,
1629 Resolved(Vec<ResolvedBoundImage>),
1630 Unresolved(Vec<BoundImage>),
1631}
1632
1633impl BoundImages {
1634 async fn from_state(state: &State) -> Result<Self> {
1635 let bound_images = match state.config_opts.bound_images {
1636 BoundImagesOpt::Skip => BoundImages::Skip,
1637 others => {
1638 let queried_images = crate::boundimage::query_bound_images(&state.container_root)?;
1639 match others {
1640 BoundImagesOpt::Stored => {
1641 let mut r = Vec::with_capacity(queried_images.len());
1643 for image in queried_images {
1644 let resolved = ResolvedBoundImage::from_image(&image).await?;
1645 tracing::debug!("Resolved {}: {}", resolved.image, resolved.digest);
1646 r.push(resolved)
1647 }
1648 BoundImages::Resolved(r)
1649 }
1650 BoundImagesOpt::Pull => {
1651 BoundImages::Unresolved(queried_images)
1653 }
1654 BoundImagesOpt::Skip => anyhow::bail!("unreachable error"),
1655 }
1656 }
1657 };
1658
1659 Ok(bound_images)
1660 }
1661}
1662
1663async fn ostree_install(state: &State, rootfs: &RootSetup, cleanup: Cleanup) -> Result<()> {
1664 let boot_uuid = rootfs
1666 .get_boot_uuid()?
1667 .or(rootfs.rootfs_uuid.as_deref())
1668 .ok_or_else(|| anyhow!("No uuid for boot/root"))?;
1669 tracing::debug!("boot uuid={boot_uuid}");
1670
1671 let bound_images = BoundImages::from_state(state).await?;
1672
1673 {
1676 let (sysroot, has_ostree) = initialize_ostree_root(state, rootfs).await?;
1677
1678 install_with_sysroot(
1679 state,
1680 rootfs,
1681 &sysroot,
1682 &boot_uuid,
1683 bound_images,
1684 has_ostree,
1685 )
1686 .await?;
1687 let ostree = sysroot.get_ostree()?;
1688
1689 if matches!(cleanup, Cleanup::TriggerOnNextBoot) {
1690 let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
1691 tracing::debug!("Writing {DESTRUCTIVE_CLEANUP}");
1692 sysroot_dir.atomic_write(DESTRUCTIVE_CLEANUP, b"")?;
1693 }
1694
1695 };
1698
1699 install_finalize(&rootfs.physical_root_path).await?;
1701
1702 Ok(())
1703}
1704
1705async fn install_to_filesystem_impl(
1706 state: &State,
1707 rootfs: &mut RootSetup,
1708 cleanup: Cleanup,
1709) -> Result<()> {
1710 if matches!(state.selinux_state, SELinuxFinalState::ForceTargetDisabled) {
1711 rootfs.kargs.extend(&Cmdline::from("selinux=0"));
1712 }
1713 let rootfs = &*rootfs;
1715
1716 match &rootfs.device_info.label {
1717 bootc_blockdev::PartitionType::Dos => crate::utils::medium_visibility_warning(
1718 "Installing to `dos` format partitions is not recommended",
1719 ),
1720 bootc_blockdev::PartitionType::Gpt => {
1721 }
1723 bootc_blockdev::PartitionType::Unknown(o) => {
1724 crate::utils::medium_visibility_warning(&format!("Unknown partition label {o}"))
1725 }
1726 }
1727
1728 if state.composefs_options.composefs_backend {
1729 let (id, verity) = initialize_composefs_repository(state, rootfs).await?;
1732 tracing::info!("id: {}, verity: {}", hex::encode(id), verity.to_hex());
1733
1734 setup_composefs_boot(rootfs, state, &hex::encode(id)).await?;
1735 } else {
1736 ostree_install(state, rootfs, cleanup).await?;
1737 }
1738
1739 if !rootfs.skip_finalize {
1741 let bootfs = rootfs.boot.as_ref().map(|_| ("boot", "boot"));
1742 for (fsname, fs) in std::iter::once(("root", ".")).chain(bootfs) {
1743 finalize_filesystem(fsname, &rootfs.physical_root, fs)?;
1744 }
1745 }
1746
1747 Ok(())
1748}
1749
1750fn installation_complete() {
1751 println!("Installation complete!");
1752}
1753
1754#[context("Installing to disk")]
1756#[cfg(feature = "install-to-disk")]
1757pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
1758 opts.validate()?;
1759
1760 const INSTALL_DISK_JOURNAL_ID: &str = "8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2";
1762 let source_image = opts
1763 .source_opts
1764 .source_imgref
1765 .as_ref()
1766 .map(|s| s.as_str())
1767 .unwrap_or("none");
1768 let target_device = opts.block_opts.device.as_str();
1769
1770 tracing::info!(
1771 message_id = INSTALL_DISK_JOURNAL_ID,
1772 bootc.source_image = source_image,
1773 bootc.target_device = target_device,
1774 bootc.via_loopback = if opts.via_loopback { "true" } else { "false" },
1775 "Starting disk installation from {} to {}",
1776 source_image,
1777 target_device
1778 );
1779
1780 let mut block_opts = opts.block_opts;
1781 let target_blockdev_meta = block_opts
1782 .device
1783 .metadata()
1784 .with_context(|| format!("Querying {}", &block_opts.device))?;
1785 if opts.via_loopback {
1786 if !opts.config_opts.generic_image {
1787 crate::utils::medium_visibility_warning(
1788 "Automatically enabling --generic-image when installing via loopback",
1789 );
1790 opts.config_opts.generic_image = true;
1791 }
1792 if !target_blockdev_meta.file_type().is_file() {
1793 anyhow::bail!(
1794 "Not a regular file (to be used via loopback): {}",
1795 block_opts.device
1796 );
1797 }
1798 } else if !target_blockdev_meta.file_type().is_block_device() {
1799 anyhow::bail!("Not a block device: {}", block_opts.device);
1800 }
1801
1802 let state = prepare_install(
1803 opts.config_opts,
1804 opts.source_opts,
1805 opts.target_opts,
1806 opts.composefs_opts,
1807 )
1808 .await?;
1809
1810 let (mut rootfs, loopback) = {
1812 let loopback_dev = if opts.via_loopback {
1813 let loopback_dev =
1814 bootc_blockdev::LoopbackDevice::new(block_opts.device.as_std_path())?;
1815 block_opts.device = loopback_dev.path().into();
1816 Some(loopback_dev)
1817 } else {
1818 None
1819 };
1820
1821 let state = state.clone();
1822 let rootfs = tokio::task::spawn_blocking(move || {
1823 baseline::install_create_rootfs(&state, block_opts)
1824 })
1825 .await??;
1826 (rootfs, loopback_dev)
1827 };
1828
1829 install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip).await?;
1830
1831 let (root_path, luksdev) = rootfs.into_storage();
1833 Task::new_and_run(
1834 "Unmounting filesystems",
1835 "umount",
1836 ["-R", root_path.as_str()],
1837 )?;
1838 if let Some(luksdev) = luksdev.as_deref() {
1839 Task::new_and_run("Closing root LUKS device", "cryptsetup", ["close", luksdev])?;
1840 }
1841
1842 if let Some(loopback_dev) = loopback {
1843 loopback_dev.close()?;
1844 }
1845
1846 if let Some(state) = Arc::into_inner(state) {
1848 state.consume()?;
1849 } else {
1850 tracing::warn!("Failed to consume state Arc");
1852 }
1853
1854 installation_complete();
1855
1856 Ok(())
1857}
1858
1859#[context("Requiring directory contains only mount points")]
1870fn require_dir_contains_only_mounts(parent_fd: &Dir, dir_name: &str) -> Result<()> {
1871 let Some(dir_fd) = parent_fd.open_dir_noxdev(dir_name)? else {
1872 return Ok(());
1874 };
1875
1876 if dir_fd.entries()?.next().is_none() {
1877 anyhow::bail!("Found empty directory: {dir_name}");
1878 }
1879
1880 for entry in dir_fd.entries()? {
1881 let entry = DirEntryUtf8::from_cap_std(entry?);
1882 let entry_name = entry.file_name()?;
1883
1884 if entry_name == LOST_AND_FOUND {
1885 continue;
1886 }
1887
1888 let etype = entry.file_type()?;
1889 if etype == FileType::dir() && dir_fd.open_dir_noxdev(&entry_name)?.is_some() {
1890 require_dir_contains_only_mounts(&dir_fd, &entry_name)?;
1891 } else {
1892 anyhow::bail!("Found entry in {dir_name}: {entry_name}");
1893 }
1894 }
1895
1896 Ok(())
1897}
1898
1899#[context("Verifying empty rootfs")]
1900fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
1901 for e in rootfs_fd.entries()? {
1902 let e = DirEntryUtf8::from_cap_std(e?);
1903 let name = e.file_name()?;
1904 if name == LOST_AND_FOUND {
1905 continue;
1906 }
1907
1908 let etype = e.file_type()?;
1910 if etype == FileType::dir() {
1911 require_dir_contains_only_mounts(rootfs_fd, &name)?;
1912 } else {
1913 anyhow::bail!("Non-empty root filesystem; found {name:?}");
1914 }
1915 }
1916 Ok(())
1917}
1918
1919fn remove_all_in_dir_no_xdev(d: &Dir, mount_err: bool) -> Result<()> {
1923 for entry in d.entries()? {
1924 let entry = entry?;
1925 let name = entry.file_name();
1926 let etype = entry.file_type()?;
1927 if etype == FileType::dir() {
1928 if let Some(subdir) = d.open_dir_noxdev(&name)? {
1929 remove_all_in_dir_no_xdev(&subdir, mount_err)?;
1930 d.remove_dir(&name)?;
1931 } else if mount_err {
1932 anyhow::bail!("Found unexpected mount point {name:?}");
1933 }
1934 } else {
1935 d.remove_file_optional(&name)?;
1936 }
1937 }
1938 anyhow::Ok(())
1939}
1940
1941#[context("Removing boot directory content except loader dir on ostree")]
1942fn remove_all_except_loader_dirs(bootdir: &Dir, is_ostree: bool) -> Result<()> {
1943 let entries = bootdir
1944 .entries()
1945 .context("Reading boot directory entries")?;
1946
1947 for entry in entries {
1948 let entry = entry.context("Reading directory entry")?;
1949 let file_name = entry.file_name();
1950 let file_name = if let Some(n) = file_name.to_str() {
1951 n
1952 } else {
1953 anyhow::bail!("Invalid non-UTF8 filename: {file_name:?} in /boot");
1954 };
1955
1956 if is_ostree && file_name.starts_with("loader") {
1960 continue;
1961 }
1962
1963 let etype = entry.file_type()?;
1964 if etype == FileType::dir() {
1965 if let Some(subdir) = bootdir.open_dir_noxdev(&file_name)? {
1967 remove_all_in_dir_no_xdev(&subdir, false)
1968 .with_context(|| format!("Removing directory contents: {}", file_name))?;
1969 bootdir.remove_dir(&file_name)?;
1970 }
1971 } else {
1972 bootdir
1973 .remove_file_optional(&file_name)
1974 .with_context(|| format!("Removing file: {}", file_name))?;
1975 }
1976 }
1977 Ok(())
1978}
1979
1980#[context("Removing boot directory content")]
1981fn clean_boot_directories(rootfs: &Dir, rootfs_path: &Utf8Path, is_ostree: bool) -> Result<()> {
1982 let bootdir =
1983 crate::utils::open_dir_remount_rw(rootfs, BOOT.into()).context("Opening /boot")?;
1984
1985 if ARCH_USES_EFI {
1986 crate::bootloader::mount_esp_part(&rootfs, &rootfs_path, is_ostree)?;
1989 }
1990
1991 remove_all_except_loader_dirs(&bootdir, is_ostree).context("Emptying /boot")?;
1993
1994 if ARCH_USES_EFI {
1996 if let Some(efidir) = bootdir
1997 .open_dir_optional(crate::bootloader::EFI_DIR)
1998 .context("Opening /boot/efi")?
1999 {
2000 remove_all_in_dir_no_xdev(&efidir, false).context("Emptying EFI system partition")?;
2001 }
2002 }
2003
2004 Ok(())
2005}
2006
2007struct RootMountInfo {
2008 mount_spec: String,
2009 kargs: Vec<String>,
2010}
2011
2012fn find_root_args_to_inherit(
2015 cmdline: &bytes::Cmdline,
2016 root_info: &Filesystem,
2017) -> Result<RootMountInfo> {
2018 let root = cmdline
2020 .find_utf8("root")?
2021 .and_then(|p| p.value().map(|p| p.to_string()));
2022 let (mount_spec, kargs) = if let Some(root) = root {
2023 let rootflags = cmdline.find(ROOTFLAGS);
2024 let inherit_kargs = cmdline.find_all_starting_with(INITRD_ARG_PREFIX);
2025 (
2026 root,
2027 rootflags
2028 .into_iter()
2029 .chain(inherit_kargs)
2030 .map(|p| utf8::Parameter::try_from(p).map(|p| p.to_string()))
2031 .collect::<Result<Vec<_>, _>>()?,
2032 )
2033 } else {
2034 let uuid = root_info
2035 .uuid
2036 .as_deref()
2037 .ok_or_else(|| anyhow!("No filesystem uuid found in target root"))?;
2038 (format!("UUID={uuid}"), Vec::new())
2039 };
2040
2041 Ok(RootMountInfo { mount_spec, kargs })
2042}
2043
2044fn warn_on_host_root(rootfs_fd: &Dir) -> Result<()> {
2045 const DELAY_SECONDS: u64 = 20;
2047
2048 let host_root_dfd = &Dir::open_ambient_dir("/proc/1/root", cap_std::ambient_authority())?;
2049 let host_root_devstat = rustix::fs::fstatvfs(host_root_dfd)?;
2050 let target_devstat = rustix::fs::fstatvfs(rootfs_fd)?;
2051 if host_root_devstat.f_fsid != target_devstat.f_fsid {
2052 tracing::debug!("Not the host root");
2053 return Ok(());
2054 }
2055 let dashes = "----------------------------";
2056 let timeout = Duration::from_secs(DELAY_SECONDS);
2057 eprintln!("{dashes}");
2058 crate::utils::medium_visibility_warning(
2059 "WARNING: This operation will OVERWRITE THE BOOTED HOST ROOT FILESYSTEM and is NOT REVERSIBLE.",
2060 );
2061 eprintln!("Waiting {timeout:?} to continue; interrupt (Control-C) to cancel.");
2062 eprintln!("{dashes}");
2063
2064 let bar = indicatif::ProgressBar::new_spinner();
2065 bar.enable_steady_tick(Duration::from_millis(100));
2066 std::thread::sleep(timeout);
2067 bar.finish();
2068
2069 Ok(())
2070}
2071
2072pub enum Cleanup {
2073 Skip,
2074 TriggerOnNextBoot,
2075}
2076
2077#[context("Installing to filesystem")]
2079pub(crate) async fn install_to_filesystem(
2080 opts: InstallToFilesystemOpts,
2081 targeting_host_root: bool,
2082 cleanup: Cleanup,
2083) -> Result<()> {
2084 const INSTALL_FILESYSTEM_JOURNAL_ID: &str = "9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3";
2086 let source_image = opts
2087 .source_opts
2088 .source_imgref
2089 .as_ref()
2090 .map(|s| s.as_str())
2091 .unwrap_or("none");
2092 let target_path = opts.filesystem_opts.root_path.as_str();
2093
2094 tracing::info!(
2095 message_id = INSTALL_FILESYSTEM_JOURNAL_ID,
2096 bootc.source_image = source_image,
2097 bootc.target_path = target_path,
2098 bootc.targeting_host_root = if targeting_host_root { "true" } else { "false" },
2099 "Starting filesystem installation from {} to {}",
2100 source_image,
2101 target_path
2102 );
2103
2104 let state = prepare_install(
2110 opts.config_opts,
2111 opts.source_opts,
2112 opts.target_opts,
2113 opts.composefs_opts,
2114 )
2115 .await?;
2116
2117 let mut fsopts = opts.filesystem_opts;
2119
2120 if targeting_host_root
2123 && fsopts.root_path.as_str() == ALONGSIDE_ROOT_MOUNT
2124 && !fsopts.root_path.try_exists()?
2125 {
2126 tracing::debug!("Mounting host / to {ALONGSIDE_ROOT_MOUNT}");
2127 std::fs::create_dir(ALONGSIDE_ROOT_MOUNT)?;
2128 bootc_mount::bind_mount_from_pidns(
2129 bootc_mount::PID1,
2130 "/".into(),
2131 ALONGSIDE_ROOT_MOUNT.into(),
2132 true,
2133 )
2134 .context("Mounting host / to {ALONGSIDE_ROOT_MOUNT}")?;
2135 }
2136
2137 let target_root_path = fsopts.root_path.clone();
2138 let target_rootfs_fd =
2140 Dir::open_ambient_dir(&target_root_path, cap_std::ambient_authority())
2141 .with_context(|| format!("Opening target root directory {target_root_path}"))?;
2142
2143 tracing::debug!("Target root filesystem: {target_root_path}");
2144
2145 if let Some(false) = target_rootfs_fd.is_mountpoint(".")? {
2146 anyhow::bail!("Not a mountpoint: {target_root_path}");
2147 }
2148
2149 {
2151 let root_path = &fsopts.root_path;
2152 let st = root_path
2153 .symlink_metadata()
2154 .with_context(|| format!("Querying target filesystem {root_path}"))?;
2155 if !st.is_dir() {
2156 anyhow::bail!("Not a directory: {root_path}");
2157 }
2158 }
2159
2160 if !fsopts.acknowledge_destructive {
2162 warn_on_host_root(&target_rootfs_fd)?;
2163 }
2164
2165 let possible_physical_root = fsopts.root_path.join("sysroot");
2168 let possible_ostree_dir = possible_physical_root.join("ostree");
2169 let is_already_ostree = possible_ostree_dir.exists();
2170 if is_already_ostree {
2171 tracing::debug!(
2172 "ostree detected in {possible_ostree_dir}, assuming target is a deployment root and using {possible_physical_root}"
2173 );
2174 fsopts.root_path = possible_physical_root;
2175 };
2176
2177 let rootfs_fd = if is_already_ostree {
2180 let root_path = &fsopts.root_path;
2181 let rootfs_fd = Dir::open_ambient_dir(&fsopts.root_path, cap_std::ambient_authority())
2182 .with_context(|| format!("Opening target root directory {root_path}"))?;
2183
2184 tracing::debug!("Root filesystem: {root_path}");
2185
2186 if let Some(false) = rootfs_fd.is_mountpoint(".")? {
2187 anyhow::bail!("Not a mountpoint: {root_path}");
2188 }
2189 rootfs_fd
2190 } else {
2191 target_rootfs_fd.try_clone()?
2192 };
2193
2194 match fsopts.replace {
2195 Some(ReplaceMode::Wipe) => {
2196 let rootfs_fd = rootfs_fd.try_clone()?;
2197 println!("Wiping contents of root");
2198 tokio::task::spawn_blocking(move || remove_all_in_dir_no_xdev(&rootfs_fd, true))
2199 .await??;
2200 }
2201 Some(ReplaceMode::Alongside) => {
2202 clean_boot_directories(&target_rootfs_fd, &target_root_path, is_already_ostree)?
2203 }
2204 None => require_empty_rootdir(&rootfs_fd)?,
2205 }
2206
2207 let inspect = bootc_mount::inspect_filesystem(&fsopts.root_path)?;
2209
2210 let root_info = if let Some(s) = fsopts.root_mount_spec {
2214 RootMountInfo {
2215 mount_spec: s.to_string(),
2216 kargs: Vec::new(),
2217 }
2218 } else if targeting_host_root {
2219 let cmdline = bytes::Cmdline::from_proc()?;
2221 find_root_args_to_inherit(&cmdline, &inspect)?
2222 } else {
2223 let uuid = inspect
2226 .uuid
2227 .as_deref()
2228 .ok_or_else(|| anyhow!("No filesystem uuid found in target root"))?;
2229 let kargs = match inspect.fstype.as_str() {
2230 "btrfs" => {
2231 let subvol = crate::utils::find_mount_option(&inspect.options, "subvol");
2232 subvol
2233 .map(|vol| format!("rootflags=subvol={vol}"))
2234 .into_iter()
2235 .collect::<Vec<_>>()
2236 }
2237 _ => Vec::new(),
2238 };
2239 RootMountInfo {
2240 mount_spec: format!("UUID={uuid}"),
2241 kargs,
2242 }
2243 };
2244 tracing::debug!("Root mount: {} {:?}", root_info.mount_spec, root_info.kargs);
2245
2246 let boot_is_mount = {
2247 let root_dev = rootfs_fd.dir_metadata()?.dev();
2248 let boot_dev = target_rootfs_fd
2249 .symlink_metadata_optional(BOOT)?
2250 .ok_or_else(|| {
2251 anyhow!("No /{BOOT} directory found in root; this is is currently required")
2252 })?
2253 .dev();
2254 tracing::debug!("root_dev={root_dev} boot_dev={boot_dev}");
2255 root_dev != boot_dev
2256 };
2257 let boot_uuid = if boot_is_mount {
2259 let boot_path = target_root_path.join(BOOT);
2260 tracing::debug!("boot_path={boot_path}");
2261 let u = bootc_mount::inspect_filesystem(&boot_path)
2262 .with_context(|| format!("Inspecting /{BOOT}"))?
2263 .uuid
2264 .ok_or_else(|| anyhow!("No UUID found for /{BOOT}"))?;
2265 Some(u)
2266 } else {
2267 None
2268 };
2269 tracing::debug!("boot UUID: {boot_uuid:?}");
2270
2271 let backing_device = {
2274 let mut dev = inspect.source;
2275 loop {
2276 tracing::debug!("Finding parents for {dev}");
2277 let mut parents = bootc_blockdev::find_parent_devices(&dev)?.into_iter();
2278 let Some(parent) = parents.next() else {
2279 break;
2280 };
2281 if let Some(next) = parents.next() {
2282 anyhow::bail!(
2283 "Found multiple parent devices {parent} and {next}; not currently supported"
2284 );
2285 }
2286 dev = parent;
2287 }
2288 dev
2289 };
2290 tracing::debug!("Backing device: {backing_device}");
2291 let device_info = bootc_blockdev::partitions_of(Utf8Path::new(&backing_device))?;
2292
2293 let rootarg = format!("root={}", root_info.mount_spec);
2294 let mut boot = if let Some(spec) = fsopts.boot_mount_spec {
2295 if spec.is_empty() {
2298 None
2299 } else {
2300 Some(MountSpec::new(&spec, "/boot"))
2301 }
2302 } else {
2303 read_boot_fstab_entry(&rootfs_fd)?
2306 .filter(|spec| spec.get_source_uuid().is_some())
2307 .or_else(|| {
2308 boot_uuid
2309 .as_deref()
2310 .map(|boot_uuid| MountSpec::new_uuid_src(boot_uuid, "/boot"))
2311 })
2312 };
2313 if let Some(boot) = boot.as_mut() {
2316 boot.push_option("ro");
2317 }
2318 let bootarg = boot.as_ref().map(|boot| format!("boot={}", &boot.source));
2321
2322 let mut kargs = if root_info.mount_spec.is_empty() {
2325 Vec::new()
2326 } else {
2327 [rootarg]
2328 .into_iter()
2329 .chain(root_info.kargs)
2330 .collect::<Vec<_>>()
2331 };
2332
2333 kargs.push(RW_KARG.to_string());
2334
2335 if let Some(bootarg) = bootarg {
2336 kargs.push(bootarg);
2337 }
2338
2339 let kargs = Cmdline::from(kargs.join(" "));
2340
2341 let skip_finalize =
2342 matches!(fsopts.replace, Some(ReplaceMode::Alongside)) || fsopts.skip_finalize;
2343 let mut rootfs = RootSetup {
2344 #[cfg(feature = "install-to-disk")]
2345 luks_device: None,
2346 device_info,
2347 physical_root_path: fsopts.root_path,
2348 physical_root: rootfs_fd,
2349 target_root_path: Some(target_root_path.clone()),
2350 rootfs_uuid: inspect.uuid.clone(),
2351 boot,
2352 kargs,
2353 skip_finalize,
2354 };
2355
2356 install_to_filesystem_impl(&state, &mut rootfs, cleanup).await?;
2357
2358 drop(rootfs);
2360
2361 installation_complete();
2362
2363 Ok(())
2364}
2365
2366pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) -> Result<()> {
2367 const INSTALL_EXISTING_ROOT_JOURNAL_ID: &str = "7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1";
2369 let source_image = opts
2370 .source_opts
2371 .source_imgref
2372 .as_ref()
2373 .map(|s| s.as_str())
2374 .unwrap_or("none");
2375 let target_path = opts.root_path.as_str();
2376
2377 tracing::info!(
2378 message_id = INSTALL_EXISTING_ROOT_JOURNAL_ID,
2379 bootc.source_image = source_image,
2380 bootc.target_path = target_path,
2381 bootc.cleanup = if opts.cleanup {
2382 "trigger_on_next_boot"
2383 } else {
2384 "skip"
2385 },
2386 "Starting installation to existing root from {} to {}",
2387 source_image,
2388 target_path
2389 );
2390
2391 let cleanup = match opts.cleanup {
2392 true => Cleanup::TriggerOnNextBoot,
2393 false => Cleanup::Skip,
2394 };
2395
2396 let opts = InstallToFilesystemOpts {
2397 filesystem_opts: InstallTargetFilesystemOpts {
2398 root_path: opts.root_path,
2399 root_mount_spec: None,
2400 boot_mount_spec: None,
2401 replace: opts.replace,
2402 skip_finalize: true,
2403 acknowledge_destructive: opts.acknowledge_destructive,
2404 },
2405 source_opts: opts.source_opts,
2406 target_opts: opts.target_opts,
2407 config_opts: opts.config_opts,
2408 composefs_opts: opts.composefs_opts,
2409 };
2410
2411 install_to_filesystem(opts, true, cleanup).await
2412}
2413
2414fn read_boot_fstab_entry(root: &Dir) -> Result<Option<MountSpec>> {
2416 let fstab_path = "etc/fstab";
2417 let fstab = match root.open_optional(fstab_path)? {
2418 Some(f) => f,
2419 None => return Ok(None),
2420 };
2421
2422 let reader = std::io::BufReader::new(fstab);
2423 for line in std::io::BufRead::lines(reader) {
2424 let line = line?;
2425 let line = line.trim();
2426
2427 if line.is_empty() || line.starts_with('#') {
2429 continue;
2430 }
2431
2432 let spec = MountSpec::from_str(line)?;
2434
2435 if spec.target == "/boot" {
2437 return Ok(Some(spec));
2438 }
2439 }
2440
2441 Ok(None)
2442}
2443
2444pub(crate) async fn install_reset(opts: InstallResetOpts) -> Result<()> {
2445 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2446 if !opts.experimental {
2447 anyhow::bail!("This command requires --experimental");
2448 }
2449
2450 let prog: ProgressWriter = opts.progress.try_into()?;
2451
2452 let sysroot = &crate::cli::get_storage().await?;
2453 let ostree = sysroot.get_ostree()?;
2454 let repo = &ostree.repo();
2455 let (booted_ostree, _deployments, host) = crate::status::get_status_require_booted(ostree)?;
2456
2457 let stateroots = list_stateroots(ostree)?;
2458 let target_stateroot = if let Some(s) = opts.stateroot {
2459 s
2460 } else {
2461 let now = chrono::Utc::now();
2462 let r = allocate_new_stateroot(&ostree, &stateroots, now)?;
2463 r.name
2464 };
2465
2466 let booted_stateroot = booted_ostree.stateroot();
2467 assert!(booted_stateroot.as_str() != target_stateroot);
2468 let (fetched, spec) = if let Some(target) = opts.target_opts.imageref()? {
2469 let mut new_spec = host.spec;
2470 new_spec.image = Some(target.into());
2471 let fetched = crate::deploy::pull(
2472 repo,
2473 &new_spec.image.as_ref().unwrap(),
2474 None,
2475 opts.quiet,
2476 prog.clone(),
2477 )
2478 .await?;
2479 (fetched, new_spec)
2480 } else {
2481 let imgstate = host
2482 .status
2483 .booted
2484 .map(|b| b.query_image(repo))
2485 .transpose()?
2486 .flatten()
2487 .ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
2488 (Box::new((*imgstate).into()), host.spec)
2489 };
2490 let spec = crate::deploy::RequiredHostSpec::from_spec(&spec)?;
2491
2492 let mut kargs = crate::bootc_kargs::get_kargs_in_root(rootfs, std::env::consts::ARCH)?;
2495
2496 if !opts.no_root_kargs {
2498 let bootcfg = booted_ostree
2499 .deployment
2500 .bootconfig()
2501 .ok_or_else(|| anyhow!("Missing bootcfg for booted deployment"))?;
2502 if let Some(options) = bootcfg.get("options") {
2503 let options_cmdline = Cmdline::from(options.as_str());
2504 let root_kargs = crate::bootc_kargs::root_args_from_cmdline(&options_cmdline);
2505 kargs.extend(&root_kargs);
2506 }
2507 }
2508
2509 if let Some(user_kargs) = opts.karg.as_ref() {
2511 for karg in user_kargs {
2512 kargs.extend(karg);
2513 }
2514 }
2515
2516 let from = MergeState::Reset {
2517 stateroot: target_stateroot.clone(),
2518 kargs,
2519 };
2520 crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone(), false).await?;
2521
2522 if let Some(boot_spec) = read_boot_fstab_entry(rootfs)? {
2524 let staged_deployment = ostree
2525 .staged_deployment()
2526 .ok_or_else(|| anyhow!("No staged deployment found"))?;
2527 let deployment_path = ostree.deployment_dirpath(&staged_deployment);
2528 let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
2529 let deployment_root = sysroot_dir.open_dir(&deployment_path)?;
2530
2531 crate::lsm::atomic_replace_labeled(
2533 &deployment_root,
2534 "etc/fstab",
2535 0o644.into(),
2536 None,
2537 |w| writeln!(w, "{}", boot_spec.to_fstab()).map_err(Into::into),
2538 )?;
2539
2540 tracing::debug!(
2541 "Copied /boot entry to new stateroot: {}",
2542 boot_spec.to_fstab()
2543 );
2544 }
2545
2546 sysroot.update_mtime()?;
2547
2548 if opts.apply {
2549 crate::reboot::reboot()?;
2550 }
2551 Ok(())
2552}
2553
2554pub(crate) async fn install_finalize(target: &Utf8Path) -> Result<()> {
2556 const INSTALL_FINALIZE_JOURNAL_ID: &str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0";
2558
2559 tracing::info!(
2560 message_id = INSTALL_FINALIZE_JOURNAL_ID,
2561 bootc.target_path = target.as_str(),
2562 "Starting installation finalization for target: {}",
2563 target
2564 );
2565
2566 crate::cli::require_root(false)?;
2567 let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(target)));
2568 sysroot.load(gio::Cancellable::NONE)?;
2569 let deployments = sysroot.deployments();
2570 if deployments.is_empty() {
2572 anyhow::bail!("Failed to find deployment in {target}");
2573 }
2574
2575 tracing::info!(
2577 message_id = INSTALL_FINALIZE_JOURNAL_ID,
2578 bootc.target_path = target.as_str(),
2579 "Successfully finalized installation for target: {}",
2580 target
2581 );
2582
2583 Ok(())
2587}
2588
2589#[cfg(test)]
2590mod tests {
2591 use super::*;
2592
2593 #[test]
2594 fn install_opts_serializable() {
2595 let c: InstallToDiskOpts = serde_json::from_value(serde_json::json!({
2596 "device": "/dev/vda"
2597 }))
2598 .unwrap();
2599 assert_eq!(c.block_opts.device, "/dev/vda");
2600 }
2601
2602 #[test]
2603 fn test_mountspec() {
2604 let mut ms = MountSpec::new("/dev/vda4", "/boot");
2605 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto defaults 0 0");
2606 ms.push_option("ro");
2607 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto ro 0 0");
2608 ms.push_option("relatime");
2609 assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto ro,relatime 0 0");
2610 }
2611
2612 #[test]
2613 fn test_gather_root_args() {
2614 let inspect = Filesystem {
2616 source: "/dev/vda4".into(),
2617 target: "/".into(),
2618 fstype: "xfs".into(),
2619 maj_min: "252:4".into(),
2620 options: "rw".into(),
2621 uuid: Some("965eb3c7-5a3f-470d-aaa2-1bcf04334bc6".into()),
2622 children: None,
2623 };
2624 let kargs = bytes::Cmdline::from("");
2625 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
2626 assert_eq!(r.mount_spec, "UUID=965eb3c7-5a3f-470d-aaa2-1bcf04334bc6");
2627
2628 let kargs = bytes::Cmdline::from(
2629 "root=/dev/mapper/root rw someother=karg rd.lvm.lv=root systemd.debug=1",
2630 );
2631
2632 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
2634 assert_eq!(r.mount_spec, "/dev/mapper/root");
2635 assert_eq!(r.kargs.len(), 1);
2636 assert_eq!(r.kargs[0], "rd.lvm.lv=root");
2637
2638 let kargs = bytes::Cmdline::from(
2640 b"root=/dev/mapper/root rw non-utf8=\xff rd.lvm.lv=root systemd.debug=1",
2641 );
2642 let r = find_root_args_to_inherit(&kargs, &inspect).unwrap();
2643 assert_eq!(r.mount_spec, "/dev/mapper/root");
2644 assert_eq!(r.kargs.len(), 1);
2645 assert_eq!(r.kargs[0], "rd.lvm.lv=root");
2646
2647 let kargs = bytes::Cmdline::from(
2649 b"root=/dev/mapper/ro\xffot rw non-utf8=\xff rd.lvm.lv=root systemd.debug=1",
2650 );
2651 let r = find_root_args_to_inherit(&kargs, &inspect);
2652 assert!(r.is_err());
2653
2654 let kargs = bytes::Cmdline::from(
2656 b"root=/dev/mapper/root rw non-utf8=\xff rd.lvm.lv=ro\xffot systemd.debug=1",
2657 );
2658 let r = find_root_args_to_inherit(&kargs, &inspect);
2659 assert!(r.is_err());
2660 }
2661
2662 #[test]
2665 fn test_remove_all_noxdev() -> Result<()> {
2666 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2667
2668 td.create_dir_all("foo/bar/baz")?;
2669 td.write("foo/bar/baz/test", b"sometest")?;
2670 td.symlink_contents("/absolute-nonexistent-link", "somelink")?;
2671 td.write("toptestfile", b"othertestcontents")?;
2672
2673 remove_all_in_dir_no_xdev(&td, true).unwrap();
2674
2675 assert_eq!(td.entries()?.count(), 0);
2676
2677 Ok(())
2678 }
2679
2680 #[test]
2681 fn test_read_boot_fstab_entry() -> Result<()> {
2682 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2683
2684 assert!(read_boot_fstab_entry(&td)?.is_none());
2686
2687 td.create_dir("etc")?;
2689 td.write("etc/fstab", "UUID=test-uuid / ext4 defaults 0 0\n")?;
2690 assert!(read_boot_fstab_entry(&td)?.is_none());
2691
2692 let fstab_content = "\
2694# /etc/fstab
2695UUID=root-uuid / ext4 defaults 0 0
2696UUID=boot-uuid /boot ext4 ro 0 0
2697UUID=home-uuid /home ext4 defaults 0 0
2698";
2699 td.write("etc/fstab", fstab_content)?;
2700 let boot_spec = read_boot_fstab_entry(&td)?.unwrap();
2701 assert_eq!(boot_spec.source, "UUID=boot-uuid");
2702 assert_eq!(boot_spec.target, "/boot");
2703 assert_eq!(boot_spec.fstype, "ext4");
2704 assert_eq!(boot_spec.options, Some("ro".to_string()));
2705
2706 let fstab_content = "\
2708# /etc/fstab
2709# Created by anaconda
2710UUID=root-uuid / ext4 defaults 0 0
2711# Boot partition
2712UUID=boot-uuid /boot ext4 defaults 0 0
2713";
2714 td.write("etc/fstab", fstab_content)?;
2715 let boot_spec = read_boot_fstab_entry(&td)?.unwrap();
2716 assert_eq!(boot_spec.source, "UUID=boot-uuid");
2717 assert_eq!(boot_spec.target, "/boot");
2718
2719 Ok(())
2720 }
2721
2722 #[test]
2723 fn test_require_dir_contains_only_mounts() -> Result<()> {
2724 {
2726 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2727 td.create_dir("empty")?;
2728 assert!(require_dir_contains_only_mounts(&td, "empty").is_err());
2729 }
2730
2731 {
2733 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2734 td.create_dir_all("var/lost+found")?;
2735 assert!(require_dir_contains_only_mounts(&td, "var").is_ok());
2736 }
2737
2738 {
2740 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2741 td.create_dir("var")?;
2742 td.write("var/test.txt", b"content")?;
2743 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2744 }
2745
2746 {
2748 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2749 td.create_dir_all("var/lib/containers")?;
2750 td.write("var/lib/containers/storage.db", b"data")?;
2751 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2752 }
2753
2754 {
2756 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2757 td.create_dir_all("boot/grub2")?;
2758 td.write("boot/grub2/grub.cfg", b"config")?;
2759 assert!(require_dir_contains_only_mounts(&td, "boot").is_err());
2760 }
2761
2762 {
2764 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2765 td.create_dir_all("var/lib/containers")?;
2766 td.create_dir_all("var/log/journal")?;
2767 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2768 }
2769
2770 {
2772 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2773 td.create_dir_all("var/lost+found")?;
2774 td.write("var/data.txt", b"content")?;
2775 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2776 }
2777
2778 {
2780 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2781 td.create_dir("var")?;
2782 td.symlink_contents("../usr/lib", "var/lib")?;
2783 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2784 }
2785
2786 {
2788 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2789 td.create_dir_all("var/lib/containers/storage/overlay")?;
2790 td.write("var/lib/containers/storage/overlay/file.txt", b"data")?;
2791 assert!(require_dir_contains_only_mounts(&td, "var").is_err());
2792 }
2793
2794 Ok(())
2795 }
2796}