1use std::ffi::{CString, OsStr, OsString};
6use std::fs::File;
7use std::io::{BufWriter, Seek};
8use std::os::fd::AsFd;
9use std::os::unix::process::CommandExt;
10use std::process::Command;
11
12use anyhow::{Context, Result, anyhow, ensure};
13use camino::{Utf8Path, Utf8PathBuf};
14use cap_std_ext::cap_std;
15use cap_std_ext::cap_std::fs::Dir;
16use cfsctl::composefs;
17use cfsctl::composefs_boot;
18use cfsctl::composefs_oci;
19use clap::CommandFactory;
20use clap::Parser;
21use clap::ValueEnum;
22use composefs::dumpfile;
23use composefs::fsverity;
24use composefs::fsverity::FsVerityHashValue;
25use composefs::splitstream::SplitStreamWriter;
26use composefs_boot::BootOps as _;
27use etc_merge::{compute_diff, print_diff};
28use fn_error_context::context;
29use indoc::indoc;
30use ostree::gio;
31use ostree_container::store::PrepareResult;
32use ostree_ext::container as ostree_container;
33
34use ostree_ext::keyfileext::KeyFileExt;
35use ostree_ext::ostree;
36use ostree_ext::sysroot::SysrootLock;
37use schemars::schema_for;
38use serde::{Deserialize, Serialize};
39
40use crate::bootc_composefs::delete::delete_composefs_deployment;
41use crate::bootc_composefs::gc::composefs_gc;
42use crate::bootc_composefs::soft_reboot::{prepare_soft_reboot_composefs, reset_soft_reboot};
43use crate::bootc_composefs::{
44 digest::{compute_composefs_digest, new_temp_composefs_repo},
45 finalize::{composefs_backend_finalize, get_etc_diff},
46 rollback::composefs_rollback,
47 state::composefs_usr_overlay,
48 switch::switch_composefs,
49 update::upgrade_composefs,
50};
51use crate::deploy::{MergeState, RequiredHostSpec};
52use crate::podstorage::set_additional_image_store;
53use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
54use crate::spec::FilesystemOverlayAccessMode;
55use crate::spec::Host;
56use crate::spec::ImageReference;
57use crate::status::get_host;
58use crate::store::{BootedOstree, Storage};
59use crate::store::{BootedStorage, BootedStorageKind};
60use crate::utils::sigpolicy_from_opt;
61use crate::{bootc_composefs, lints};
62
63#[derive(Debug, Parser, PartialEq, Eq)]
65pub(crate) struct ProgressOptions {
66 #[clap(long, hide = true)]
70 pub(crate) progress_fd: Option<RawProgressFd>,
71}
72
73impl TryFrom<ProgressOptions> for ProgressWriter {
74 type Error = anyhow::Error;
75
76 fn try_from(value: ProgressOptions) -> Result<Self> {
77 let r = value
78 .progress_fd
79 .map(TryInto::try_into)
80 .transpose()?
81 .unwrap_or_default();
82 Ok(r)
83 }
84}
85
86#[derive(Debug, Parser, PartialEq, Eq)]
88pub(crate) struct UpgradeOpts {
89 #[clap(long)]
91 pub(crate) quiet: bool,
92
93 #[clap(long, conflicts_with = "apply")]
97 pub(crate) check: bool,
98
99 #[clap(long, conflicts_with = "check")]
103 pub(crate) apply: bool,
104
105 #[clap(long = "soft-reboot", conflicts_with = "check")]
109 pub(crate) soft_reboot: Option<SoftRebootMode>,
110
111 #[clap(long, conflicts_with_all = ["check", "apply"])]
117 pub(crate) download_only: bool,
118
119 #[clap(long, conflicts_with_all = ["check", "download_only"])]
125 pub(crate) from_downloaded: bool,
126
127 #[clap(long)]
132 pub(crate) tag: Option<String>,
133
134 #[clap(flatten)]
135 pub(crate) progress: ProgressOptions,
136}
137
138#[derive(Debug, Parser, PartialEq, Eq)]
140pub(crate) struct SwitchOpts {
141 #[clap(long)]
143 pub(crate) quiet: bool,
144
145 #[clap(long)]
149 pub(crate) apply: bool,
150
151 #[clap(long = "soft-reboot")]
155 pub(crate) soft_reboot: Option<SoftRebootMode>,
156
157 #[clap(long, default_value = "registry")]
159 pub(crate) transport: String,
160
161 #[clap(long, hide = true)]
163 pub(crate) no_signature_verification: bool,
164
165 #[clap(long)]
171 pub(crate) enforce_container_sigpolicy: bool,
172
173 #[clap(long, hide = true)]
177 pub(crate) mutate_in_place: bool,
178
179 #[clap(long)]
181 pub(crate) retain: bool,
182
183 #[clap(long = "experimental-unified-storage", hide = true)]
189 pub(crate) unified_storage_exp: bool,
190
191 pub(crate) target: String,
193
194 #[clap(flatten)]
195 pub(crate) progress: ProgressOptions,
196}
197
198#[derive(Debug, Parser, PartialEq, Eq)]
200pub(crate) struct RollbackOpts {
201 #[clap(long)]
207 pub(crate) apply: bool,
208
209 #[clap(long = "soft-reboot")]
213 pub(crate) soft_reboot: Option<SoftRebootMode>,
214}
215
216#[derive(Debug, Parser, PartialEq, Eq)]
218pub(crate) struct EditOpts {
219 #[clap(long, short = 'f')]
221 pub(crate) filename: Option<String>,
222
223 #[clap(long)]
225 pub(crate) quiet: bool,
226}
227
228#[derive(Debug, Clone, ValueEnum, PartialEq, Eq)]
229#[clap(rename_all = "lowercase")]
230pub(crate) enum OutputFormat {
231 HumanReadable,
233 Yaml,
235 Json,
237}
238
239#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
240#[clap(rename_all = "lowercase")]
241pub(crate) enum SoftRebootMode {
242 Required,
244 Auto,
246}
247
248#[derive(Debug, Parser, PartialEq, Eq)]
250pub(crate) struct StatusOpts {
251 #[clap(long, hide = true)]
255 pub(crate) json: bool,
256
257 #[clap(long)]
259 pub(crate) format: Option<OutputFormat>,
260
261 #[clap(long)]
266 pub(crate) format_version: Option<u32>,
267
268 #[clap(long)]
270 pub(crate) booted: bool,
271
272 #[clap(long, short = 'v')]
274 pub(crate) verbose: bool,
275}
276
277#[derive(Debug, Parser, PartialEq, Eq)]
279pub(crate) struct UsrOverlayOpts {
280 #[clap(long)]
284 pub(crate) read_only: bool,
285}
286
287#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
288pub(crate) enum InstallOpts {
289 #[cfg(feature = "install-to-disk")]
300 ToDisk(crate::install::InstallToDiskOpts),
301 ToFilesystem(crate::install::InstallToFilesystemOpts),
308 ToExistingRoot(crate::install::InstallToExistingRootOpts),
315 #[clap(hide = true)]
320 Reset(crate::install::InstallResetOpts),
321 Finalize {
324 root_path: Utf8PathBuf,
326 },
327 EnsureCompletion {},
335 PrintConfiguration(crate::install::InstallPrintConfigurationOpts),
342}
343
344#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
346pub(crate) enum ContainerOpts {
347 Inspect {
352 #[clap(long, default_value = "/")]
354 rootfs: Utf8PathBuf,
355
356 #[clap(long)]
358 json: bool,
359
360 #[clap(long, conflicts_with = "json")]
362 format: Option<OutputFormat>,
363 },
364 Lint {
370 #[clap(long, default_value = "/")]
372 rootfs: Utf8PathBuf,
373
374 #[clap(long)]
376 fatal_warnings: bool,
377
378 #[clap(long)]
383 list: bool,
384
385 #[clap(long)]
390 skip: Vec<String>,
391
392 #[clap(long)]
395 no_truncate: bool,
396 },
397 #[clap(hide = true)]
399 ComputeComposefsDigest {
400 #[clap(default_value = "/target")]
402 path: Utf8PathBuf,
403
404 #[clap(long)]
406 write_dumpfile_to: Option<Utf8PathBuf>,
407 },
408 #[clap(hide = true)]
410 ComputeComposefsDigestFromStorage {
411 #[clap(long)]
413 write_dumpfile_to: Option<Utf8PathBuf>,
414
415 image: Option<String>,
417 },
418 Ukify {
427 #[clap(long, default_value = "/")]
429 rootfs: Utf8PathBuf,
430
431 #[clap(long = "karg", hide = true)]
435 kargs: Vec<String>,
436
437 #[clap(long)]
439 allow_missing_verity: bool,
440
441 #[clap(last = true)]
443 args: Vec<OsString>,
444 },
445 #[clap(hide = true)]
453 Export {
454 #[clap(long, default_value = "tar")]
456 format: ExportFormat,
457
458 #[clap(long, short = 'o')]
460 output: Option<Utf8PathBuf>,
461
462 #[clap(long)]
465 kernel_in_boot: bool,
466
467 #[clap(long)]
469 disable_selinux: bool,
470
471 target: Utf8PathBuf,
473 },
474}
475
476#[derive(Debug, Clone, ValueEnum, PartialEq, Eq)]
477pub(crate) enum ExportFormat {
478 Tar,
480}
481
482#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
484pub(crate) enum ImageCmdOpts {
485 List {
487 #[clap(allow_hyphen_values = true)]
488 args: Vec<OsString>,
489 },
490 Build {
492 #[clap(allow_hyphen_values = true)]
493 args: Vec<OsString>,
494 },
495 Pull {
497 #[clap(allow_hyphen_values = true)]
498 args: Vec<OsString>,
499 },
500 Push {
502 #[clap(allow_hyphen_values = true)]
503 args: Vec<OsString>,
504 },
505}
506
507#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
508#[serde(rename_all = "kebab-case")]
509pub(crate) enum ImageListType {
510 #[default]
512 All,
513 Logical,
515 Host,
517}
518
519impl std::fmt::Display for ImageListType {
520 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521 self.to_possible_value().unwrap().get_name().fmt(f)
522 }
523}
524
525#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
526#[serde(rename_all = "kebab-case")]
527pub(crate) enum ImageListFormat {
528 #[default]
530 Table,
531 Json,
533}
534impl std::fmt::Display for ImageListFormat {
535 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536 self.to_possible_value().unwrap().get_name().fmt(f)
537 }
538}
539
540#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
542pub(crate) enum ImageOpts {
543 List {
547 #[clap(long = "type")]
549 #[arg(default_value_t)]
550 list_type: ImageListType,
551 #[clap(long = "format")]
552 #[arg(default_value_t)]
553 list_format: ImageListFormat,
554 },
555 CopyToStorage {
572 #[clap(long)]
573 source: Option<String>,
575
576 #[clap(long)]
577 target: Option<String>,
580 },
581 SetUnified,
586 PullFromDefaultStorage {
588 image: String,
590 },
591 #[clap(subcommand)]
593 Cmd(ImageCmdOpts),
594}
595
596#[derive(Debug, Clone, clap::ValueEnum, PartialEq, Eq)]
597pub(crate) enum SchemaType {
598 Host,
599 Progress,
600}
601
602#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
604pub(crate) enum FsverityOpts {
605 Measure {
607 path: Utf8PathBuf,
609 },
610 Enable {
612 path: Utf8PathBuf,
614 },
615}
616
617#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
619pub(crate) enum InternalsOpts {
620 SystemdGenerator {
621 normal_dir: Utf8PathBuf,
622 #[allow(dead_code)]
623 early_dir: Option<Utf8PathBuf>,
624 #[allow(dead_code)]
625 late_dir: Option<Utf8PathBuf>,
626 },
627 FixupEtcFstab,
628 PrintJsonSchema {
630 #[clap(long)]
631 of: SchemaType,
632 },
633 #[clap(subcommand)]
634 Fsverity(FsverityOpts),
635 Fsck,
637 Cleanup,
639 Relabel {
640 #[clap(long)]
641 as_path: Option<Utf8PathBuf>,
643
644 path: Utf8PathBuf,
646 },
647 OstreeExt {
649 #[clap(allow_hyphen_values = true)]
650 args: Vec<OsString>,
651 },
652 Cfs {
654 #[clap(allow_hyphen_values = true)]
655 args: Vec<OsString>,
656 },
657 OstreeContainer {
659 #[clap(allow_hyphen_values = true)]
660 args: Vec<OsString>,
661 },
662 TestComposefs,
664 LoopbackCleanupHelper {
666 #[clap(long)]
668 device: String,
669 },
670 AllocateCleanupLoopback {
672 #[clap(long)]
674 file_path: Utf8PathBuf,
675 },
676 BootcInstallCompletion {
678 sysroot: Utf8PathBuf,
680
681 stateroot: String,
683 },
684 Reboot,
687 #[cfg(feature = "rhsm")]
688 PublishRhsmFacts,
690 DirDiff {
692 pristine_etc: Utf8PathBuf,
694 current_etc: Utf8PathBuf,
696 new_etc: Utf8PathBuf,
698 #[clap(long)]
700 merge: bool,
701 },
702 #[cfg(feature = "docgen")]
703 DumpCliJson,
705 PrepSoftReboot {
706 #[clap(required_unless_present = "reset")]
707 deployment: Option<String>,
708 #[clap(long, conflicts_with = "reset")]
709 reboot: bool,
710 #[clap(long, conflicts_with = "reboot")]
711 reset: bool,
712 },
713 ComposefsGC {
714 #[clap(long)]
715 dry_run: bool,
716 },
717}
718
719#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
720pub(crate) enum StateOpts {
721 WipeOstree,
723}
724
725impl InternalsOpts {
726 const GENERATOR_BIN: &'static str = "bootc-systemd-generator";
728}
729
730#[derive(Debug, Parser, PartialEq, Eq)]
738#[clap(name = "bootc")]
739#[clap(rename_all = "kebab-case")]
740#[clap(version,long_version=clap::crate_version!())]
741#[allow(clippy::large_enum_variant)]
742pub(crate) enum Opt {
743 #[clap(alias = "update")]
756 Upgrade(UpgradeOpts),
757 Switch(SwitchOpts),
768 #[command(after_help = indoc! {r#"
780 Note on Rollbacks and the `/etc` Directory:
781
782 When you perform a rollback (e.g., with `bootc rollback`), any
783 changes made to files in the `/etc` directory won't carry over
784 to the rolled-back deployment. The `/etc` files will revert
785 to their state from that previous deployment instead.
786
787 This is because `bootc rollback` just reorders the existing
788 deployments. It doesn't create new deployments. The `/etc`
789 merges happen when new deployments are created.
790 "#})]
791 Rollback(RollbackOpts),
792 Edit(EditOpts),
802 Status(StatusOpts),
806 #[clap(alias = "usroverlay")]
810 UsrOverlay(UsrOverlayOpts),
811 #[clap(subcommand)]
815 Install(InstallOpts),
816 #[clap(subcommand)]
818 Container(ContainerOpts),
819 #[clap(subcommand, hide = true)]
823 Image(ImageOpts),
824 #[clap(hide = true)]
826 ExecInHostMountNamespace {
827 #[clap(trailing_var_arg = true, allow_hyphen_values = true)]
828 args: Vec<OsString>,
829 },
830 #[clap(hide = true)]
832 #[clap(subcommand)]
833 State(StateOpts),
834 #[clap(subcommand)]
835 #[clap(hide = true)]
836 Internals(InternalsOpts),
837 ComposefsFinalizeStaged,
838 #[clap(hide = true)]
840 ConfigDiff,
841 #[clap(hide = true)]
845 Completion {
846 #[clap(value_enum)]
848 shell: clap_complete::aot::Shell,
849 },
850 #[clap(hide = true)]
851 DeleteDeployment {
852 depl_id: String,
853 },
854}
855
856#[context("Ensuring mountns")]
861pub(crate) fn ensure_self_unshared_mount_namespace() -> Result<()> {
862 let uid = rustix::process::getuid();
863 if !uid.is_root() {
864 tracing::debug!("Not root, assuming no need to unshare");
865 return Ok(());
866 }
867 let recurse_env = "_ostree_unshared";
868 let ns_pid1 = std::fs::read_link("/proc/1/ns/mnt").context("Reading /proc/1/ns/mnt")?;
869 let ns_self = std::fs::read_link("/proc/self/ns/mnt").context("Reading /proc/self/ns/mnt")?;
870 if ns_pid1 != ns_self {
872 tracing::debug!("Already in a mount namespace");
873 return Ok(());
874 }
875 if std::env::var_os(recurse_env).is_some() {
876 let am_pid1 = rustix::process::getpid().is_init();
877 if am_pid1 {
878 tracing::debug!("We are pid 1");
879 return Ok(());
880 } else {
881 anyhow::bail!("Failed to unshare mount namespace");
882 }
883 }
884 bootc_utils::reexec::reexec_with_guardenv(recurse_env, &["unshare", "-m", "--"])
885}
886
887#[context("Initializing storage")]
890pub(crate) async fn get_storage() -> Result<crate::store::BootedStorage> {
891 let env = crate::store::Environment::detect()?;
892 prepare_for_write()?;
895 let r = BootedStorage::new(env)
896 .await?
897 .ok_or_else(|| anyhow!("System not booted via bootc"))?;
898 Ok(r)
899}
900
901#[context("Querying root privilege")]
902pub(crate) fn require_root(is_container: bool) -> Result<()> {
903 ensure!(
904 rustix::process::getuid().is_root(),
905 if is_container {
906 "The user inside the container from which you are running this command must be root"
907 } else {
908 "This command must be executed as the root user"
909 }
910 );
911
912 ensure!(
913 rustix::thread::capability_is_in_bounding_set(rustix::thread::CapabilitySet::SYS_ADMIN)?,
914 if is_container {
915 "The container must be executed with full privileges (e.g. --privileged flag)"
916 } else {
917 "This command requires full root privileges (CAP_SYS_ADMIN)"
918 }
919 );
920
921 tracing::trace!("Verified uid 0 with CAP_SYS_ADMIN");
922
923 Ok(())
924}
925
926fn has_soft_reboot_capability(deployment: Option<&crate::spec::BootEntry>) -> bool {
928 deployment.map(|d| d.soft_reboot_capable).unwrap_or(false)
929}
930
931#[context("Preparing soft reboot")]
933fn prepare_soft_reboot(sysroot: &SysrootLock, deployment: &ostree::Deployment) -> Result<()> {
934 let cancellable = ostree::gio::Cancellable::NONE;
935 sysroot
936 .deployment_set_soft_reboot(deployment, false, cancellable)
937 .context("Failed to prepare soft-reboot")?;
938 Ok(())
939}
940
941#[context("Handling soft reboot")]
943fn handle_soft_reboot<F>(
944 soft_reboot_mode: Option<SoftRebootMode>,
945 entry: Option<&crate::spec::BootEntry>,
946 deployment_type: &str,
947 execute_soft_reboot: F,
948) -> Result<()>
949where
950 F: FnOnce() -> Result<()>,
951{
952 let Some(mode) = soft_reboot_mode else {
953 return Ok(());
954 };
955
956 let can_soft_reboot = has_soft_reboot_capability(entry);
957 match mode {
958 SoftRebootMode::Required => {
959 if can_soft_reboot {
960 execute_soft_reboot()?;
961 } else {
962 anyhow::bail!(
963 "Soft reboot was required but {} deployment is not soft-reboot capable",
964 deployment_type
965 );
966 }
967 }
968 SoftRebootMode::Auto => {
969 if can_soft_reboot {
970 execute_soft_reboot()?;
971 }
972 }
973 }
974 Ok(())
975}
976
977#[context("Handling staged soft reboot")]
979fn handle_staged_soft_reboot(
980 booted_ostree: &BootedOstree<'_>,
981 soft_reboot_mode: Option<SoftRebootMode>,
982 host: &crate::spec::Host,
983) -> Result<()> {
984 handle_soft_reboot(
985 soft_reboot_mode,
986 host.status.staged.as_ref(),
987 "staged",
988 || soft_reboot_staged(booted_ostree.sysroot),
989 )
990}
991
992#[context("Soft reboot staged deployment")]
994fn soft_reboot_staged(sysroot: &SysrootLock) -> Result<()> {
995 println!("Staged deployment is soft-reboot capable, preparing for soft-reboot...");
996
997 let deployments_list = sysroot.deployments();
998 let staged_deployment = deployments_list
999 .iter()
1000 .find(|d| d.is_staged())
1001 .ok_or_else(|| anyhow::anyhow!("Failed to find staged deployment"))?;
1002
1003 prepare_soft_reboot(sysroot, staged_deployment)?;
1004 Ok(())
1005}
1006
1007#[context("Soft reboot rollback deployment")]
1009fn soft_reboot_rollback(booted_ostree: &BootedOstree<'_>) -> Result<()> {
1010 println!("Rollback deployment is soft-reboot capable, preparing for soft-reboot...");
1011
1012 let deployments_list = booted_ostree.sysroot.deployments();
1013 let target_deployment = deployments_list
1014 .first()
1015 .ok_or_else(|| anyhow::anyhow!("No rollback deployment found!"))?;
1016
1017 prepare_soft_reboot(booted_ostree.sysroot, target_deployment)
1018}
1019
1020#[context("Preparing for write")]
1024pub(crate) fn prepare_for_write() -> Result<()> {
1025 use std::sync::atomic::{AtomicBool, Ordering};
1026
1027 static ENTERED: AtomicBool = AtomicBool::new(false);
1033 if ENTERED.load(Ordering::SeqCst) {
1034 return Ok(());
1035 }
1036 if ostree_ext::container_utils::running_in_container() {
1037 anyhow::bail!("Detected container; this command requires a booted host system.");
1038 }
1039 crate::cli::require_root(false)?;
1040 ensure_self_unshared_mount_namespace()?;
1041 if crate::lsm::selinux_enabled()? && !crate::lsm::selinux_ensure_install()? {
1042 tracing::debug!("Do not have install_t capabilities");
1043 }
1044 ENTERED.store(true, Ordering::SeqCst);
1045 Ok(())
1046}
1047
1048#[context("Upgrading")]
1050async fn upgrade(
1051 opts: UpgradeOpts,
1052 storage: &Storage,
1053 booted_ostree: &BootedOstree<'_>,
1054) -> Result<()> {
1055 let repo = &booted_ostree.repo();
1056
1057 let host = crate::status::get_status(booted_ostree)?.1;
1058 let current_image = host.spec.image.as_ref();
1059
1060 let derived_image = if let Some(ref tag) = opts.tag {
1062 let image = current_image.ok_or_else(|| {
1063 anyhow::anyhow!("--tag requires a booted image with a specified source")
1064 })?;
1065 Some(image.with_tag(tag)?)
1066 } else {
1067 None
1068 };
1069
1070 let imgref = derived_image.as_ref().or(current_image);
1071 let prog: ProgressWriter = opts.progress.try_into()?;
1072
1073 if imgref.is_none() {
1075 let booted_incompatible = host.status.booted.as_ref().is_some_and(|b| b.incompatible);
1076
1077 let staged_incompatible = host.status.staged.as_ref().is_some_and(|b| b.incompatible);
1078
1079 if booted_incompatible || staged_incompatible {
1080 return Err(anyhow::anyhow!(
1081 "Deployment contains local rpm-ostree modifications; cannot upgrade via bootc. You can run `rpm-ostree reset` to undo the modifications."
1082 ));
1083 }
1084 }
1085
1086 let imgref = imgref.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
1087 let spec = RequiredHostSpec { image: imgref };
1089 let booted_image = host
1090 .status
1091 .booted
1092 .as_ref()
1093 .map(|b| b.query_image(repo))
1094 .transpose()?
1095 .flatten();
1096 let staged = host.status.staged.as_ref();
1098 let staged_image = staged.as_ref().and_then(|s| s.image.as_ref());
1099 let mut changed = false;
1100
1101 if opts.from_downloaded {
1103 let ostree = storage.get_ostree()?;
1104 let staged_deployment = ostree
1105 .staged_deployment()
1106 .ok_or_else(|| anyhow::anyhow!("No staged deployment found"))?;
1107
1108 if staged_deployment.is_finalization_locked() {
1109 ostree.change_finalization(&staged_deployment)?;
1110 println!("Staged deployment will now be applied on reboot");
1111 } else {
1112 println!("Staged deployment is already set to apply on reboot");
1113 }
1114
1115 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &host)?;
1116 if opts.apply {
1117 crate::reboot::reboot()?;
1118 }
1119 return Ok(());
1120 }
1121
1122 let use_unified = crate::deploy::image_exists_in_unified_storage(storage, imgref).await?;
1126
1127 if opts.check {
1128 let ostree_imgref = imgref.clone().into();
1129 let mut imp =
1130 crate::deploy::new_importer(repo, &ostree_imgref, Some(&booted_ostree.deployment))
1131 .await?;
1132 match imp.prepare().await? {
1133 PrepareResult::AlreadyPresent(_) => {
1134 println!("No changes in: {ostree_imgref:#}");
1135 }
1136 PrepareResult::Ready(r) => {
1137 crate::deploy::check_bootc_label(&r.config);
1138 println!("Update available for: {ostree_imgref:#}");
1139 if let Some(version) = r.version() {
1140 println!(" Version: {version}");
1141 }
1142 println!(" Digest: {}", r.manifest_digest);
1143 changed = true;
1144 if let Some(previous_image) = booted_image.as_ref() {
1145 let diff =
1146 ostree_container::ManifestDiff::new(&previous_image.manifest, &r.manifest);
1147 diff.print();
1148 }
1149 }
1150 }
1151 } else {
1152 let fetched = if use_unified {
1153 crate::deploy::pull_unified(
1154 repo,
1155 imgref,
1156 None,
1157 opts.quiet,
1158 prog.clone(),
1159 storage,
1160 Some(&booted_ostree.deployment),
1161 )
1162 .await?
1163 } else {
1164 crate::deploy::pull(
1165 repo,
1166 imgref,
1167 None,
1168 opts.quiet,
1169 prog.clone(),
1170 Some(&booted_ostree.deployment),
1171 )
1172 .await?
1173 };
1174 let staged_digest = staged_image.map(|s| s.digest().expect("valid digest in status"));
1175 let fetched_digest = &fetched.manifest_digest;
1176 tracing::debug!("staged: {staged_digest:?}");
1177 tracing::debug!("fetched: {fetched_digest}");
1178 let staged_unchanged = staged_digest
1179 .as_ref()
1180 .map(|d| d == fetched_digest)
1181 .unwrap_or_default();
1182 let booted_unchanged = booted_image
1183 .as_ref()
1184 .map(|img| &img.manifest_digest == fetched_digest)
1185 .unwrap_or_default();
1186 if staged_unchanged {
1187 let staged_deployment = storage.get_ostree()?.staged_deployment();
1188 let mut download_only_changed = false;
1189
1190 if let Some(staged) = staged_deployment {
1191 if opts.download_only {
1193 if !staged.is_finalization_locked() {
1195 storage.get_ostree()?.change_finalization(&staged)?;
1196 println!("Image downloaded, but will not be applied on reboot");
1197 download_only_changed = true;
1198 }
1199 } else if !opts.check {
1200 if staged.is_finalization_locked() {
1203 storage.get_ostree()?.change_finalization(&staged)?;
1204 println!("Staged deployment will now be applied on reboot");
1205 download_only_changed = true;
1206 }
1207 }
1208 } else if opts.download_only || opts.apply {
1209 anyhow::bail!("No staged deployment found");
1210 }
1211
1212 if !download_only_changed {
1213 println!("Staged update present, not changed");
1214 }
1215
1216 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &host)?;
1217 if opts.apply {
1218 crate::reboot::reboot()?;
1219 }
1220 } else if booted_unchanged {
1221 println!("No update available.")
1222 } else {
1223 let stateroot = booted_ostree.stateroot();
1224 let from = MergeState::from_stateroot(storage, &stateroot)?;
1225 crate::deploy::stage(
1226 storage,
1227 from,
1228 &fetched,
1229 &spec,
1230 prog.clone(),
1231 opts.download_only,
1232 )
1233 .await?;
1234 changed = true;
1235 if let Some(prev) = booted_image.as_ref() {
1236 if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
1237 let diff =
1238 ostree_container::ManifestDiff::new(&prev.manifest, &fetched_manifest);
1239 diff.print();
1240 }
1241 }
1242 }
1243 }
1244 if changed {
1245 storage.update_mtime()?;
1246
1247 if opts.soft_reboot.is_some() {
1248 let updated_host = crate::status::get_status(booted_ostree)?.1;
1251 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &updated_host)?;
1252 }
1253
1254 if opts.apply {
1255 crate::reboot::reboot()?;
1256 }
1257 } else {
1258 tracing::debug!("No changes");
1259 }
1260
1261 Ok(())
1262}
1263pub(crate) fn imgref_for_switch(opts: &SwitchOpts) -> Result<ImageReference> {
1264 let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
1265 let imgref = ostree_container::ImageReference {
1266 transport,
1267 name: opts.target.to_string(),
1268 };
1269 let sigverify = sigpolicy_from_opt(opts.enforce_container_sigpolicy);
1270 let target = ostree_container::OstreeImageReference { sigverify, imgref };
1271 let target = ImageReference::from(target);
1272
1273 return Ok(target);
1274}
1275
1276#[context("Switching (ostree)")]
1278async fn switch_ostree(
1279 opts: SwitchOpts,
1280 storage: &Storage,
1281 booted_ostree: &BootedOstree<'_>,
1282) -> Result<()> {
1283 let target = imgref_for_switch(&opts)?;
1284 let prog: ProgressWriter = opts.progress.try_into()?;
1285 let cancellable = gio::Cancellable::NONE;
1286
1287 let repo = &booted_ostree.repo();
1288 let (_, host) = crate::status::get_status(booted_ostree)?;
1289
1290 let new_spec = {
1291 let mut new_spec = host.spec.clone();
1292 new_spec.image = Some(target.clone());
1293 new_spec
1294 };
1295
1296 if new_spec == host.spec {
1297 println!("Image specification is unchanged.");
1298 return Ok(());
1299 }
1300
1301 const SWITCH_JOURNAL_ID: &str = "7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1";
1303 let old_image = host
1304 .spec
1305 .image
1306 .as_ref()
1307 .map(|i| i.image.as_str())
1308 .unwrap_or("none");
1309
1310 tracing::info!(
1311 message_id = SWITCH_JOURNAL_ID,
1312 bootc.old_image_reference = old_image,
1313 bootc.new_image_reference = &target.image,
1314 bootc.new_image_transport = &target.transport,
1315 "Switching from image {} to {}",
1316 old_image,
1317 target.image
1318 );
1319
1320 let new_spec = RequiredHostSpec::from_spec(&new_spec)?;
1321
1322 let use_unified = if opts.unified_storage_exp {
1326 true
1327 } else {
1328 crate::deploy::image_exists_in_unified_storage(storage, &target).await?
1329 };
1330
1331 let fetched = if use_unified {
1332 crate::deploy::pull_unified(
1333 repo,
1334 &target,
1335 None,
1336 opts.quiet,
1337 prog.clone(),
1338 storage,
1339 Some(&booted_ostree.deployment),
1340 )
1341 .await?
1342 } else {
1343 crate::deploy::pull(
1344 repo,
1345 &target,
1346 None,
1347 opts.quiet,
1348 prog.clone(),
1349 Some(&booted_ostree.deployment),
1350 )
1351 .await?
1352 };
1353
1354 if !opts.retain {
1355 if let Some(booted_origin) = booted_ostree.deployment.origin() {
1357 if let Some(ostree_ref) = booted_origin.optional_string("origin", "refspec")? {
1358 let (remote, ostree_ref) =
1359 ostree::parse_refspec(&ostree_ref).context("Failed to parse ostree ref")?;
1360 repo.set_ref_immediate(remote.as_deref(), &ostree_ref, None, cancellable)?;
1361 }
1362 }
1363 }
1364
1365 let stateroot = booted_ostree.stateroot();
1366 let from = MergeState::from_stateroot(storage, &stateroot)?;
1367 crate::deploy::stage(storage, from, &fetched, &new_spec, prog.clone(), false).await?;
1368
1369 storage.update_mtime()?;
1370
1371 if opts.soft_reboot.is_some() {
1372 let updated_host = crate::status::get_status(booted_ostree)?.1;
1375 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &updated_host)?;
1376 }
1377
1378 if opts.apply {
1379 crate::reboot::reboot()?;
1380 }
1381
1382 Ok(())
1383}
1384
1385#[context("Switching")]
1387async fn switch(opts: SwitchOpts) -> Result<()> {
1388 if opts.mutate_in_place {
1392 let target = imgref_for_switch(&opts)?;
1393 let deployid = {
1394 let target = target.clone();
1396 let root = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1397 tokio::task::spawn_blocking(move || {
1398 crate::deploy::switch_origin_inplace(&root, &target)
1399 })
1400 .await??
1401 };
1402 println!("Updated {deployid} to pull from {target}");
1403 return Ok(());
1404 }
1405 let storage = &get_storage().await?;
1406 match storage.kind()? {
1407 BootedStorageKind::Ostree(booted_ostree) => {
1408 switch_ostree(opts, storage, &booted_ostree).await
1409 }
1410 BootedStorageKind::Composefs(booted_cfs) => {
1411 switch_composefs(opts, storage, &booted_cfs).await
1412 }
1413 }
1414}
1415
1416#[context("Rollback (ostree)")]
1418async fn rollback_ostree(
1419 opts: &RollbackOpts,
1420 storage: &Storage,
1421 booted_ostree: &BootedOstree<'_>,
1422) -> Result<()> {
1423 crate::deploy::rollback(storage).await?;
1424
1425 if opts.soft_reboot.is_some() {
1426 let host = crate::status::get_status(booted_ostree)?.1;
1428
1429 handle_soft_reboot(
1430 opts.soft_reboot,
1431 host.status.rollback.as_ref(),
1432 "rollback",
1433 || soft_reboot_rollback(booted_ostree),
1434 )?;
1435 }
1436
1437 Ok(())
1438}
1439
1440#[context("Rollback")]
1442async fn rollback(opts: &RollbackOpts) -> Result<()> {
1443 let storage = &get_storage().await?;
1444 match storage.kind()? {
1445 BootedStorageKind::Ostree(booted_ostree) => {
1446 rollback_ostree(opts, storage, &booted_ostree).await
1447 }
1448 BootedStorageKind::Composefs(booted_cfs) => composefs_rollback(storage, &booted_cfs).await,
1449 }
1450}
1451
1452#[context("Editing spec (ostree)")]
1454async fn edit_ostree(
1455 opts: EditOpts,
1456 storage: &Storage,
1457 booted_ostree: &BootedOstree<'_>,
1458) -> Result<()> {
1459 let repo = &booted_ostree.repo();
1460 let (_, host) = crate::status::get_status(booted_ostree)?;
1461
1462 let new_host: Host = if let Some(filename) = opts.filename {
1463 let mut r = std::io::BufReader::new(std::fs::File::open(filename)?);
1464 serde_yaml::from_reader(&mut r)?
1465 } else {
1466 let tmpf = tempfile::NamedTempFile::with_suffix(".yaml")?;
1467 serde_yaml::to_writer(std::io::BufWriter::new(tmpf.as_file()), &host)?;
1468 crate::utils::spawn_editor(&tmpf)?;
1469 tmpf.as_file().seek(std::io::SeekFrom::Start(0))?;
1470 serde_yaml::from_reader(&mut tmpf.as_file())?
1471 };
1472
1473 if new_host.spec == host.spec {
1474 println!("Edit cancelled, no changes made.");
1475 return Ok(());
1476 }
1477 host.spec.verify_transition(&new_host.spec)?;
1478 let new_spec = RequiredHostSpec::from_spec(&new_host.spec)?;
1479
1480 let prog = ProgressWriter::default();
1481
1482 if host.spec.boot_order != new_host.spec.boot_order {
1485 return crate::deploy::rollback(storage).await;
1486 }
1487
1488 let fetched = crate::deploy::pull(
1489 repo,
1490 new_spec.image,
1491 None,
1492 opts.quiet,
1493 prog.clone(),
1494 Some(&booted_ostree.deployment),
1495 )
1496 .await?;
1497
1498 let stateroot = booted_ostree.stateroot();
1501 let from = MergeState::from_stateroot(storage, &stateroot)?;
1502 crate::deploy::stage(storage, from, &fetched, &new_spec, prog.clone(), false).await?;
1503
1504 storage.update_mtime()?;
1505
1506 Ok(())
1507}
1508
1509#[context("Editing spec")]
1511async fn edit(opts: EditOpts) -> Result<()> {
1512 let storage = &get_storage().await?;
1513 match storage.kind()? {
1514 BootedStorageKind::Ostree(booted_ostree) => {
1515 edit_ostree(opts, storage, &booted_ostree).await
1516 }
1517 BootedStorageKind::Composefs(_) => {
1518 anyhow::bail!("Edit is not yet supported for composefs backend")
1519 }
1520 }
1521}
1522
1523async fn usroverlay(access_mode: FilesystemOverlayAccessMode) -> Result<()> {
1525 let args = match access_mode {
1528 FilesystemOverlayAccessMode::ReadOnly => ["admin", "unlock", "--transient"].as_slice(),
1530
1531 FilesystemOverlayAccessMode::ReadWrite => ["admin", "unlock"].as_slice(),
1532 };
1533 Err(Command::new("ostree").args(args).exec().into())
1534}
1535
1536fn join_host_ipc_namespace() -> Result<()> {
1546 let caps = rustix::thread::capabilities(None).context("capget")?;
1547 if !caps
1548 .effective
1549 .contains(rustix::thread::CapabilitySet::SYS_ADMIN)
1550 {
1551 return Ok(());
1552 }
1553 let ns_pid1 = std::fs::read_link("/proc/1/ns/ipc").context("reading /proc/1/ns/ipc")?;
1554 let ns_self = std::fs::read_link("/proc/self/ns/ipc").context("reading /proc/self/ns/ipc")?;
1555 if ns_pid1 != ns_self {
1556 let pid1ipcns = std::fs::File::open("/proc/1/ns/ipc").context("open pid1 ipcns")?;
1557 rustix::thread::move_into_link_name_space(
1558 pid1ipcns.as_fd(),
1559 Some(rustix::thread::LinkNameSpaceType::InterProcessCommunication),
1560 )
1561 .context("setns(ipc)")?;
1562 tracing::debug!("Joined pid1 IPC namespace");
1563 }
1564 Ok(())
1565}
1566
1567#[allow(unsafe_code)]
1570pub fn global_init() -> Result<()> {
1571 join_host_ipc_namespace()?;
1572 ostree::glib::set_prgname(bootc_utils::NAME.into());
1575 if let Err(e) = rustix::thread::set_name(&CString::new(bootc_utils::NAME).unwrap()) {
1576 eprintln!("failed to set name: {e}");
1578 }
1579 ostree::SePolicy::set_null_log();
1581 let am_root = rustix::process::getuid().is_root();
1582 if std::env::var_os("HOME").is_none() && am_root {
1585 unsafe {
1590 std::env::set_var("HOME", "/root");
1591 }
1592 }
1593 Ok(())
1594}
1595
1596pub async fn run_from_iter<I>(args: I) -> Result<()>
1599where
1600 I: IntoIterator,
1601 I::Item: Into<OsString> + Clone,
1602{
1603 run_from_opt(Opt::parse_including_static(args)).await
1604}
1605
1606fn callname_from_argv0(argv0: &OsStr) -> &str {
1610 let default = "bootc";
1611 std::path::Path::new(argv0)
1612 .file_name()
1613 .and_then(|s| s.to_str())
1614 .filter(|s| !s.is_empty())
1615 .unwrap_or(default)
1616}
1617
1618impl Opt {
1619 fn parse_including_static<I>(args: I) -> Self
1622 where
1623 I: IntoIterator,
1624 I::Item: Into<OsString> + Clone,
1625 {
1626 let mut args = args.into_iter();
1627 let first = if let Some(first) = args.next() {
1628 let first: OsString = first.into();
1629 let argv0 = callname_from_argv0(&first);
1630 tracing::debug!("argv0={argv0:?}");
1631 let mapped = match argv0 {
1632 InternalsOpts::GENERATOR_BIN => {
1633 Some(["bootc", "internals", "systemd-generator"].as_slice())
1634 }
1635 "ostree-container" | "ostree-ima-sign" | "ostree-provisional-repair" => {
1636 Some(["bootc", "internals", "ostree-ext"].as_slice())
1637 }
1638 _ => None,
1639 };
1640 if let Some(base_args) = mapped {
1641 let base_args = base_args.iter().map(OsString::from);
1642 return Opt::parse_from(base_args.chain(args.map(|i| i.into())));
1643 }
1644 Some(first)
1645 } else {
1646 None
1647 };
1648 Opt::parse_from(first.into_iter().chain(args.map(|i| i.into())))
1649 }
1650}
1651
1652async fn run_from_opt(opt: Opt) -> Result<()> {
1654 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1655 match opt {
1656 Opt::Upgrade(opts) => {
1657 let storage = &get_storage().await?;
1658 match storage.kind()? {
1659 BootedStorageKind::Ostree(booted_ostree) => {
1660 upgrade(opts, storage, &booted_ostree).await
1661 }
1662 BootedStorageKind::Composefs(booted_cfs) => {
1663 upgrade_composefs(opts, storage, &booted_cfs).await
1664 }
1665 }
1666 }
1667 Opt::Switch(opts) => switch(opts).await,
1668 Opt::Rollback(opts) => {
1669 rollback(&opts).await?;
1670 if opts.apply {
1671 crate::reboot::reboot()?;
1672 }
1673 Ok(())
1674 }
1675 Opt::Edit(opts) => edit(opts).await,
1676 Opt::UsrOverlay(opts) => {
1677 use crate::store::Environment;
1678 let env = Environment::detect()?;
1679 let access_mode = if opts.read_only {
1680 FilesystemOverlayAccessMode::ReadOnly
1681 } else {
1682 FilesystemOverlayAccessMode::ReadWrite
1683 };
1684 match env {
1685 Environment::OstreeBooted => usroverlay(access_mode).await,
1686 Environment::ComposefsBooted(_) => composefs_usr_overlay(access_mode),
1687 _ => anyhow::bail!("usroverlay only applies on booted hosts"),
1688 }
1689 }
1690 Opt::Container(opts) => match opts {
1691 ContainerOpts::Inspect {
1692 rootfs,
1693 json,
1694 format,
1695 } => crate::status::container_inspect(&rootfs, json, format),
1696 ContainerOpts::Lint {
1697 rootfs,
1698 fatal_warnings,
1699 list,
1700 skip,
1701 no_truncate,
1702 } => {
1703 if list {
1704 return lints::lint_list(std::io::stdout().lock());
1705 }
1706 let warnings = if fatal_warnings {
1707 lints::WarningDisposition::FatalWarnings
1708 } else {
1709 lints::WarningDisposition::AllowWarnings
1710 };
1711 let root_type = if rootfs == "/" {
1712 lints::RootType::Running
1713 } else {
1714 lints::RootType::Alternative
1715 };
1716
1717 let root = &Dir::open_ambient_dir(rootfs, cap_std::ambient_authority())?;
1718 let skip = skip.iter().map(|s| s.as_str());
1719 lints::lint(
1720 root,
1721 warnings,
1722 root_type,
1723 skip,
1724 std::io::stdout().lock(),
1725 no_truncate,
1726 )?;
1727 Ok(())
1728 }
1729 ContainerOpts::ComputeComposefsDigest {
1730 path,
1731 write_dumpfile_to,
1732 } => {
1733 let digest = compute_composefs_digest(&path, write_dumpfile_to.as_deref())?;
1734 println!("{digest}");
1735 Ok(())
1736 }
1737 ContainerOpts::ComputeComposefsDigestFromStorage {
1738 write_dumpfile_to,
1739 image,
1740 } => {
1741 let (_td_guard, repo) = new_temp_composefs_repo()?;
1742
1743 let mut proxycfg = crate::deploy::new_proxy_config();
1744
1745 let image = if let Some(image) = image {
1746 image
1747 } else {
1748 let host_container_store = Utf8Path::new("/run/host-container-storage");
1749 let container_info = crate::containerenv::get_container_execution_info(&root)?;
1752 let iid = container_info.imageid;
1753 tracing::debug!("Computing digest of {iid}");
1754
1755 if !host_container_store.try_exists()? {
1756 anyhow::bail!(
1757 "Must be readonly mount of host container store: {host_container_store}"
1758 );
1759 }
1760 let mut cmd = Command::new("skopeo");
1762 set_additional_image_store(&mut cmd, "/run/host-container-storage");
1763 proxycfg.skopeo_cmd = Some(cmd);
1764 iid
1765 };
1766
1767 let imgref = format!("containers-storage:{image}");
1768 let pull_result = composefs_oci::pull(&repo, &imgref, None, Some(proxycfg))
1769 .await
1770 .context("Pulling image")?;
1771 let mut fs = composefs_oci::image::create_filesystem(
1772 &repo,
1773 &pull_result.config_digest,
1774 Some(&pull_result.config_verity),
1775 )
1776 .context("Populating fs")?;
1777 fs.transform_for_boot(&repo).context("Preparing for boot")?;
1778 let id = fs.compute_image_id();
1779 println!("{}", id.to_hex());
1780
1781 if let Some(path) = write_dumpfile_to.as_deref() {
1782 let mut w = File::create(path)
1783 .with_context(|| format!("Opening {path}"))
1784 .map(BufWriter::new)?;
1785 dumpfile::write_dumpfile(&mut w, &fs).context("Writing dumpfile")?;
1786 }
1787
1788 Ok(())
1789 }
1790 ContainerOpts::Ukify {
1791 rootfs,
1792 kargs,
1793 allow_missing_verity,
1794 args,
1795 } => crate::ukify::build_ukify(&rootfs, &kargs, &args, allow_missing_verity),
1796 ContainerOpts::Export {
1797 format,
1798 target,
1799 output,
1800 kernel_in_boot,
1801 disable_selinux,
1802 } => {
1803 crate::container_export::export(
1804 &format,
1805 &target,
1806 output.as_deref(),
1807 kernel_in_boot,
1808 disable_selinux,
1809 )
1810 .await
1811 }
1812 },
1813 Opt::Completion { shell } => {
1814 use clap_complete::aot::generate;
1815
1816 let mut cmd = Opt::command();
1817 let mut stdout = std::io::stdout();
1818 let bin_name = "bootc";
1819 generate(shell, &mut cmd, bin_name, &mut stdout);
1820 Ok(())
1821 }
1822 Opt::Image(opts) => match opts {
1823 ImageOpts::List {
1824 list_type,
1825 list_format,
1826 } => crate::image::list_entrypoint(list_type, list_format).await,
1827
1828 ImageOpts::CopyToStorage { source, target } => {
1829 let host = get_host().await?;
1831
1832 let storage = get_storage().await?;
1833
1834 match storage.kind()? {
1835 BootedStorageKind::Ostree(..) => {
1836 crate::image::push_entrypoint(
1837 &storage,
1838 &host,
1839 source.as_deref(),
1840 target.as_deref(),
1841 )
1842 .await
1843 }
1844 BootedStorageKind::Composefs(booted) => {
1845 bootc_composefs::export::export_repo_to_image(
1846 &storage,
1847 &booted,
1848 source.as_deref(),
1849 target.as_deref(),
1850 )
1851 .await
1852 }
1853 }
1854 }
1855 ImageOpts::SetUnified => crate::image::set_unified_entrypoint().await,
1856 ImageOpts::PullFromDefaultStorage { image } => {
1857 let storage = get_storage().await?;
1858 storage
1859 .get_ensure_imgstore()?
1860 .pull_from_host_storage(&image)
1861 .await
1862 }
1863 ImageOpts::Cmd(opt) => {
1864 let storage = get_storage().await?;
1865 let imgstore = storage.get_ensure_imgstore()?;
1866 match opt {
1867 ImageCmdOpts::List { args } => {
1868 crate::image::imgcmd_entrypoint(imgstore, "list", &args).await
1869 }
1870 ImageCmdOpts::Build { args } => {
1871 crate::image::imgcmd_entrypoint(imgstore, "build", &args).await
1872 }
1873 ImageCmdOpts::Pull { args } => {
1874 crate::image::imgcmd_entrypoint(imgstore, "pull", &args).await
1875 }
1876 ImageCmdOpts::Push { args } => {
1877 crate::image::imgcmd_entrypoint(imgstore, "push", &args).await
1878 }
1879 }
1880 }
1881 },
1882 Opt::Install(opts) => match opts {
1883 #[cfg(feature = "install-to-disk")]
1884 InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await,
1885 InstallOpts::ToFilesystem(opts) => {
1886 crate::install::install_to_filesystem(opts, false, crate::install::Cleanup::Skip)
1887 .await
1888 }
1889 InstallOpts::ToExistingRoot(opts) => {
1890 crate::install::install_to_existing_root(opts).await
1891 }
1892 InstallOpts::Reset(opts) => crate::install::install_reset(opts).await,
1893 InstallOpts::PrintConfiguration(opts) => crate::install::print_configuration(opts),
1894 InstallOpts::EnsureCompletion {} => {
1895 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1896 crate::install::completion::run_from_anaconda(rootfs).await
1897 }
1898 InstallOpts::Finalize { root_path } => {
1899 crate::install::install_finalize(&root_path).await
1900 }
1901 },
1902 Opt::ExecInHostMountNamespace { args } => {
1903 crate::install::exec_in_host_mountns(args.as_slice())
1904 }
1905 Opt::Status(opts) => super::status::status(opts).await,
1906 Opt::Internals(opts) => match opts {
1907 InternalsOpts::SystemdGenerator {
1908 normal_dir,
1909 early_dir: _,
1910 late_dir: _,
1911 } => {
1912 let unit_dir = &Dir::open_ambient_dir(normal_dir, cap_std::ambient_authority())?;
1913 crate::generator::generator(root, unit_dir)
1914 }
1915 InternalsOpts::OstreeExt { args } => {
1916 ostree_ext::cli::run_from_iter(["ostree-ext".into()].into_iter().chain(args)).await
1917 }
1918 InternalsOpts::OstreeContainer { args } => {
1919 ostree_ext::cli::run_from_iter(
1920 ["ostree-ext".into(), "container".into()]
1921 .into_iter()
1922 .chain(args),
1923 )
1924 .await
1925 }
1926 InternalsOpts::TestComposefs => {
1927 let storage = get_storage().await?;
1929 let cfs = storage.get_ensure_composefs()?;
1930 let testdata = b"some test data";
1931 let testdata_digest = hex::encode(openssl::sha::sha256(testdata));
1932 let mut w = SplitStreamWriter::new(&cfs, 0);
1933 w.write_inline(testdata);
1934 let object = cfs
1935 .write_stream(w, &testdata_digest, Some("testobject"))?
1936 .to_hex();
1937 assert_eq!(
1938 object,
1939 "dc31ae5d2f637e98d2171821d60d2fcafb8084d6a4bb3bd9cdc7ad41decce6e48f85d5413d22371d36b223945042f53a2a6ab449b8e45d8896ba7d8694a16681"
1940 );
1941 Ok(())
1942 }
1943 InternalsOpts::Fsverity(args) => match args {
1945 FsverityOpts::Measure { path } => {
1946 let fd =
1947 std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
1948 let digest: fsverity::Sha256HashValue = fsverity::measure_verity(&fd)?;
1949 let digest = digest.to_hex();
1950 println!("{digest}");
1951 Ok(())
1952 }
1953 FsverityOpts::Enable { path } => {
1954 let fd =
1955 std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
1956 fsverity::enable_verity_raw::<fsverity::Sha256HashValue>(&fd)?;
1957 Ok(())
1958 }
1959 },
1960 InternalsOpts::Cfs { args } => cfsctl::run_from_iter(args.iter()).await,
1961 InternalsOpts::Reboot => crate::reboot::reboot(),
1962 InternalsOpts::Fsck => {
1963 let storage = &get_storage().await?;
1964 crate::fsck::fsck(&storage, std::io::stdout().lock()).await?;
1965 Ok(())
1966 }
1967 InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root),
1968 InternalsOpts::PrintJsonSchema { of } => {
1969 let schema = match of {
1970 SchemaType::Host => schema_for!(crate::spec::Host),
1971 SchemaType::Progress => schema_for!(crate::progress_jsonl::Event),
1972 };
1973 let mut stdout = std::io::stdout().lock();
1974 serde_json::to_writer_pretty(&mut stdout, &schema)?;
1975 Ok(())
1976 }
1977 InternalsOpts::Cleanup => {
1978 let storage = get_storage().await?;
1979 crate::deploy::cleanup(&storage).await
1980 }
1981 InternalsOpts::Relabel { as_path, path } => {
1982 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1983 let path = path.strip_prefix("/")?;
1984 let sepolicy =
1985 &ostree::SePolicy::new(&gio::File::for_path("/"), gio::Cancellable::NONE)?;
1986 crate::lsm::relabel_recurse(root, path, as_path.as_deref(), sepolicy)?;
1987 Ok(())
1988 }
1989 InternalsOpts::BootcInstallCompletion { sysroot, stateroot } => {
1990 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1991 crate::install::completion::run_from_ostree(rootfs, &sysroot, &stateroot).await
1992 }
1993 InternalsOpts::LoopbackCleanupHelper { device } => {
1994 crate::blockdev::run_loopback_cleanup_helper(&device).await
1995 }
1996 InternalsOpts::AllocateCleanupLoopback { file_path: _ } => {
1997 let temp_file =
1999 tempfile::NamedTempFile::new().context("Failed to create temporary file")?;
2000 let temp_path = temp_file.path();
2001
2002 let loopback = crate::blockdev::LoopbackDevice::new(temp_path)
2004 .context("Failed to create loopback device")?;
2005
2006 println!("Created loopback device: {}", loopback.path());
2007
2008 loopback
2010 .close()
2011 .context("Failed to close loopback device")?;
2012
2013 println!("Successfully closed loopback device");
2014 Ok(())
2015 }
2016 #[cfg(feature = "rhsm")]
2017 InternalsOpts::PublishRhsmFacts => crate::rhsm::publish_facts(&root).await,
2018 #[cfg(feature = "docgen")]
2019 InternalsOpts::DumpCliJson => {
2020 use clap::CommandFactory;
2021 let cmd = Opt::command();
2022 let json = crate::cli_json::dump_cli_json(&cmd)?;
2023 println!("{}", json);
2024 Ok(())
2025 }
2026 InternalsOpts::DirDiff {
2027 pristine_etc,
2028 current_etc,
2029 new_etc,
2030 merge,
2031 } => {
2032 let pristine_etc =
2033 Dir::open_ambient_dir(pristine_etc, cap_std::ambient_authority())?;
2034 let current_etc = Dir::open_ambient_dir(current_etc, cap_std::ambient_authority())?;
2035 let new_etc = Dir::open_ambient_dir(new_etc, cap_std::ambient_authority())?;
2036
2037 let (p, c, n) =
2038 etc_merge::traverse_etc(&pristine_etc, ¤t_etc, Some(&new_etc))?;
2039
2040 let n = n
2041 .as_ref()
2042 .ok_or_else(|| anyhow::anyhow!("Failed to get new directory tree"))?;
2043
2044 let diff = compute_diff(&p, &c, &n)?;
2045 print_diff(&diff, &mut std::io::stdout());
2046
2047 if merge {
2048 etc_merge::merge(¤t_etc, &c, &new_etc, &n, &diff)?;
2049 }
2050
2051 Ok(())
2052 }
2053 InternalsOpts::PrepSoftReboot {
2054 deployment,
2055 reboot,
2056 reset,
2057 } => {
2058 let storage = &get_storage().await?;
2059
2060 match storage.kind()? {
2061 BootedStorageKind::Ostree(..) => {
2062 anyhow::bail!("soft-reboot only implemented for composefs")
2064 }
2065
2066 BootedStorageKind::Composefs(booted_cfs) => {
2067 if reset {
2068 return reset_soft_reboot();
2069 }
2070
2071 prepare_soft_reboot_composefs(
2072 &storage,
2073 &booted_cfs,
2074 deployment.as_deref(),
2075 SoftRebootMode::Required,
2076 reboot,
2077 )
2078 .await
2079 }
2080 }
2081 }
2082 InternalsOpts::ComposefsGC { dry_run } => {
2083 let storage = &get_storage().await?;
2084
2085 match storage.kind()? {
2086 BootedStorageKind::Ostree(..) => {
2087 anyhow::bail!("composefs-gc only works for composefs backend");
2088 }
2089
2090 BootedStorageKind::Composefs(booted_cfs) => {
2091 let gc_result = composefs_gc(storage, &booted_cfs, dry_run).await?;
2092
2093 if dry_run {
2094 println!("Dry run (no files deleted)");
2095 }
2096
2097 println!(
2098 "Objects: {} removed ({} bytes)",
2099 gc_result.objects_removed, gc_result.objects_bytes
2100 );
2101
2102 if gc_result.images_pruned > 0 || gc_result.streams_pruned > 0 {
2103 println!(
2104 "Pruned symlinks: {} images, {} streams",
2105 gc_result.images_pruned, gc_result.streams_pruned
2106 );
2107 }
2108
2109 Ok(())
2110 }
2111 }
2112 }
2113 },
2114 Opt::State(opts) => match opts {
2115 StateOpts::WipeOstree => {
2116 let sysroot = ostree::Sysroot::new_default();
2117 sysroot.load(gio::Cancellable::NONE)?;
2118 crate::deploy::wipe_ostree(sysroot).await?;
2119 Ok(())
2120 }
2121 },
2122
2123 Opt::ComposefsFinalizeStaged => {
2124 let storage = &get_storage().await?;
2125 match storage.kind()? {
2126 BootedStorageKind::Ostree(_) => {
2127 anyhow::bail!("ComposefsFinalizeStaged is only supported for composefs backend")
2128 }
2129 BootedStorageKind::Composefs(booted_cfs) => {
2130 composefs_backend_finalize(storage, &booted_cfs).await
2131 }
2132 }
2133 }
2134
2135 Opt::ConfigDiff => {
2136 let storage = &get_storage().await?;
2137 match storage.kind()? {
2138 BootedStorageKind::Ostree(_) => {
2139 anyhow::bail!("ConfigDiff is only supported for composefs backend")
2140 }
2141 BootedStorageKind::Composefs(booted_cfs) => {
2142 get_etc_diff(storage, &booted_cfs).await
2143 }
2144 }
2145 }
2146
2147 Opt::DeleteDeployment { depl_id } => {
2148 let storage = &get_storage().await?;
2149 match storage.kind()? {
2150 BootedStorageKind::Ostree(_) => {
2151 anyhow::bail!("DeleteDeployment is only supported for composefs backend")
2152 }
2153 BootedStorageKind::Composefs(booted_cfs) => {
2154 delete_composefs_deployment(&depl_id, storage, &booted_cfs).await
2155 }
2156 }
2157 }
2158 }
2159}
2160
2161#[cfg(test)]
2162mod tests {
2163 use super::*;
2164
2165 #[test]
2166 fn test_callname() {
2167 use std::os::unix::ffi::OsStrExt;
2168
2169 let mapped_cases = [
2171 ("", "bootc"),
2172 ("/foo/bar", "bar"),
2173 ("/foo/bar/", "bar"),
2174 ("foo/bar", "bar"),
2175 ("../foo/bar", "bar"),
2176 ("usr/bin/ostree-container", "ostree-container"),
2177 ];
2178 for (input, output) in mapped_cases {
2179 assert_eq!(
2180 output,
2181 callname_from_argv0(OsStr::new(input)),
2182 "Handling mapped case {input}"
2183 );
2184 }
2185
2186 assert_eq!("bootc", callname_from_argv0(OsStr::from_bytes(b"foo\x80")));
2188
2189 let ident_cases = ["foo", "bootc"];
2191 for case in ident_cases {
2192 assert_eq!(
2193 case,
2194 callname_from_argv0(OsStr::new(case)),
2195 "Handling ident case {case}"
2196 );
2197 }
2198 }
2199
2200 #[test]
2201 fn test_parse_install_args() {
2202 let o = Opt::try_parse_from([
2204 "bootc",
2205 "install",
2206 "to-filesystem",
2207 "--target-no-signature-verification",
2208 "/target",
2209 ])
2210 .unwrap();
2211 let o = match o {
2212 Opt::Install(InstallOpts::ToFilesystem(fsopts)) => fsopts,
2213 o => panic!("Expected filesystem opts, not {o:?}"),
2214 };
2215 assert!(o.target_opts.target_no_signature_verification);
2216 assert_eq!(o.filesystem_opts.root_path.as_str(), "/target");
2217 assert_eq!(
2219 o.config_opts.bound_images,
2220 crate::install::BoundImagesOpt::Stored
2221 );
2222 }
2223
2224 #[test]
2225 fn test_parse_opts() {
2226 assert!(matches!(
2227 Opt::parse_including_static(["bootc", "status"]),
2228 Opt::Status(StatusOpts {
2229 json: false,
2230 format: None,
2231 format_version: None,
2232 booted: false,
2233 verbose: false
2234 })
2235 ));
2236 assert!(matches!(
2237 Opt::parse_including_static(["bootc", "status", "--format-version=0"]),
2238 Opt::Status(StatusOpts {
2239 format_version: Some(0),
2240 ..
2241 })
2242 ));
2243
2244 assert!(matches!(
2246 Opt::parse_including_static(["bootc", "status", "--verbose"]),
2247 Opt::Status(StatusOpts { verbose: true, .. })
2248 ));
2249
2250 assert!(matches!(
2252 Opt::parse_including_static(["bootc", "status", "-v"]),
2253 Opt::Status(StatusOpts { verbose: true, .. })
2254 ));
2255 }
2256
2257 #[test]
2258 fn test_parse_generator() {
2259 assert!(matches!(
2260 Opt::parse_including_static([
2261 "/usr/lib/systemd/system/bootc-systemd-generator",
2262 "/run/systemd/system"
2263 ]),
2264 Opt::Internals(InternalsOpts::SystemdGenerator { normal_dir, .. }) if normal_dir == "/run/systemd/system"
2265 ));
2266 }
2267
2268 #[test]
2269 fn test_parse_ostree_ext() {
2270 assert!(matches!(
2271 Opt::parse_including_static(["bootc", "internals", "ostree-container"]),
2272 Opt::Internals(InternalsOpts::OstreeContainer { .. })
2273 ));
2274
2275 fn peel(o: Opt) -> Vec<OsString> {
2276 match o {
2277 Opt::Internals(InternalsOpts::OstreeExt { args }) => args,
2278 o => panic!("unexpected {o:?}"),
2279 }
2280 }
2281 let args = peel(Opt::parse_including_static([
2282 "/usr/libexec/libostree/ext/ostree-ima-sign",
2283 "ima-sign",
2284 "--repo=foo",
2285 "foo",
2286 "bar",
2287 "baz",
2288 ]));
2289 assert_eq!(
2290 args.as_slice(),
2291 ["ima-sign", "--repo=foo", "foo", "bar", "baz"]
2292 );
2293
2294 let args = peel(Opt::parse_including_static([
2295 "/usr/libexec/libostree/ext/ostree-container",
2296 "container",
2297 "image",
2298 "pull",
2299 ]));
2300 assert_eq!(args.as_slice(), ["container", "image", "pull"]);
2301 }
2302
2303 #[test]
2304 fn test_parse_upgrade_options() {
2305 let o = Opt::try_parse_from(["bootc", "upgrade", "--tag", "v1.1"]).unwrap();
2307 match o {
2308 Opt::Upgrade(opts) => {
2309 assert_eq!(opts.tag, Some("v1.1".to_string()));
2310 }
2311 _ => panic!("Expected Upgrade variant"),
2312 }
2313
2314 let o = Opt::try_parse_from(["bootc", "upgrade", "--tag", "v1.1", "--check"]).unwrap();
2316 match o {
2317 Opt::Upgrade(opts) => {
2318 assert_eq!(opts.tag, Some("v1.1".to_string()));
2319 assert!(opts.check);
2320 }
2321 _ => panic!("Expected Upgrade variant"),
2322 }
2323 }
2324
2325 #[test]
2326 fn test_image_reference_with_tag() {
2327 let current = ImageReference {
2329 image: "quay.io/example/myapp:v1.0".to_string(),
2330 transport: "registry".to_string(),
2331 signature: None,
2332 };
2333 let result = current.with_tag("v1.1").unwrap();
2334 assert_eq!(result.image, "quay.io/example/myapp:v1.1");
2335 assert_eq!(result.transport, "registry");
2336
2337 let current_with_digest = ImageReference {
2339 image: "quay.io/example/myapp:v1.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(),
2340 transport: "registry".to_string(),
2341 signature: None,
2342 };
2343 let result = current_with_digest.with_tag("v2.0").unwrap();
2344 assert_eq!(result.image, "quay.io/example/myapp:v2.0");
2345
2346 let containers_storage = ImageReference {
2348 image: "localhost/myapp:v1.0".to_string(),
2349 transport: "containers-storage".to_string(),
2350 signature: None,
2351 };
2352 let result = containers_storage.with_tag("v1.1").unwrap();
2353 assert_eq!(result.image, "localhost/myapp:v1.1");
2354 assert_eq!(result.transport, "containers-storage");
2355
2356 let containers_storage_with_digest = ImageReference {
2358 image:
2359 "localhost/myapp:v1.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
2360 .to_string(),
2361 transport: "containers-storage".to_string(),
2362 signature: None,
2363 };
2364 let result = containers_storage_with_digest.with_tag("v2.0").unwrap();
2365 assert_eq!(result.image, "localhost/myapp:v2.0");
2366 assert_eq!(result.transport, "containers-storage");
2367
2368 let no_tag = ImageReference {
2370 image: "localhost/myapp".to_string(),
2371 transport: "containers-storage".to_string(),
2372 signature: None,
2373 };
2374 let result = no_tag.with_tag("v1.0").unwrap();
2375 assert_eq!(result.image, "localhost/myapp:v1.0");
2376 assert_eq!(result.transport, "containers-storage");
2377 }
2378
2379 #[test]
2380 fn test_generate_completion_scripts_contain_commands() {
2381 use clap_complete::aot::{Shell, generate};
2382
2383 let want = ["install", "upgrade"];
2392
2393 for shell in [Shell::Bash, Shell::Zsh, Shell::Fish] {
2394 let mut cmd = Opt::command();
2395 let mut buf = Vec::new();
2396 generate(shell, &mut cmd, "bootc", &mut buf);
2397 let s = String::from_utf8(buf).expect("completion should be utf8");
2398 for w in &want {
2399 assert!(s.contains(w), "{shell:?} completion missing {w}");
2400 }
2401 }
2402 }
2403}