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 clap::CommandFactory;
17use clap::Parser;
18use clap::ValueEnum;
19use composefs::dumpfile;
20use composefs::fsverity;
21use composefs::fsverity::FsVerityHashValue;
22use composefs_ctl::composefs;
23use composefs_ctl::composefs_boot;
24use composefs_ctl::composefs_oci;
25
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(long)]
443 write_dumpfile_to: Option<Utf8PathBuf>,
444
445 #[clap(last = true)]
447 args: Vec<OsString>,
448 },
449 #[clap(hide = true)]
457 Export {
458 #[clap(long, default_value = "tar")]
460 format: ExportFormat,
461
462 #[clap(long, short = 'o')]
464 output: Option<Utf8PathBuf>,
465
466 #[clap(long)]
469 kernel_in_boot: bool,
470
471 #[clap(long)]
473 disable_selinux: bool,
474
475 target: Utf8PathBuf,
477 },
478}
479
480#[derive(Debug, Clone, ValueEnum, PartialEq, Eq)]
481pub(crate) enum ExportFormat {
482 Tar,
484}
485
486#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
488pub(crate) enum ImageCmdOpts {
489 List {
491 #[clap(allow_hyphen_values = true)]
492 args: Vec<OsString>,
493 },
494 Build {
496 #[clap(allow_hyphen_values = true)]
497 args: Vec<OsString>,
498 },
499 Pull {
501 #[clap(required = true)]
503 images: Vec<String>,
504 },
505 Push {
507 #[clap(allow_hyphen_values = true)]
508 args: Vec<OsString>,
509 },
510}
511
512#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
513#[serde(rename_all = "kebab-case")]
514pub(crate) enum ImageListType {
515 #[default]
517 All,
518 Logical,
520 Host,
522}
523
524impl std::fmt::Display for ImageListType {
525 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
526 self.to_possible_value().unwrap().get_name().fmt(f)
527 }
528}
529
530#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
531#[serde(rename_all = "kebab-case")]
532pub(crate) enum ImageListFormat {
533 #[default]
535 Table,
536 Json,
538}
539impl std::fmt::Display for ImageListFormat {
540 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
541 self.to_possible_value().unwrap().get_name().fmt(f)
542 }
543}
544
545#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
547pub(crate) enum ImageOpts {
548 List {
552 #[clap(long = "type")]
554 #[arg(default_value_t)]
555 list_type: ImageListType,
556 #[clap(long = "format")]
557 #[arg(default_value_t)]
558 list_format: ImageListFormat,
559 },
560 CopyToStorage {
577 #[clap(long)]
578 source: Option<String>,
580
581 #[clap(long)]
582 target: Option<String>,
585 },
586 SetUnified,
591 PullFromDefaultStorage {
593 image: String,
595 },
596 #[clap(subcommand)]
598 Cmd(ImageCmdOpts),
599}
600
601#[derive(Debug, Clone, clap::ValueEnum, PartialEq, Eq)]
602pub(crate) enum SchemaType {
603 Host,
604 Progress,
605}
606
607#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
609pub(crate) enum FsverityOpts {
610 Measure {
612 path: Utf8PathBuf,
614 },
615 Enable {
617 path: Utf8PathBuf,
619 },
620}
621
622#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
624pub(crate) enum InternalsOpts {
625 SystemdGenerator {
626 normal_dir: Utf8PathBuf,
627 #[allow(dead_code)]
628 early_dir: Option<Utf8PathBuf>,
629 #[allow(dead_code)]
630 late_dir: Option<Utf8PathBuf>,
631 },
632 FixupEtcFstab,
633 PrintJsonSchema {
635 #[clap(long)]
636 of: SchemaType,
637 },
638 #[clap(subcommand)]
639 Fsverity(FsverityOpts),
640 Fsck,
642 Cleanup,
644 Relabel {
645 #[clap(long)]
646 as_path: Option<Utf8PathBuf>,
648
649 path: Utf8PathBuf,
651 },
652 RelabelOverlayMountpoints,
655 OstreeExt {
657 #[clap(allow_hyphen_values = true)]
658 args: Vec<OsString>,
659 },
660 Cfs {
662 #[clap(allow_hyphen_values = true)]
663 args: Vec<OsString>,
664 },
665 OstreeContainer {
667 #[clap(allow_hyphen_values = true)]
668 args: Vec<OsString>,
669 },
670 TestComposefs,
672 LoopbackCleanupHelper {
674 #[clap(long)]
676 device: String,
677 },
678 AllocateCleanupLoopback {
680 #[clap(long)]
682 file_path: Utf8PathBuf,
683 },
684 BootcInstallCompletion {
686 sysroot: Utf8PathBuf,
688
689 stateroot: String,
691 },
692 Reboot,
695 #[cfg(feature = "rhsm")]
696 PublishRhsmFacts,
698 DirDiff {
700 pristine_etc: Utf8PathBuf,
702 current_etc: Utf8PathBuf,
704 new_etc: Utf8PathBuf,
706 #[clap(long)]
708 merge: bool,
709 },
710 #[cfg(feature = "docgen")]
711 DumpCliJson,
713 PrepSoftReboot {
714 #[clap(required_unless_present = "reset")]
715 deployment: Option<String>,
716 #[clap(long, conflicts_with = "reset")]
717 reboot: bool,
718 #[clap(long, conflicts_with = "reboot")]
719 reset: bool,
720 },
721 ComposefsGC {
722 #[clap(long)]
723 dry_run: bool,
724 #[clap(long)]
727 assert_no_op: bool,
728 #[clap(long)]
730 prune_repo: bool,
731 },
732 #[clap(subcommand)]
734 Blockdev(BlockdevOpts),
735}
736
737#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
739pub(crate) enum BlockdevOpts {
740 Ls {
746 device: Utf8PathBuf,
748 },
749 LsFilesystem {
754 path: Utf8PathBuf,
756 },
757}
758
759#[derive(Debug, Parser, PartialEq, Eq)]
761pub(crate) struct SetOptionsForSourceOpts {
762 #[clap(long)]
767 pub(crate) source: String,
768
769 #[clap(long)]
774 pub(crate) options: Option<String>,
775}
776
777#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
785pub(crate) enum LoaderEntriesOpts {
786 SetOptionsForSource(SetOptionsForSourceOpts),
805}
806
807#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
808pub(crate) enum StateOpts {
809 WipeOstree,
811}
812
813impl InternalsOpts {
814 const GENERATOR_BIN: &'static str = "bootc-systemd-generator";
816}
817
818#[derive(Debug, Parser, PartialEq, Eq)]
826#[clap(name = "bootc")]
827#[clap(rename_all = "kebab-case")]
828#[clap(version,long_version=clap::crate_version!())]
829#[allow(clippy::large_enum_variant)]
830pub(crate) enum Opt {
831 #[clap(alias = "update")]
844 Upgrade(UpgradeOpts),
845 Switch(SwitchOpts),
856 #[command(after_help = indoc! {r#"
868 Note on Rollbacks and the `/etc` Directory:
869
870 When you perform a rollback (e.g., with `bootc rollback`), any
871 changes made to files in the `/etc` directory won't carry over
872 to the rolled-back deployment. The `/etc` files will revert
873 to their state from that previous deployment instead.
874
875 This is because `bootc rollback` just reorders the existing
876 deployments. It doesn't create new deployments. The `/etc`
877 merges happen when new deployments are created.
878 "#})]
879 Rollback(RollbackOpts),
880 Edit(EditOpts),
890 Status(StatusOpts),
894 #[clap(alias = "usroverlay")]
898 UsrOverlay(UsrOverlayOpts),
899 #[clap(subcommand)]
903 Install(InstallOpts),
904 #[clap(subcommand)]
906 Container(ContainerOpts),
907 #[clap(subcommand, hide = true)]
911 Image(ImageOpts),
912 #[clap(subcommand)]
916 LoaderEntries(LoaderEntriesOpts),
917 #[clap(hide = true)]
919 ExecInHostMountNamespace {
920 #[clap(trailing_var_arg = true, allow_hyphen_values = true)]
921 args: Vec<OsString>,
922 },
923 #[clap(hide = true)]
925 #[clap(subcommand)]
926 State(StateOpts),
927 #[clap(subcommand)]
928 #[clap(hide = true)]
929 Internals(InternalsOpts),
930 ComposefsFinalizeStaged,
931 #[clap(hide = true)]
933 ConfigDiff,
934 #[clap(hide = true)]
938 Completion {
939 #[clap(value_enum)]
941 shell: clap_complete::aot::Shell,
942 },
943 #[clap(hide = true)]
944 DeleteDeployment {
945 depl_id: String,
946 },
947}
948
949#[context("Ensuring mountns")]
954pub(crate) fn ensure_self_unshared_mount_namespace() -> Result<()> {
955 let uid = rustix::process::getuid();
956 if !uid.is_root() {
957 tracing::debug!("Not root, assuming no need to unshare");
958 return Ok(());
959 }
960 let recurse_env = "_ostree_unshared";
961 let ns_pid1 = std::fs::read_link("/proc/1/ns/mnt").context("Reading /proc/1/ns/mnt")?;
962 let ns_self = std::fs::read_link("/proc/self/ns/mnt").context("Reading /proc/self/ns/mnt")?;
963 if ns_pid1 != ns_self {
965 tracing::debug!("Already in a mount namespace");
966 return Ok(());
967 }
968 if std::env::var_os(recurse_env).is_some() {
969 let am_pid1 = rustix::process::getpid().is_init();
970 if am_pid1 {
971 tracing::debug!("We are pid 1");
972 return Ok(());
973 } else {
974 anyhow::bail!("Failed to unshare mount namespace");
975 }
976 }
977 bootc_utils::reexec::reexec_with_guardenv(recurse_env, &["unshare", "-m", "--"])
978}
979
980#[context("Initializing storage")]
983pub(crate) async fn get_storage() -> Result<crate::store::BootedStorage> {
984 let env = crate::store::Environment::detect()?;
985 prepare_for_write()?;
988 let r = BootedStorage::new(env)
989 .await?
990 .ok_or_else(|| anyhow!("System not booted via bootc"))?;
991 Ok(r)
992}
993
994#[context("Querying root privilege")]
995pub(crate) fn require_root(is_container: bool) -> Result<()> {
996 ensure!(
997 rustix::process::getuid().is_root(),
998 if is_container {
999 "The user inside the container from which you are running this command must be root"
1000 } else {
1001 "This command must be executed as the root user"
1002 }
1003 );
1004
1005 ensure!(
1006 rustix::thread::capability_is_in_bounding_set(rustix::thread::CapabilitySet::SYS_ADMIN)?,
1007 if is_container {
1008 "The container must be executed with full privileges (e.g. --privileged flag)"
1009 } else {
1010 "This command requires full root privileges (CAP_SYS_ADMIN)"
1011 }
1012 );
1013
1014 tracing::trace!("Verified uid 0 with CAP_SYS_ADMIN");
1015
1016 Ok(())
1017}
1018
1019fn has_soft_reboot_capability(deployment: Option<&crate::spec::BootEntry>) -> bool {
1021 deployment.map(|d| d.soft_reboot_capable).unwrap_or(false)
1022}
1023
1024#[context("Preparing soft reboot")]
1026fn prepare_soft_reboot(sysroot: &SysrootLock, deployment: &ostree::Deployment) -> Result<()> {
1027 let cancellable = ostree::gio::Cancellable::NONE;
1028 sysroot
1029 .deployment_set_soft_reboot(deployment, false, cancellable)
1030 .context("Failed to prepare soft-reboot")?;
1031 Ok(())
1032}
1033
1034#[context("Handling soft reboot")]
1036fn handle_soft_reboot<F>(
1037 soft_reboot_mode: Option<SoftRebootMode>,
1038 entry: Option<&crate::spec::BootEntry>,
1039 deployment_type: &str,
1040 execute_soft_reboot: F,
1041) -> Result<()>
1042where
1043 F: FnOnce() -> Result<()>,
1044{
1045 let Some(mode) = soft_reboot_mode else {
1046 return Ok(());
1047 };
1048
1049 let can_soft_reboot = has_soft_reboot_capability(entry);
1050 match mode {
1051 SoftRebootMode::Required => {
1052 if can_soft_reboot {
1053 execute_soft_reboot()?;
1054 } else {
1055 anyhow::bail!(
1056 "Soft reboot was required but {} deployment is not soft-reboot capable",
1057 deployment_type
1058 );
1059 }
1060 }
1061 SoftRebootMode::Auto => {
1062 if can_soft_reboot {
1063 execute_soft_reboot()?;
1064 }
1065 }
1066 }
1067 Ok(())
1068}
1069
1070#[context("Handling staged soft reboot")]
1072fn handle_staged_soft_reboot(
1073 booted_ostree: &BootedOstree<'_>,
1074 soft_reboot_mode: Option<SoftRebootMode>,
1075 host: &crate::spec::Host,
1076) -> Result<()> {
1077 handle_soft_reboot(
1078 soft_reboot_mode,
1079 host.status.staged.as_ref(),
1080 "staged",
1081 || soft_reboot_staged(booted_ostree.sysroot),
1082 )
1083}
1084
1085#[context("Soft reboot staged deployment")]
1087fn soft_reboot_staged(sysroot: &SysrootLock) -> Result<()> {
1088 println!("Staged deployment is soft-reboot capable, preparing for soft-reboot...");
1089
1090 let deployments_list = sysroot.deployments();
1091 let staged_deployment = deployments_list
1092 .iter()
1093 .find(|d| d.is_staged())
1094 .ok_or_else(|| anyhow::anyhow!("Failed to find staged deployment"))?;
1095
1096 prepare_soft_reboot(sysroot, staged_deployment)?;
1097 Ok(())
1098}
1099
1100#[context("Soft reboot rollback deployment")]
1102fn soft_reboot_rollback(booted_ostree: &BootedOstree<'_>) -> Result<()> {
1103 println!("Rollback deployment is soft-reboot capable, preparing for soft-reboot...");
1104
1105 let deployments_list = booted_ostree.sysroot.deployments();
1106 let target_deployment = deployments_list
1107 .first()
1108 .ok_or_else(|| anyhow::anyhow!("No rollback deployment found!"))?;
1109
1110 prepare_soft_reboot(booted_ostree.sysroot, target_deployment)
1111}
1112
1113#[context("Preparing for write")]
1117pub(crate) fn prepare_for_write() -> Result<()> {
1118 use std::sync::atomic::{AtomicBool, Ordering};
1119
1120 static ENTERED: AtomicBool = AtomicBool::new(false);
1126 if ENTERED.load(Ordering::SeqCst) {
1127 return Ok(());
1128 }
1129 if ostree_ext::container_utils::running_in_container() {
1130 anyhow::bail!("Detected container; this command requires a booted host system.");
1131 }
1132 crate::cli::require_root(false)?;
1133 ensure_self_unshared_mount_namespace()?;
1134 if crate::lsm::selinux_enabled()? && !crate::lsm::selinux_ensure_install()? {
1135 tracing::debug!("Do not have install_t capabilities");
1136 }
1137 ENTERED.store(true, Ordering::SeqCst);
1138 Ok(())
1139}
1140
1141#[context("Upgrading")]
1143async fn upgrade(
1144 opts: UpgradeOpts,
1145 storage: &Storage,
1146 booted_ostree: &BootedOstree<'_>,
1147) -> Result<()> {
1148 let repo = &booted_ostree.repo();
1149
1150 let host = crate::status::get_status(booted_ostree)?.1;
1151 let current_image = host.spec.image.as_ref();
1152
1153 let derived_image = if let Some(ref tag) = opts.tag {
1155 let image = current_image.ok_or_else(|| {
1156 anyhow::anyhow!("--tag requires a booted image with a specified source")
1157 })?;
1158 Some(image.with_tag(tag)?)
1159 } else {
1160 None
1161 };
1162
1163 let imgref = derived_image.as_ref().or(current_image);
1164 let prog: ProgressWriter = opts.progress.try_into()?;
1165
1166 if imgref.is_none() {
1168 let booted_incompatible = host.status.booted.as_ref().is_some_and(|b| b.incompatible);
1169
1170 let staged_incompatible = host.status.staged.as_ref().is_some_and(|b| b.incompatible);
1171
1172 if booted_incompatible || staged_incompatible {
1173 return Err(anyhow::anyhow!(
1174 "Deployment contains local rpm-ostree modifications; cannot upgrade via bootc. You can run `rpm-ostree reset` to undo the modifications."
1175 ));
1176 }
1177 }
1178
1179 let imgref = imgref.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
1180 let spec = RequiredHostSpec { image: imgref };
1182 let booted_image = host
1183 .status
1184 .booted
1185 .as_ref()
1186 .map(|b| b.query_image(repo))
1187 .transpose()?
1188 .flatten();
1189 let staged = host.status.staged.as_ref();
1191 let staged_image = staged.as_ref().and_then(|s| s.image.as_ref());
1192 let mut changed = false;
1193
1194 if opts.from_downloaded {
1196 let ostree = storage.get_ostree()?;
1197 let staged_deployment = ostree
1198 .staged_deployment()
1199 .ok_or_else(|| anyhow::anyhow!("No staged deployment found"))?;
1200
1201 if staged_deployment.is_finalization_locked() {
1202 ostree.change_finalization(&staged_deployment)?;
1203 println!("Staged deployment will now be applied on reboot");
1204 } else {
1205 println!("Staged deployment is already set to apply on reboot");
1206 }
1207
1208 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &host)?;
1209 if opts.apply {
1210 crate::reboot::reboot()?;
1211 }
1212 return Ok(());
1213 }
1214
1215 let use_unified = crate::deploy::image_exists_in_unified_storage(storage, imgref).await?;
1219
1220 if opts.check {
1221 let ostree_imgref = imgref.clone().into();
1222 let mut imp =
1223 crate::deploy::new_importer(repo, &ostree_imgref, Some(&booted_ostree.deployment))
1224 .await?;
1225 match imp.prepare().await? {
1226 PrepareResult::AlreadyPresent(_) => {
1227 println!("No changes in: {ostree_imgref:#}");
1228 }
1229 PrepareResult::Ready(r) => {
1230 crate::deploy::check_bootc_label(&r.config);
1231 println!("Update available for: {ostree_imgref:#}");
1232 if let Some(version) = r.version() {
1233 println!(" Version: {version}");
1234 }
1235 println!(" Digest: {}", r.manifest_digest);
1236 changed = true;
1237 if let Some(previous_image) = booted_image.as_ref() {
1238 let diff =
1239 ostree_container::ManifestDiff::new(&previous_image.manifest, &r.manifest);
1240 diff.print();
1241 }
1242 }
1243 }
1244 } else {
1245 let fetched = if use_unified {
1246 crate::deploy::pull_unified(
1247 repo,
1248 imgref,
1249 None,
1250 opts.quiet,
1251 prog.clone(),
1252 storage,
1253 Some(&booted_ostree.deployment),
1254 )
1255 .await?
1256 } else {
1257 crate::deploy::pull(
1258 repo,
1259 imgref,
1260 None,
1261 opts.quiet,
1262 prog.clone(),
1263 Some(&booted_ostree.deployment),
1264 )
1265 .await?
1266 };
1267 let staged_digest = staged_image.map(|s| s.digest().expect("valid digest in status"));
1268 let fetched_digest = &fetched.manifest_digest;
1269 tracing::debug!("staged: {staged_digest:?}");
1270 tracing::debug!("fetched: {fetched_digest}");
1271 let staged_unchanged = staged_digest
1272 .as_ref()
1273 .map(|d| d == fetched_digest)
1274 .unwrap_or_default();
1275 let booted_unchanged = booted_image
1276 .as_ref()
1277 .map(|img| &img.manifest_digest == fetched_digest)
1278 .unwrap_or_default();
1279 if staged_unchanged {
1280 let staged_deployment = storage.get_ostree()?.staged_deployment();
1281 let mut download_only_changed = false;
1282
1283 if let Some(staged) = staged_deployment {
1284 if opts.download_only {
1286 if !staged.is_finalization_locked() {
1288 storage.get_ostree()?.change_finalization(&staged)?;
1289 println!("Image downloaded, but will not be applied on reboot");
1290 download_only_changed = true;
1291 }
1292 } else if !opts.check {
1293 if staged.is_finalization_locked() {
1296 storage.get_ostree()?.change_finalization(&staged)?;
1297 println!("Staged deployment will now be applied on reboot");
1298 download_only_changed = true;
1299 }
1300 }
1301 } else if opts.download_only || opts.apply {
1302 anyhow::bail!("No staged deployment found");
1303 }
1304
1305 if !download_only_changed {
1306 println!("Staged update present, not changed");
1307 }
1308
1309 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &host)?;
1310 if opts.apply {
1311 crate::reboot::reboot()?;
1312 }
1313 } else if booted_unchanged {
1314 println!("No update available.")
1315 } else {
1316 let stateroot = booted_ostree.stateroot();
1317 let from = MergeState::from_stateroot(storage, &stateroot)?;
1318 crate::deploy::stage(
1319 storage,
1320 from,
1321 &fetched,
1322 &spec,
1323 prog.clone(),
1324 opts.download_only,
1325 )
1326 .await?;
1327 changed = true;
1328 if let Some(prev) = booted_image.as_ref() {
1329 if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
1330 let diff =
1331 ostree_container::ManifestDiff::new(&prev.manifest, &fetched_manifest);
1332 diff.print();
1333 }
1334 }
1335 }
1336 }
1337 if changed {
1338 storage.update_mtime()?;
1339
1340 if opts.soft_reboot.is_some() {
1341 let updated_host = crate::status::get_status(booted_ostree)?.1;
1344 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &updated_host)?;
1345 }
1346
1347 if opts.apply {
1348 crate::reboot::reboot()?;
1349 }
1350 } else {
1351 tracing::debug!("No changes");
1352 }
1353
1354 Ok(())
1355}
1356pub(crate) fn imgref_for_switch(opts: &SwitchOpts) -> Result<ImageReference> {
1357 let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
1358 let imgref = ostree_container::ImageReference {
1359 transport,
1360 name: opts.target.to_string(),
1361 };
1362 let sigverify = sigpolicy_from_opt(opts.enforce_container_sigpolicy);
1363 let target = ostree_container::OstreeImageReference { sigverify, imgref };
1364 let target = ImageReference::from(target);
1365
1366 return Ok(target);
1367}
1368
1369#[context("Switching (ostree)")]
1371async fn switch_ostree(
1372 opts: SwitchOpts,
1373 storage: &Storage,
1374 booted_ostree: &BootedOstree<'_>,
1375) -> Result<()> {
1376 let target = imgref_for_switch(&opts)?;
1377 let prog: ProgressWriter = opts.progress.try_into()?;
1378 let cancellable = gio::Cancellable::NONE;
1379
1380 let repo = &booted_ostree.repo();
1381 let (_, host) = crate::status::get_status(booted_ostree)?;
1382
1383 let new_spec = {
1384 let mut new_spec = host.spec.clone();
1385 new_spec.image = Some(target.clone());
1386 new_spec
1387 };
1388
1389 if new_spec == host.spec {
1390 println!("Image specification is unchanged.");
1391 return Ok(());
1392 }
1393
1394 const SWITCH_JOURNAL_ID: &str = "7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1";
1396 let old_image = host
1397 .spec
1398 .image
1399 .as_ref()
1400 .map(|i| i.image.as_str())
1401 .unwrap_or("none");
1402
1403 tracing::info!(
1404 message_id = SWITCH_JOURNAL_ID,
1405 bootc.old_image_reference = old_image,
1406 bootc.new_image_reference = &target.image,
1407 bootc.new_image_transport = &target.transport,
1408 "Switching from image {} to {}",
1409 old_image,
1410 target.image
1411 );
1412
1413 let new_spec = RequiredHostSpec::from_spec(&new_spec)?;
1414
1415 let use_unified = if opts.unified_storage_exp {
1419 true
1420 } else {
1421 crate::deploy::image_exists_in_unified_storage(storage, &target).await?
1422 };
1423
1424 let fetched = if use_unified {
1425 crate::deploy::pull_unified(
1426 repo,
1427 &target,
1428 None,
1429 opts.quiet,
1430 prog.clone(),
1431 storage,
1432 Some(&booted_ostree.deployment),
1433 )
1434 .await?
1435 } else {
1436 crate::deploy::pull(
1437 repo,
1438 &target,
1439 None,
1440 opts.quiet,
1441 prog.clone(),
1442 Some(&booted_ostree.deployment),
1443 )
1444 .await?
1445 };
1446
1447 if !opts.retain {
1448 if let Some(booted_origin) = booted_ostree.deployment.origin() {
1450 if let Some(ostree_ref) = booted_origin.optional_string("origin", "refspec")? {
1451 let (remote, ostree_ref) =
1452 ostree::parse_refspec(&ostree_ref).context("Failed to parse ostree ref")?;
1453 repo.set_ref_immediate(remote.as_deref(), &ostree_ref, None, cancellable)?;
1454 }
1455 }
1456 }
1457
1458 let stateroot = booted_ostree.stateroot();
1459 let from = MergeState::from_stateroot(storage, &stateroot)?;
1460 crate::deploy::stage(storage, from, &fetched, &new_spec, prog.clone(), false).await?;
1461
1462 storage.update_mtime()?;
1463
1464 if opts.soft_reboot.is_some() {
1465 let updated_host = crate::status::get_status(booted_ostree)?.1;
1468 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &updated_host)?;
1469 }
1470
1471 if opts.apply {
1472 crate::reboot::reboot()?;
1473 }
1474
1475 Ok(())
1476}
1477
1478#[context("Switching")]
1480async fn switch(opts: SwitchOpts) -> Result<()> {
1481 if opts.mutate_in_place {
1485 let target = imgref_for_switch(&opts)?;
1486 let deployid = {
1487 let target = target.clone();
1489 let root = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1490 tokio::task::spawn_blocking(move || {
1491 crate::deploy::switch_origin_inplace(&root, &target)
1492 })
1493 .await??
1494 };
1495 println!("Updated {deployid} to pull from {target}");
1496 return Ok(());
1497 }
1498 let storage = &get_storage().await?;
1499 match storage.kind()? {
1500 BootedStorageKind::Ostree(booted_ostree) => {
1501 switch_ostree(opts, storage, &booted_ostree).await
1502 }
1503 BootedStorageKind::Composefs(booted_cfs) => {
1504 switch_composefs(opts, storage, &booted_cfs).await
1505 }
1506 }
1507}
1508
1509#[context("Rollback (ostree)")]
1511async fn rollback_ostree(
1512 opts: &RollbackOpts,
1513 storage: &Storage,
1514 booted_ostree: &BootedOstree<'_>,
1515) -> Result<()> {
1516 crate::deploy::rollback(storage).await?;
1517
1518 if opts.soft_reboot.is_some() {
1519 let host = crate::status::get_status(booted_ostree)?.1;
1521
1522 handle_soft_reboot(
1523 opts.soft_reboot,
1524 host.status.rollback.as_ref(),
1525 "rollback",
1526 || soft_reboot_rollback(booted_ostree),
1527 )?;
1528 }
1529
1530 Ok(())
1531}
1532
1533#[context("Rollback")]
1535async fn rollback(opts: &RollbackOpts) -> Result<()> {
1536 let storage = &get_storage().await?;
1537 match storage.kind()? {
1538 BootedStorageKind::Ostree(booted_ostree) => {
1539 rollback_ostree(opts, storage, &booted_ostree).await
1540 }
1541 BootedStorageKind::Composefs(booted_cfs) => composefs_rollback(storage, &booted_cfs).await,
1542 }
1543}
1544
1545#[context("Editing spec (ostree)")]
1547async fn edit_ostree(
1548 opts: EditOpts,
1549 storage: &Storage,
1550 booted_ostree: &BootedOstree<'_>,
1551) -> Result<()> {
1552 let repo = &booted_ostree.repo();
1553 let (_, host) = crate::status::get_status(booted_ostree)?;
1554
1555 let new_host: Host = if let Some(filename) = opts.filename {
1556 let mut r = std::io::BufReader::new(std::fs::File::open(filename)?);
1557 serde_yaml::from_reader(&mut r)?
1558 } else {
1559 let tmpf = tempfile::NamedTempFile::with_suffix(".yaml")?;
1560 serde_yaml::to_writer(std::io::BufWriter::new(tmpf.as_file()), &host)?;
1561 crate::utils::spawn_editor(&tmpf)?;
1562 tmpf.as_file().seek(std::io::SeekFrom::Start(0))?;
1563 serde_yaml::from_reader(&mut tmpf.as_file())?
1564 };
1565
1566 if new_host.spec == host.spec {
1567 println!("Edit cancelled, no changes made.");
1568 return Ok(());
1569 }
1570 host.spec.verify_transition(&new_host.spec)?;
1571 let new_spec = RequiredHostSpec::from_spec(&new_host.spec)?;
1572
1573 let prog = ProgressWriter::default();
1574
1575 if host.spec.boot_order != new_host.spec.boot_order {
1578 return crate::deploy::rollback(storage).await;
1579 }
1580
1581 let fetched = crate::deploy::pull(
1582 repo,
1583 new_spec.image,
1584 None,
1585 opts.quiet,
1586 prog.clone(),
1587 Some(&booted_ostree.deployment),
1588 )
1589 .await?;
1590
1591 let stateroot = booted_ostree.stateroot();
1594 let from = MergeState::from_stateroot(storage, &stateroot)?;
1595 crate::deploy::stage(storage, from, &fetched, &new_spec, prog.clone(), false).await?;
1596
1597 storage.update_mtime()?;
1598
1599 Ok(())
1600}
1601
1602#[context("Editing spec")]
1604async fn edit(opts: EditOpts) -> Result<()> {
1605 let storage = &get_storage().await?;
1606 match storage.kind()? {
1607 BootedStorageKind::Ostree(booted_ostree) => {
1608 edit_ostree(opts, storage, &booted_ostree).await
1609 }
1610 BootedStorageKind::Composefs(_) => {
1611 anyhow::bail!("Edit is not yet supported for composefs backend")
1612 }
1613 }
1614}
1615
1616async fn usroverlay(access_mode: FilesystemOverlayAccessMode) -> Result<()> {
1618 let args = match access_mode {
1621 FilesystemOverlayAccessMode::ReadOnly => ["admin", "unlock", "--transient"].as_slice(),
1623
1624 FilesystemOverlayAccessMode::ReadWrite => ["admin", "unlock"].as_slice(),
1625 };
1626 Err(Command::new("ostree").args(args).exec().into())
1627}
1628
1629fn join_host_ipc_namespace() -> Result<()> {
1642 let caps = rustix::thread::capabilities(None).context("capget")?;
1643 if !caps
1644 .effective
1645 .contains(rustix::thread::CapabilitySet::SYS_ADMIN)
1646 {
1647 return Ok(());
1648 }
1649 let ns_pid1 = match std::fs::read_link("/proc/1/ns/ipc") {
1650 Ok(v) => v,
1651 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
1652 return Ok(());
1653 }
1654 Err(e) => return Err(e).context("reading /proc/1/ns/ipc"),
1655 };
1656 let ns_self = std::fs::read_link("/proc/self/ns/ipc").context("reading /proc/self/ns/ipc")?;
1657 if ns_pid1 != ns_self {
1658 let pid1ipcns = std::fs::File::open("/proc/1/ns/ipc").context("open pid1 ipcns")?;
1659 rustix::thread::move_into_link_name_space(
1660 pid1ipcns.as_fd(),
1661 Some(rustix::thread::LinkNameSpaceType::InterProcessCommunication),
1662 )
1663 .context("setns(ipc)")?;
1664 }
1665 Ok(())
1666}
1667
1668#[allow(unsafe_code)]
1671pub fn global_init() -> Result<()> {
1672 join_host_ipc_namespace()?;
1673 ostree::glib::set_prgname(bootc_utils::NAME.into());
1676 if let Err(e) = rustix::thread::set_name(&CString::new(bootc_utils::NAME).unwrap()) {
1677 eprintln!("failed to set name: {e}");
1679 }
1680 ostree::SePolicy::set_null_log();
1682 let am_root = rustix::process::getuid().is_root();
1683 if std::env::var_os("HOME").is_none() && am_root {
1686 unsafe {
1691 std::env::set_var("HOME", "/root");
1692 }
1693 }
1694 Ok(())
1695}
1696
1697pub async fn run_from_iter<I>(args: I) -> Result<()>
1700where
1701 I: IntoIterator,
1702 I::Item: Into<OsString> + Clone,
1703{
1704 run_from_opt(Opt::parse_including_static(args)).await
1705}
1706
1707fn callname_from_argv0(argv0: &OsStr) -> &str {
1711 let default = "bootc";
1712 std::path::Path::new(argv0)
1713 .file_name()
1714 .and_then(|s| s.to_str())
1715 .filter(|s| !s.is_empty())
1716 .unwrap_or(default)
1717}
1718
1719impl Opt {
1720 fn parse_including_static<I>(args: I) -> Self
1723 where
1724 I: IntoIterator,
1725 I::Item: Into<OsString> + Clone,
1726 {
1727 let mut args = args.into_iter();
1728 let first = if let Some(first) = args.next() {
1729 let first: OsString = first.into();
1730 let argv0 = callname_from_argv0(&first);
1731 tracing::debug!("argv0={argv0:?}");
1732 let mapped = match argv0 {
1733 InternalsOpts::GENERATOR_BIN => {
1734 Some(["bootc", "internals", "systemd-generator"].as_slice())
1735 }
1736 "ostree-container" | "ostree-ima-sign" | "ostree-provisional-repair" => {
1737 Some(["bootc", "internals", "ostree-ext"].as_slice())
1738 }
1739 _ => None,
1740 };
1741 if let Some(base_args) = mapped {
1742 let base_args = base_args.iter().map(OsString::from);
1743 return Opt::parse_from(base_args.chain(args.map(|i| i.into())));
1744 }
1745 Some(first)
1746 } else {
1747 None
1748 };
1749 Opt::parse_from(first.into_iter().chain(args.map(|i| i.into())))
1750 }
1751}
1752
1753async fn run_from_opt(opt: Opt) -> Result<()> {
1755 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1756 match opt {
1757 Opt::Upgrade(opts) => {
1758 let storage = &get_storage().await?;
1759 match storage.kind()? {
1760 BootedStorageKind::Ostree(booted_ostree) => {
1761 upgrade(opts, storage, &booted_ostree).await
1762 }
1763 BootedStorageKind::Composefs(booted_cfs) => {
1764 upgrade_composefs(opts, storage, &booted_cfs).await
1765 }
1766 }
1767 }
1768 Opt::Switch(opts) => switch(opts).await,
1769 Opt::Rollback(opts) => {
1770 rollback(&opts).await?;
1771 if opts.apply {
1772 crate::reboot::reboot()?;
1773 }
1774 Ok(())
1775 }
1776 Opt::Edit(opts) => edit(opts).await,
1777 Opt::UsrOverlay(opts) => {
1778 use crate::store::Environment;
1779 let env = Environment::detect()?;
1780 let access_mode = if opts.read_only {
1781 FilesystemOverlayAccessMode::ReadOnly
1782 } else {
1783 FilesystemOverlayAccessMode::ReadWrite
1784 };
1785 match env {
1786 Environment::OstreeBooted => usroverlay(access_mode).await,
1787 Environment::ComposefsBooted(_) => composefs_usr_overlay(access_mode),
1788 _ => anyhow::bail!("usroverlay only applies on booted hosts"),
1789 }
1790 }
1791 Opt::Container(opts) => match opts {
1792 ContainerOpts::Inspect {
1793 rootfs,
1794 json,
1795 format,
1796 } => crate::status::container_inspect(&rootfs, json, format),
1797 ContainerOpts::Lint {
1798 rootfs,
1799 fatal_warnings,
1800 list,
1801 skip,
1802 no_truncate,
1803 } => {
1804 if list {
1805 return lints::lint_list(std::io::stdout().lock());
1806 }
1807 let warnings = if fatal_warnings {
1808 lints::WarningDisposition::FatalWarnings
1809 } else {
1810 lints::WarningDisposition::AllowWarnings
1811 };
1812 let root_type = if rootfs == "/" {
1813 lints::RootType::Running
1814 } else {
1815 lints::RootType::Alternative
1816 };
1817
1818 let root = &Dir::open_ambient_dir(rootfs, cap_std::ambient_authority())?;
1819 let skip = skip.iter().map(|s| s.as_str());
1820 lints::lint(
1821 root,
1822 warnings,
1823 root_type,
1824 skip,
1825 std::io::stdout().lock(),
1826 no_truncate,
1827 )?;
1828 Ok(())
1829 }
1830 ContainerOpts::ComputeComposefsDigest {
1831 path,
1832 write_dumpfile_to,
1833 } => {
1834 let digest = compute_composefs_digest(&path, write_dumpfile_to.as_deref()).await?;
1835 println!("{digest}");
1836 Ok(())
1837 }
1838 ContainerOpts::ComputeComposefsDigestFromStorage {
1839 write_dumpfile_to,
1840 image,
1841 } => {
1842 let (_td_guard, repo) = new_temp_composefs_repo()?;
1843
1844 let mut proxycfg = crate::deploy::new_proxy_config();
1845
1846 let image = if let Some(image) = image {
1847 image
1848 } else {
1849 let host_container_store = Utf8Path::new("/run/host-container-storage");
1850 let container_info = crate::containerenv::get_container_execution_info(&root)?;
1853 let iid = container_info.imageid;
1854 tracing::debug!("Computing digest of {iid}");
1855
1856 if !host_container_store.try_exists()? {
1857 anyhow::bail!(
1858 "Must be readonly mount of host container store: {host_container_store}"
1859 );
1860 }
1861 let mut cmd = Command::new(bootc_utils::skopeo_bin());
1863 set_additional_image_store(&mut cmd, "/run/host-container-storage");
1864 proxycfg.skopeo_cmd = Some(cmd);
1865 iid
1866 };
1867
1868 let imgref = format!("containers-storage:{image}");
1869 let host_store = std::path::Path::new("/run/host-container-storage");
1870 let opts = composefs_oci::PullOptions {
1871 img_proxy_config: Some(proxycfg),
1872 additional_image_stores: &[host_store],
1873 ..Default::default()
1874 };
1875 let pull_result = composefs_oci::pull(&repo, &imgref, None, opts)
1876 .await
1877 .context("Pulling image")?;
1878 let mut fs = composefs_oci::image::create_filesystem(
1879 &repo,
1880 &pull_result.config_digest,
1881 Some(&pull_result.config_verity),
1882 )
1883 .context("Populating fs")?;
1884 fs.transform_for_boot(&repo).context("Preparing for boot")?;
1885 let id = fs.compute_image_id();
1886 println!("{}", id.to_hex());
1887
1888 if let Some(path) = write_dumpfile_to.as_deref() {
1889 let mut w = File::create(path)
1890 .with_context(|| format!("Opening {path}"))
1891 .map(BufWriter::new)?;
1892 dumpfile::write_dumpfile(&mut w, &fs).context("Writing dumpfile")?;
1893 }
1894
1895 Ok(())
1896 }
1897 ContainerOpts::Ukify {
1898 rootfs,
1899 kargs,
1900 allow_missing_verity,
1901 write_dumpfile_to,
1902 args,
1903 } => {
1904 crate::ukify::build_ukify(
1905 &rootfs,
1906 &kargs,
1907 &args,
1908 allow_missing_verity,
1909 write_dumpfile_to.as_deref(),
1910 )
1911 .await
1912 }
1913 ContainerOpts::Export {
1914 format,
1915 target,
1916 output,
1917 kernel_in_boot,
1918 disable_selinux,
1919 } => {
1920 crate::container_export::export(
1921 &format,
1922 &target,
1923 output.as_deref(),
1924 kernel_in_boot,
1925 disable_selinux,
1926 )
1927 .await
1928 }
1929 },
1930 Opt::Completion { shell } => {
1931 use clap_complete::aot::generate;
1932
1933 let mut cmd = Opt::command();
1934 let mut stdout = std::io::stdout();
1935 let bin_name = "bootc";
1936 generate(shell, &mut cmd, bin_name, &mut stdout);
1937 Ok(())
1938 }
1939 Opt::Image(opts) => match opts {
1940 ImageOpts::List {
1941 list_type,
1942 list_format,
1943 } => crate::image::list_entrypoint(list_type, list_format).await,
1944
1945 ImageOpts::CopyToStorage { source, target } => {
1946 let host = get_host().await?;
1948
1949 let storage = get_storage().await?;
1950
1951 match storage.kind()? {
1952 BootedStorageKind::Ostree(..) => {
1953 crate::image::push_entrypoint(
1954 &storage,
1955 &host,
1956 source.as_deref(),
1957 target.as_deref(),
1958 )
1959 .await
1960 }
1961 BootedStorageKind::Composefs(booted) => {
1962 bootc_composefs::export::export_repo_to_image(
1963 &storage,
1964 &booted,
1965 source.as_deref(),
1966 target.as_deref(),
1967 )
1968 .await
1969 }
1970 }
1971 }
1972 ImageOpts::SetUnified => crate::image::set_unified_entrypoint().await,
1973 ImageOpts::PullFromDefaultStorage { image } => {
1974 let storage = get_storage().await?;
1975 storage
1976 .get_ensure_imgstore()?
1977 .pull_from_host_storage(&image)
1978 .await
1979 }
1980 ImageOpts::Cmd(opt) => {
1981 let storage = get_storage().await?;
1982 let imgstore = storage.get_ensure_imgstore()?;
1983 match opt {
1984 ImageCmdOpts::List { args } => {
1985 crate::image::imgcmd_entrypoint(imgstore, "list", &args).await
1986 }
1987 ImageCmdOpts::Build { args } => {
1988 crate::image::imgcmd_entrypoint(imgstore, "build", &args).await
1989 }
1990 ImageCmdOpts::Pull { images } => {
1991 for image in &images {
1992 imgstore.pull_with_progress(image).await?;
1993 }
1994 Ok(())
1995 }
1996 ImageCmdOpts::Push { args } => {
1997 crate::image::imgcmd_entrypoint(imgstore, "push", &args).await
1998 }
1999 }
2000 }
2001 },
2002 Opt::Install(opts) => match opts {
2003 #[cfg(feature = "install-to-disk")]
2004 InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await,
2005 InstallOpts::ToFilesystem(opts) => {
2006 crate::install::install_to_filesystem(opts, false, crate::install::Cleanup::Skip)
2007 .await
2008 }
2009 InstallOpts::ToExistingRoot(opts) => {
2010 crate::install::install_to_existing_root(opts).await
2011 }
2012 InstallOpts::Reset(opts) => crate::install::install_reset(opts).await,
2013 InstallOpts::PrintConfiguration(opts) => crate::install::print_configuration(opts),
2014 InstallOpts::EnsureCompletion {} => {
2015 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2016 crate::install::completion::run_from_anaconda(rootfs).await
2017 }
2018 InstallOpts::Finalize { root_path } => {
2019 crate::install::install_finalize(&root_path).await
2020 }
2021 },
2022 Opt::LoaderEntries(opts) => match opts {
2023 LoaderEntriesOpts::SetOptionsForSource(opts) => {
2024 let storage = get_storage().await?;
2025 let sysroot = storage.get_ostree()?;
2026 crate::loader_entries::set_options_for_source_staged(
2027 sysroot,
2028 &opts.source,
2029 opts.options.as_deref(),
2030 )?;
2031 Ok(())
2032 }
2033 },
2034 Opt::ExecInHostMountNamespace { args } => {
2035 crate::install::exec_in_host_mountns(args.as_slice())
2036 }
2037 Opt::Status(opts) => super::status::status(opts).await,
2038 Opt::Internals(opts) => match opts {
2039 InternalsOpts::SystemdGenerator {
2040 normal_dir,
2041 early_dir: _,
2042 late_dir: _,
2043 } => {
2044 let unit_dir = &Dir::open_ambient_dir(normal_dir, cap_std::ambient_authority())?;
2045 crate::generator::generator(root, unit_dir)
2046 }
2047 InternalsOpts::OstreeExt { args } => {
2048 ostree_ext::cli::run_from_iter(["ostree-ext".into()].into_iter().chain(args)).await
2049 }
2050 InternalsOpts::OstreeContainer { args } => {
2051 ostree_ext::cli::run_from_iter(
2052 ["ostree-ext".into(), "container".into()]
2053 .into_iter()
2054 .chain(args),
2055 )
2056 .await
2057 }
2058 InternalsOpts::TestComposefs => {
2059 let storage = get_storage().await?;
2061 let cfs = storage.get_ensure_composefs()?;
2062 let testdata = b"some test data";
2063 let testdata_digest = hex::encode(openssl::sha::sha256(testdata));
2064 let mut w = cfs.create_stream(0)?;
2065 w.write_inline(testdata);
2066 let object = cfs
2067 .write_stream(w, &testdata_digest, Some("testobject"))?
2068 .to_hex();
2069 assert_eq!(
2070 object,
2071 "84245c6936db9939dda9c1fbeafdcbd2b49f7605354c88d4f016c4d941551f45bad0fbcdbee12ba8adfe4fb63541de57ac02729edbacdb556325e342b89d340d"
2072 );
2073 Ok(())
2074 }
2075 InternalsOpts::Fsverity(args) => match args {
2077 FsverityOpts::Measure { path } => {
2078 let fd =
2079 std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
2080 let digest: fsverity::Sha256HashValue = fsverity::measure_verity(&fd)?;
2081 let digest = digest.to_hex();
2082 println!("{digest}");
2083 Ok(())
2084 }
2085 FsverityOpts::Enable { path } => {
2086 let fd =
2087 std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
2088 fsverity::enable_verity_raw::<fsverity::Sha256HashValue>(&fd)?;
2089 Ok(())
2090 }
2091 },
2092 InternalsOpts::Cfs { args } => composefs_ctl::run_from_iter(args.iter()).await,
2093 InternalsOpts::Reboot => crate::reboot::reboot(),
2094 InternalsOpts::Fsck => {
2095 let storage = &get_storage().await?;
2096 crate::fsck::fsck(&storage, std::io::stdout().lock()).await?;
2097 Ok(())
2098 }
2099 InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root),
2100 InternalsOpts::PrintJsonSchema { of } => {
2101 let schema = match of {
2102 SchemaType::Host => schema_for!(crate::spec::Host),
2103 SchemaType::Progress => schema_for!(crate::progress_jsonl::Event),
2104 };
2105 let mut stdout = std::io::stdout().lock();
2106 serde_json::to_writer_pretty(&mut stdout, &schema)?;
2107 Ok(())
2108 }
2109 InternalsOpts::Cleanup => {
2110 let storage = get_storage().await?;
2111 crate::deploy::cleanup(&storage).await
2112 }
2113 InternalsOpts::Relabel { as_path, path } => {
2114 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2115 let path = path.strip_prefix("/")?;
2116 let sepolicy =
2117 &ostree::SePolicy::new(&gio::File::for_path("/"), gio::Cancellable::NONE)?;
2118 crate::lsm::relabel_recurse(root, path, as_path.as_deref(), sepolicy)?;
2119 Ok(())
2120 }
2121 InternalsOpts::RelabelOverlayMountpoints => {
2122 crate::generator::relabel_overlay_mountpoints()
2123 }
2124 InternalsOpts::BootcInstallCompletion { sysroot, stateroot } => {
2125 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2126 crate::install::completion::run_from_ostree(rootfs, &sysroot, &stateroot).await
2127 }
2128 InternalsOpts::LoopbackCleanupHelper { device } => {
2129 crate::blockdev::run_loopback_cleanup_helper(&device).await
2130 }
2131 InternalsOpts::AllocateCleanupLoopback { file_path: _ } => {
2132 let temp_file =
2134 tempfile::NamedTempFile::new().context("Failed to create temporary file")?;
2135 let temp_path = temp_file.path();
2136
2137 let loopback = crate::blockdev::LoopbackDevice::new(temp_path)
2139 .context("Failed to create loopback device")?;
2140
2141 println!("Created loopback device: {}", loopback.path());
2142
2143 loopback
2145 .close()
2146 .context("Failed to close loopback device")?;
2147
2148 println!("Successfully closed loopback device");
2149 Ok(())
2150 }
2151 #[cfg(feature = "rhsm")]
2152 InternalsOpts::PublishRhsmFacts => crate::rhsm::publish_facts(&root).await,
2153 #[cfg(feature = "docgen")]
2154 InternalsOpts::DumpCliJson => {
2155 use clap::CommandFactory;
2156 let cmd = Opt::command();
2157 let json = crate::cli_json::dump_cli_json(&cmd)?;
2158 println!("{}", json);
2159 Ok(())
2160 }
2161 InternalsOpts::DirDiff {
2162 pristine_etc,
2163 current_etc,
2164 new_etc,
2165 merge,
2166 } => {
2167 let pristine_etc =
2168 Dir::open_ambient_dir(pristine_etc, cap_std::ambient_authority())?;
2169 let current_etc = Dir::open_ambient_dir(current_etc, cap_std::ambient_authority())?;
2170 let new_etc = Dir::open_ambient_dir(new_etc, cap_std::ambient_authority())?;
2171
2172 let (p, c, n) =
2173 etc_merge::traverse_etc(&pristine_etc, ¤t_etc, Some(&new_etc))?;
2174
2175 let n = n
2176 .as_ref()
2177 .ok_or_else(|| anyhow::anyhow!("Failed to get new directory tree"))?;
2178
2179 let diff = compute_diff(&p, &c, &n)?;
2180 print_diff(&diff, &mut std::io::stdout());
2181
2182 if merge {
2183 etc_merge::merge(¤t_etc, &c, &new_etc, &n, &diff)?;
2184 }
2185
2186 Ok(())
2187 }
2188 InternalsOpts::PrepSoftReboot {
2189 deployment,
2190 reboot,
2191 reset,
2192 } => {
2193 let storage = &get_storage().await?;
2194
2195 match storage.kind()? {
2196 BootedStorageKind::Ostree(..) => {
2197 anyhow::bail!("soft-reboot only implemented for composefs")
2199 }
2200
2201 BootedStorageKind::Composefs(booted_cfs) => {
2202 if reset {
2203 return reset_soft_reboot();
2204 }
2205
2206 prepare_soft_reboot_composefs(
2207 &storage,
2208 &booted_cfs,
2209 deployment.as_deref(),
2210 SoftRebootMode::Required,
2211 reboot,
2212 )
2213 .await
2214 }
2215 }
2216 }
2217 InternalsOpts::ComposefsGC {
2218 dry_run,
2219 assert_no_op,
2220 prune_repo,
2221 } => {
2222 let storage = &get_storage().await?;
2223
2224 match storage.kind()? {
2225 BootedStorageKind::Ostree(..) => {
2226 anyhow::bail!("composefs-gc only works for composefs backend");
2227 }
2228
2229 BootedStorageKind::Composefs(booted_cfs) => {
2230 let effective_dry_run = dry_run || assert_no_op;
2231 let gc_result =
2232 composefs_gc(storage, &booted_cfs, effective_dry_run, prune_repo)
2233 .await?;
2234
2235 if effective_dry_run {
2236 println!("Dry run (no files deleted)");
2237 }
2238
2239 println!(
2240 "Objects: {} removed ({} bytes)",
2241 gc_result.objects_removed, gc_result.objects_bytes
2242 );
2243
2244 if gc_result.images_pruned > 0 || gc_result.streams_pruned > 0 {
2245 println!(
2246 "Pruned symlinks: {} images, {} streams",
2247 gc_result.images_pruned, gc_result.streams_pruned
2248 );
2249 }
2250
2251 if assert_no_op {
2252 let is_noop = gc_result.objects_removed == 0
2253 && gc_result.images_pruned == 0
2254 && gc_result.streams_pruned == 0;
2255 if !is_noop {
2256 anyhow::bail!(
2257 "--assert-no-op: GC would remove {} object(s), {} image symlink(s), {} stream symlink(s) (issue #1808)",
2258 gc_result.objects_removed,
2259 gc_result.images_pruned,
2260 gc_result.streams_pruned,
2261 );
2262 }
2263 }
2264
2265 Ok(())
2266 }
2267 }
2268 }
2269 InternalsOpts::Blockdev(opts) => {
2270 let dev = match opts {
2271 BlockdevOpts::Ls { device } => crate::blockdev::list_dev(&device)?,
2272 BlockdevOpts::LsFilesystem { path } => {
2273 let dir = Dir::open_ambient_dir(&path, cap_std::ambient_authority())?;
2274 crate::blockdev::list_dev_by_dir(&dir)?
2275 }
2276 };
2277 serde_json::to_writer_pretty(std::io::stdout().lock(), &dev)?;
2278 println!();
2279 Ok(())
2280 }
2281 },
2282 Opt::State(opts) => match opts {
2283 StateOpts::WipeOstree => {
2284 let sysroot = ostree::Sysroot::new_default();
2285 sysroot.load(gio::Cancellable::NONE)?;
2286 crate::deploy::wipe_ostree(sysroot).await?;
2287 Ok(())
2288 }
2289 },
2290
2291 Opt::ComposefsFinalizeStaged => {
2292 let storage = &get_storage().await?;
2293 match storage.kind()? {
2294 BootedStorageKind::Ostree(_) => {
2295 anyhow::bail!("ComposefsFinalizeStaged is only supported for composefs backend")
2296 }
2297 BootedStorageKind::Composefs(booted_cfs) => {
2298 composefs_backend_finalize(storage, &booted_cfs).await
2299 }
2300 }
2301 }
2302
2303 Opt::ConfigDiff => {
2304 let storage = &get_storage().await?;
2305 match storage.kind()? {
2306 BootedStorageKind::Ostree(_) => {
2307 anyhow::bail!("ConfigDiff is only supported for composefs backend")
2308 }
2309 BootedStorageKind::Composefs(booted_cfs) => {
2310 get_etc_diff(storage, &booted_cfs).await
2311 }
2312 }
2313 }
2314
2315 Opt::DeleteDeployment { depl_id } => {
2316 let storage = &get_storage().await?;
2317 match storage.kind()? {
2318 BootedStorageKind::Ostree(_) => {
2319 anyhow::bail!("DeleteDeployment is only supported for composefs backend")
2320 }
2321 BootedStorageKind::Composefs(booted_cfs) => {
2322 delete_composefs_deployment(&depl_id, storage, &booted_cfs).await
2323 }
2324 }
2325 }
2326 }
2327}
2328
2329#[cfg(test)]
2330mod tests {
2331 use super::*;
2332
2333 #[test]
2334 fn test_callname() {
2335 use std::os::unix::ffi::OsStrExt;
2336
2337 let mapped_cases = [
2339 ("", "bootc"),
2340 ("/foo/bar", "bar"),
2341 ("/foo/bar/", "bar"),
2342 ("foo/bar", "bar"),
2343 ("../foo/bar", "bar"),
2344 ("usr/bin/ostree-container", "ostree-container"),
2345 ];
2346 for (input, output) in mapped_cases {
2347 assert_eq!(
2348 output,
2349 callname_from_argv0(OsStr::new(input)),
2350 "Handling mapped case {input}"
2351 );
2352 }
2353
2354 assert_eq!("bootc", callname_from_argv0(OsStr::from_bytes(b"foo\x80")));
2356
2357 let ident_cases = ["foo", "bootc"];
2359 for case in ident_cases {
2360 assert_eq!(
2361 case,
2362 callname_from_argv0(OsStr::new(case)),
2363 "Handling ident case {case}"
2364 );
2365 }
2366 }
2367
2368 #[test]
2369 fn test_parse_install_args() {
2370 let o = Opt::try_parse_from([
2372 "bootc",
2373 "install",
2374 "to-filesystem",
2375 "--target-no-signature-verification",
2376 "/target",
2377 ])
2378 .unwrap();
2379 let o = match o {
2380 Opt::Install(InstallOpts::ToFilesystem(fsopts)) => fsopts,
2381 o => panic!("Expected filesystem opts, not {o:?}"),
2382 };
2383 assert!(o.target_opts.target_no_signature_verification);
2384 assert_eq!(o.filesystem_opts.root_path.as_str(), "/target");
2385 assert_eq!(
2387 o.config_opts.bound_images,
2388 crate::install::BoundImagesOpt::Stored
2389 );
2390 }
2391
2392 #[test]
2393 fn test_parse_opts() {
2394 assert!(matches!(
2395 Opt::parse_including_static(["bootc", "status"]),
2396 Opt::Status(StatusOpts {
2397 json: false,
2398 format: None,
2399 format_version: None,
2400 booted: false,
2401 verbose: false
2402 })
2403 ));
2404 assert!(matches!(
2405 Opt::parse_including_static(["bootc", "status", "--format-version=0"]),
2406 Opt::Status(StatusOpts {
2407 format_version: Some(0),
2408 ..
2409 })
2410 ));
2411
2412 assert!(matches!(
2414 Opt::parse_including_static(["bootc", "status", "--verbose"]),
2415 Opt::Status(StatusOpts { verbose: true, .. })
2416 ));
2417
2418 assert!(matches!(
2420 Opt::parse_including_static(["bootc", "status", "-v"]),
2421 Opt::Status(StatusOpts { verbose: true, .. })
2422 ));
2423 }
2424
2425 #[test]
2426 fn test_parse_generator() {
2427 assert!(matches!(
2428 Opt::parse_including_static([
2429 "/usr/lib/systemd/system/bootc-systemd-generator",
2430 "/run/systemd/system"
2431 ]),
2432 Opt::Internals(InternalsOpts::SystemdGenerator { normal_dir, .. }) if normal_dir == "/run/systemd/system"
2433 ));
2434 }
2435
2436 #[test]
2437 fn test_parse_ostree_ext() {
2438 assert!(matches!(
2439 Opt::parse_including_static(["bootc", "internals", "ostree-container"]),
2440 Opt::Internals(InternalsOpts::OstreeContainer { .. })
2441 ));
2442
2443 fn peel(o: Opt) -> Vec<OsString> {
2444 match o {
2445 Opt::Internals(InternalsOpts::OstreeExt { args }) => args,
2446 o => panic!("unexpected {o:?}"),
2447 }
2448 }
2449 let args = peel(Opt::parse_including_static([
2450 "/usr/libexec/libostree/ext/ostree-ima-sign",
2451 "ima-sign",
2452 "--repo=foo",
2453 "foo",
2454 "bar",
2455 "baz",
2456 ]));
2457 assert_eq!(
2458 args.as_slice(),
2459 ["ima-sign", "--repo=foo", "foo", "bar", "baz"]
2460 );
2461
2462 let args = peel(Opt::parse_including_static([
2463 "/usr/libexec/libostree/ext/ostree-container",
2464 "container",
2465 "image",
2466 "pull",
2467 ]));
2468 assert_eq!(args.as_slice(), ["container", "image", "pull"]);
2469 }
2470
2471 #[test]
2472 fn test_parse_upgrade_options() {
2473 let o = Opt::try_parse_from(["bootc", "upgrade", "--tag", "v1.1"]).unwrap();
2475 match o {
2476 Opt::Upgrade(opts) => {
2477 assert_eq!(opts.tag, Some("v1.1".to_string()));
2478 }
2479 _ => panic!("Expected Upgrade variant"),
2480 }
2481
2482 let o = Opt::try_parse_from(["bootc", "upgrade", "--tag", "v1.1", "--check"]).unwrap();
2484 match o {
2485 Opt::Upgrade(opts) => {
2486 assert_eq!(opts.tag, Some("v1.1".to_string()));
2487 assert!(opts.check);
2488 }
2489 _ => panic!("Expected Upgrade variant"),
2490 }
2491 }
2492
2493 #[test]
2494 fn test_image_reference_with_tag() {
2495 let current = ImageReference {
2497 image: "quay.io/example/myapp:v1.0".to_string(),
2498 transport: "registry".to_string(),
2499 signature: None,
2500 };
2501 let result = current.with_tag("v1.1").unwrap();
2502 assert_eq!(result.image, "quay.io/example/myapp:v1.1");
2503 assert_eq!(result.transport, "registry");
2504
2505 let current_with_digest = ImageReference {
2507 image: "quay.io/example/myapp:v1.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(),
2508 transport: "registry".to_string(),
2509 signature: None,
2510 };
2511 let result = current_with_digest.with_tag("v2.0").unwrap();
2512 assert_eq!(result.image, "quay.io/example/myapp:v2.0");
2513
2514 let containers_storage = ImageReference {
2516 image: "localhost/myapp:v1.0".to_string(),
2517 transport: "containers-storage".to_string(),
2518 signature: None,
2519 };
2520 let result = containers_storage.with_tag("v1.1").unwrap();
2521 assert_eq!(result.image, "localhost/myapp:v1.1");
2522 assert_eq!(result.transport, "containers-storage");
2523
2524 let containers_storage_with_digest = ImageReference {
2526 image:
2527 "localhost/myapp:v1.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
2528 .to_string(),
2529 transport: "containers-storage".to_string(),
2530 signature: None,
2531 };
2532 let result = containers_storage_with_digest.with_tag("v2.0").unwrap();
2533 assert_eq!(result.image, "localhost/myapp:v2.0");
2534 assert_eq!(result.transport, "containers-storage");
2535
2536 let no_tag = ImageReference {
2538 image: "localhost/myapp".to_string(),
2539 transport: "containers-storage".to_string(),
2540 signature: None,
2541 };
2542 let result = no_tag.with_tag("v1.0").unwrap();
2543 assert_eq!(result.image, "localhost/myapp:v1.0");
2544 assert_eq!(result.transport, "containers-storage");
2545 }
2546
2547 #[test]
2548 fn test_generate_completion_scripts_contain_commands() {
2549 use clap_complete::aot::{Shell, generate};
2550
2551 let want = ["install", "upgrade"];
2560
2561 for shell in [Shell::Bash, Shell::Zsh, Shell::Fish] {
2562 let mut cmd = Opt::command();
2563 let mut buf = Vec::new();
2564 generate(shell, &mut cmd, "bootc", &mut buf);
2565 let s = String::from_utf8(buf).expect("completion should be utf8");
2566 for w in &want {
2567 assert!(s.contains(w), "{shell:?} completion missing {w}");
2568 }
2569 }
2570 }
2571}