1use std::ffi::{CString, OsStr, OsString};
6use std::fs::File;
7use std::io::{BufWriter, Seek, SeekFrom};
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::erofs::format::FormatVersion;
21use composefs::fsverity;
22use composefs::fsverity::FsVerityHashValue;
23use composefs_ctl::composefs;
24use composefs_ctl::composefs_boot;
25use composefs_ctl::composefs_oci;
26
27use composefs_boot::BootOps as _;
28use etc_merge::{compute_diff, print_diff};
29use fn_error_context::context;
30use indoc::indoc;
31use ocidir::cap_std::ambient_authority;
32use ostree::gio;
33use ostree_container::store::PrepareResult;
34use ostree_ext::container as ostree_container;
35
36use ostree_ext::keyfileext::KeyFileExt;
37use ostree_ext::ostree;
38use ostree_ext::sysroot::SysrootLock;
39use schemars::schema_for;
40use serde::{Deserialize, Serialize};
41
42use crate::bootc_composefs::boot::BootType;
43use crate::bootc_composefs::delete::delete_composefs_deployment;
44use crate::bootc_composefs::gc::{GCOpts, composefs_gc};
45use crate::bootc_composefs::soft_reboot::{prepare_soft_reboot_composefs, reset_soft_reboot};
46use crate::bootc_composefs::state::get_usr_overlay_status;
47use crate::bootc_composefs::status::get_composefs_status;
48use crate::bootc_composefs::uki_addons_cli::handle_addon_cli_cmd;
49use crate::bootc_composefs::{
50 aboot,
51 digest::{compute_composefs_digest, new_temp_composefs_repo},
52 finalize::{composefs_backend_finalize, get_etc_diff},
53 rollback::composefs_rollback,
54 service::start_finalize_stated_svc,
55 state::composefs_usr_overlay,
56 switch::switch_composefs,
57 update::upgrade_composefs,
58};
59use crate::deploy::{MergeState, RequiredHostSpec};
60use crate::install::UkiAddonOpts;
61use crate::podstorage::set_additional_image_store;
62use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
63use crate::spec::FilesystemOverlayAccessMode;
64use crate::spec::Host;
65use crate::spec::ImageReference;
66use crate::status::get_host;
67use crate::store::{BootedOstree, Storage};
68use crate::store::{BootedStorage, BootedStorageKind};
69use crate::utils::sigpolicy_from_opt;
70use crate::{bootc_composefs, lints};
71
72#[derive(Clone, Debug, Parser, PartialEq, Eq)]
74pub(crate) struct ProgressOptions {
75 #[clap(long, hide = true)]
79 pub(crate) progress_fd: Option<RawProgressFd>,
80}
81
82impl TryFrom<ProgressOptions> for ProgressWriter {
83 type Error = anyhow::Error;
84
85 fn try_from(value: ProgressOptions) -> Result<Self> {
86 let r = value
87 .progress_fd
88 .map(TryInto::try_into)
89 .transpose()?
90 .unwrap_or_default();
91 Ok(r)
92 }
93}
94
95#[derive(Debug, Parser, PartialEq, Eq)]
96pub(crate) struct DownloadOnlyOpts {
97 #[clap(long, conflicts_with = "apply")]
103 pub(crate) download_only: bool,
104
105 #[clap(long, conflicts_with_all = ["download_only", "scoped", "global"])]
111 pub(crate) from_downloaded: bool,
112}
113
114#[derive(Debug, Parser, PartialEq, Eq)]
116pub(crate) struct UpgradeOpts {
117 #[clap(long)]
119 pub(crate) quiet: bool,
120
121 #[clap(long, conflicts_with_all = ["apply", "download_only", "from_downloaded", "scoped", "global"])]
125 pub(crate) check: bool,
126
127 #[clap(long, conflicts_with = "check")]
131 pub(crate) apply: bool,
132
133 #[clap(long = "soft-reboot", conflicts_with = "check")]
138 pub(crate) soft_reboot: Option<SoftRebootMode>,
139
140 #[clap(flatten)]
141 pub(crate) download_opts: DownloadOnlyOpts,
142
143 #[clap(long)]
148 pub(crate) tag: Option<String>,
149
150 #[clap(long, value_name = "PATH", conflicts_with_all = ["check", "from_downloaded", "tag"])]
155 pub(crate) from_delta: Option<Utf8PathBuf>,
156
157 #[clap(flatten)]
158 pub(crate) progress: ProgressOptions,
159
160 #[clap(flatten)]
162 pub(crate) uki_addon_opts: UkiAddonOpts,
163}
164
165#[derive(Debug, Parser, PartialEq, Eq)]
167pub(crate) struct SwitchOpts {
168 #[clap(long)]
170 pub(crate) quiet: bool,
171
172 #[clap(long)]
176 pub(crate) apply: bool,
177
178 #[clap(long = "soft-reboot")]
183 pub(crate) soft_reboot: Option<SoftRebootMode>,
184
185 #[clap(long, default_value = "registry")]
187 pub(crate) transport: String,
188
189 #[clap(flatten)]
190 pub(crate) download_opts: DownloadOnlyOpts,
191
192 #[clap(long, hide = true)]
194 pub(crate) no_signature_verification: bool,
195
196 #[clap(long)]
203 pub(crate) enforce_container_sigpolicy: bool,
204
205 #[clap(long, hide = true)]
209 pub(crate) mutate_in_place: bool,
210
211 #[clap(long)]
213 pub(crate) retain: bool,
214
215 #[clap(long = "experimental-unified-storage", hide = true)]
221 pub(crate) unified_storage_exp: bool,
222
223 #[clap(long, value_name = "PATH", conflicts_with_all = [
228 "from_downloaded",
229 "transport",
230 "mutate_in_place",
231 "unified_storage_exp",
232 "enforce_container_sigpolicy",
233 ])]
234 pub(crate) from_delta: Option<Utf8PathBuf>,
235
236 #[clap(
239 required_unless_present = "from_downloaded",
240 conflicts_with = "from_downloaded"
241 )]
242 pub(crate) target: Option<String>,
243
244 #[clap(flatten)]
245 pub(crate) progress: ProgressOptions,
246
247 #[clap(flatten)]
249 pub(crate) uki_addon_opts: UkiAddonOpts,
250}
251
252#[derive(Debug, Parser, PartialEq, Eq)]
256pub(crate) struct ComposefsFinalizeStagedOpts {
257 #[clap(long)]
262 pub(crate) hold: bool,
263}
264
265#[derive(Debug, Parser, PartialEq, Eq)]
267pub(crate) struct RollbackOpts {
268 #[clap(long)]
274 pub(crate) apply: bool,
275
276 #[clap(long = "soft-reboot")]
280 pub(crate) soft_reboot: Option<SoftRebootMode>,
281}
282
283#[derive(Debug, Parser, PartialEq, Eq)]
285pub(crate) struct EditOpts {
286 #[clap(long, short = 'f')]
288 pub(crate) filename: Option<String>,
289
290 #[clap(long)]
292 pub(crate) quiet: bool,
293}
294
295#[derive(Debug, Clone, ValueEnum, PartialEq, Eq)]
296#[clap(rename_all = "lowercase")]
297pub(crate) enum OutputFormat {
298 HumanReadable,
300 Yaml,
302 Json,
304}
305
306#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
307#[clap(rename_all = "lowercase")]
308pub(crate) enum SoftRebootMode {
309 Required,
311 Auto,
313}
314
315#[derive(Debug, Parser, PartialEq, Eq)]
317pub(crate) struct StatusOpts {
318 #[clap(long, hide = true)]
322 pub(crate) json: bool,
323
324 #[clap(long)]
326 pub(crate) format: Option<OutputFormat>,
327
328 #[clap(long)]
333 pub(crate) format_version: Option<u32>,
334
335 #[clap(long)]
337 pub(crate) booted: bool,
338
339 #[clap(long, short = 'v')]
341 pub(crate) verbose: bool,
342}
343
344#[derive(Debug, Parser, PartialEq, Eq)]
346pub(crate) struct UsrOverlayOpts {
347 #[clap(long)]
351 pub(crate) read_only: bool,
352}
353
354#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
355pub(crate) enum InstallOpts {
356 Mount(crate::mount::MountOpts),
358 #[cfg(feature = "install-to-disk")]
369 ToDisk(crate::install::InstallToDiskOpts),
370 ToFilesystem(crate::install::InstallToFilesystemOpts),
377 ToExistingRoot(crate::install::InstallToExistingRootOpts),
384 #[clap(hide = true)]
389 Reset(crate::install::InstallResetOpts),
390 Finalize {
393 root_path: Utf8PathBuf,
395 },
396 EnsureCompletion {},
404 PrintConfiguration(crate::install::InstallPrintConfigurationOpts),
411}
412
413#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
415pub(crate) enum ContainerOpts {
416 Inspect {
421 #[clap(long, default_value = "/")]
423 rootfs: Utf8PathBuf,
424
425 #[clap(long)]
427 json: bool,
428
429 #[clap(long, conflicts_with = "json")]
431 format: Option<OutputFormat>,
432 },
433 Lint {
439 #[clap(long, default_value = "/")]
441 rootfs: Utf8PathBuf,
442
443 #[clap(long)]
445 fatal_warnings: bool,
446
447 #[clap(long)]
452 list: bool,
453
454 #[clap(long)]
459 skip: Vec<String>,
460
461 #[clap(long)]
464 no_truncate: bool,
465 },
466 ComputeComposefsDigest {
472 #[clap(default_value = "/target")]
474 path: Utf8PathBuf,
475
476 #[clap(long)]
478 write_dumpfile_to: Option<Utf8PathBuf>,
479
480 #[clap(long, default_value = "v1")]
485 erofs_version: ErofsVersionArg,
486 },
487 #[clap(hide = true)]
489 ComputeComposefsDigestFromStorage {
490 #[clap(long)]
492 write_dumpfile_to: Option<Utf8PathBuf>,
493
494 #[clap(long, default_value = "v1")]
499 erofs_version: ErofsVersionArg,
500
501 image: Option<String>,
503 },
504 SplitKernelAndRootfs {
515 #[clap(long, default_value = "/")]
517 rootfs: Utf8PathBuf,
518
519 #[clap(long)]
521 output: Utf8PathBuf,
522 },
523 Ukify {
532 #[clap(long, default_value = "/")]
534 rootfs: Utf8PathBuf,
535
536 #[clap(long = "karg", hide = true)]
540 kargs: Vec<String>,
541
542 #[clap(long)]
544 allow_missing_verity: bool,
545
546 #[clap(long)]
554 erofs_version: Option<BootArtifactErofsVersionArg>,
555
556 #[clap(long)]
558 write_dumpfile_to: Option<Utf8PathBuf>,
559
560 #[clap(long)]
565 kernel_dir: Option<Utf8PathBuf>,
566
567 #[clap(last = true)]
569 args: Vec<OsString>,
570 },
571 Aboot {
573 #[clap(long, default_value = "/")]
575 rootfs: Utf8PathBuf,
576
577 #[clap(long)]
579 allow_missing_verity: bool,
580
581 #[clap(long)]
587 erofs_version: Option<BootArtifactErofsVersionArg>,
588
589 #[clap(long)]
591 write_dumpfile_to: Option<Utf8PathBuf>,
592
593 #[clap(long)]
596 kernel_dir: Option<Utf8PathBuf>,
597
598 #[clap(long)]
600 out: Option<Utf8PathBuf>,
601 },
602 #[clap(hide = true)]
610 Export {
611 #[clap(long, default_value = "tar")]
613 format: ExportFormat,
614
615 #[clap(long, short = 'o')]
617 output: Option<Utf8PathBuf>,
618
619 #[clap(long)]
622 kernel_in_boot: bool,
623
624 #[clap(long, default_value = "enabled")]
626 selinux: ExportSelinuxMode,
627
628 target: Utf8PathBuf,
630 },
631}
632
633#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
635pub(crate) enum ErofsVersionArg {
636 V1,
638 V2,
640}
641
642impl From<ErofsVersionArg> for FormatVersion {
643 fn from(v: ErofsVersionArg) -> Self {
644 match v {
645 ErofsVersionArg::V1 => FormatVersion::V1,
646 ErofsVersionArg::V2 => FormatVersion::V2,
647 }
648 }
649}
650
651#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
653pub(crate) enum BootArtifactErofsVersionArg {
654 V1,
656 V1Only,
658 V2,
660}
661
662#[derive(Debug, Clone, ValueEnum, PartialEq, Eq)]
663pub(crate) enum ExportSelinuxMode {
664 Enabled,
666 WarnOnMissing,
668 Disabled,
670}
671
672#[derive(Debug, Clone, ValueEnum, PartialEq, Eq)]
673pub(crate) enum ExportFormat {
674 Tar,
676}
677
678#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
680pub(crate) enum ImageCmdOpts {
681 List {
683 #[clap(allow_hyphen_values = true)]
684 args: Vec<OsString>,
685 },
686 Build {
688 #[clap(allow_hyphen_values = true)]
689 args: Vec<OsString>,
690 },
691 Pull {
693 #[clap(required = true)]
695 images: Vec<String>,
696 },
697 Push {
699 #[clap(allow_hyphen_values = true)]
700 args: Vec<OsString>,
701 },
702}
703
704#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
705#[serde(rename_all = "kebab-case")]
706pub(crate) enum ImageListType {
707 #[default]
709 All,
710 Logical,
712 Host,
714}
715
716impl std::fmt::Display for ImageListType {
717 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718 self.to_possible_value().unwrap().get_name().fmt(f)
719 }
720}
721
722#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
723#[serde(rename_all = "kebab-case")]
724pub(crate) enum ImageListFormat {
725 #[default]
727 Table,
728 Json,
730}
731impl std::fmt::Display for ImageListFormat {
732 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
733 self.to_possible_value().unwrap().get_name().fmt(f)
734 }
735}
736
737#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
739pub(crate) enum ImageOpts {
740 List {
744 #[clap(long = "type")]
746 #[arg(default_value_t)]
747 list_type: ImageListType,
748 #[clap(long = "format")]
749 #[arg(default_value_t)]
750 list_format: ImageListFormat,
751 },
752 CopyToStorage {
769 #[clap(long)]
770 source: Option<String>,
772
773 #[clap(long)]
774 target: Option<String>,
777 },
778 SetUnified,
783 PullFromDefaultStorage {
785 image: String,
787 },
788 #[clap(subcommand)]
790 Cmd(ImageCmdOpts),
791}
792
793#[derive(Debug, Clone, clap::ValueEnum, PartialEq, Eq)]
794pub(crate) enum SchemaType {
795 Host,
796 Progress,
797}
798
799#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
801pub(crate) enum FsverityOpts {
802 Measure {
804 path: Utf8PathBuf,
806 },
807 Enable {
809 path: Utf8PathBuf,
811 },
812}
813
814#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
815pub(crate) enum UkiSubcommands {
816 Extract {
820 path: Utf8PathBuf,
822 output_path: Utf8PathBuf,
824 },
825}
826
827#[cfg(feature = "selinux")]
829#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
830pub(crate) enum SelinuxOpts {
831 IsUnlabeled {
833 #[arg(value_parser = parse_absolute_path)]
835 path: Utf8PathBuf,
836 },
837}
838
839pub(crate) fn parse_absolute_path(value: &str) -> std::result::Result<Utf8PathBuf, String> {
840 let path = Utf8PathBuf::from(value);
841 if path.is_absolute() {
842 Ok(path)
843 } else {
844 Err("path must be absolute".to_string())
845 }
846}
847
848#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
850pub(crate) enum InternalsOpts {
851 SystemdGenerator {
852 normal_dir: Utf8PathBuf,
853 #[allow(dead_code)]
854 early_dir: Option<Utf8PathBuf>,
855 #[allow(dead_code)]
856 late_dir: Option<Utf8PathBuf>,
857 },
858 FixupEtcFstab,
859 SysusersSync,
862 PrintJsonSchema {
864 #[clap(long)]
865 of: SchemaType,
866 },
867 #[clap(subcommand)]
868 Fsverity(FsverityOpts),
869 #[cfg(feature = "selinux")]
870 #[clap(subcommand)]
871 Selinux(SelinuxOpts),
872 Fsck,
874 Cleanup,
876 Relabel {
877 #[clap(long)]
878 as_path: Option<Utf8PathBuf>,
880
881 path: Utf8PathBuf,
883 },
884 RelabelOverlayMountpoints,
887 OstreeExt {
889 #[clap(allow_hyphen_values = true)]
890 args: Vec<OsString>,
891 },
892 Cfs {
894 #[clap(allow_hyphen_values = true)]
895 args: Vec<OsString>,
896 },
897 OstreeContainer {
899 #[clap(allow_hyphen_values = true)]
900 args: Vec<OsString>,
901 },
902 TestComposefs,
904 ComposefsAbootReconcile,
906 LoopbackCleanupHelper {
908 #[clap(long)]
910 device: String,
911 },
912 AllocateCleanupLoopback {
914 #[clap(long)]
916 file_path: Utf8PathBuf,
917 },
918 BootcInstallCompletion {
920 sysroot: Utf8PathBuf,
922
923 stateroot: String,
925 },
926 Reboot,
929 #[cfg(feature = "rhsm")]
930 PublishRhsmFacts,
932 DirDiff {
934 pristine_etc: Utf8PathBuf,
936 current_etc: Utf8PathBuf,
938 new_etc: Utf8PathBuf,
940 #[clap(long)]
942 merge: bool,
943 },
944 #[cfg(feature = "docgen")]
945 DumpCliJson,
947 PrepSoftReboot {
948 #[clap(required_unless_present = "reset")]
949 deployment: Option<String>,
950 #[clap(long, conflicts_with = "reset")]
951 reboot: bool,
952 #[clap(long, conflicts_with = "reboot")]
953 reset: bool,
954 },
955 ComposefsGC {
956 #[clap(long)]
957 dry_run: bool,
958 #[clap(long)]
961 assert_no_op: bool,
962 #[clap(long)]
964 prune_repo: bool,
965 },
966 #[clap(subcommand)]
968 Blockdev(BlockdevOpts),
969 #[clap(subcommand)]
971 Uki(UkiSubcommands),
972}
973
974#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
976pub(crate) enum BlockdevOpts {
977 Ls {
983 device: Utf8PathBuf,
985 },
986 LsFilesystem {
991 path: Utf8PathBuf,
993 },
994}
995
996#[derive(Debug, Parser, PartialEq, Eq)]
998pub(crate) struct SetOptionsForSourceOpts {
999 #[clap(long)]
1004 pub(crate) source: String,
1005
1006 #[clap(long)]
1011 pub(crate) options: Option<String>,
1012}
1013
1014#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
1022pub(crate) enum LoaderEntriesOpts {
1023 SetOptionsForSource(SetOptionsForSourceOpts),
1042}
1043
1044const LOADER_ENTRIES_SUBCOMMAND: &str = "loader-entries";
1046
1047#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
1048pub(crate) enum StateOpts {
1049 WipeOstree,
1051}
1052
1053impl InternalsOpts {
1054 const GENERATOR_BIN: &'static str = "bootc-systemd-generator";
1056}
1057
1058#[derive(Debug, Clone, Copy, clap::ValueEnum, PartialEq, Eq)]
1059pub(crate) enum UkiAddonScope {
1060 Global,
1061 Scoped,
1062}
1063
1064#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
1065pub(crate) enum UkiAddonCliOpts {
1066 List {
1068 #[clap(long)]
1070 json: bool,
1071 #[clap(long)]
1073 verbose: bool,
1074 },
1075 Remove {
1079 name: String,
1081 addon_type: UkiAddonScope,
1083 },
1084 Add {
1088 name: String,
1090 addon_type: UkiAddonScope,
1092 },
1093 ListReferenced {
1095 #[clap(long)]
1097 json: bool,
1098 },
1099}
1100
1101#[derive(Debug, Parser, PartialEq, Eq)]
1109#[clap(name = "bootc")]
1110#[clap(rename_all = "kebab-case")]
1111#[clap(version,long_version=clap::crate_version!())]
1112#[allow(clippy::large_enum_variant)]
1113pub(crate) enum Opt {
1114 #[clap(alias = "update")]
1127 Upgrade(UpgradeOpts),
1128 Switch(SwitchOpts),
1139 #[command(after_help = indoc! {r#"
1151 Note on Rollbacks and the `/etc` Directory:
1152
1153 When you perform a rollback (e.g., with `bootc rollback`), any
1154 changes made to files in the `/etc` directory won't carry over
1155 to the rolled-back deployment. The `/etc` files will revert
1156 to their state from that previous deployment instead.
1157
1158 This is because `bootc rollback` just reorders the existing
1159 deployments. It doesn't create new deployments. The `/etc`
1160 merges happen when new deployments are created.
1161 "#})]
1162 Rollback(RollbackOpts),
1163 Edit(EditOpts),
1173 Status(StatusOpts),
1177 #[clap(alias = "usroverlay")]
1181 UsrOverlay(UsrOverlayOpts),
1182 #[clap(subcommand)]
1186 Install(InstallOpts),
1187 #[clap(subcommand)]
1189 Container(ContainerOpts),
1190 #[clap(subcommand, hide = true)]
1194 Image(ImageOpts),
1195 #[clap(subcommand, name = LOADER_ENTRIES_SUBCOMMAND)]
1202 LoaderEntries(LoaderEntriesOpts),
1203 #[clap(hide = true)]
1205 ExecInHostMountNamespace {
1206 #[clap(trailing_var_arg = true, allow_hyphen_values = true)]
1207 args: Vec<OsString>,
1208 },
1209 #[clap(hide = true)]
1211 #[clap(subcommand)]
1212 State(StateOpts),
1213 #[clap(subcommand)]
1214 #[clap(hide = true)]
1215 Internals(InternalsOpts),
1216 #[clap(hide = true)]
1217 ComposefsFinalizeStaged(ComposefsFinalizeStagedOpts),
1218 #[clap(hide = true)]
1220 ConfigDiff,
1221 #[clap(hide = true)]
1225 Completion {
1226 #[clap(value_enum)]
1228 shell: clap_complete::aot::Shell,
1229 },
1230 #[clap(hide = true)]
1231 DeleteDeployment { depl_id: String },
1232
1233 #[clap(subcommand)]
1235 UkiAddon(UkiAddonCliOpts),
1236}
1237
1238#[context("Ensuring mountns")]
1243pub(crate) fn ensure_self_unshared_mount_namespace(reexec_env: &[(&str, &str)]) -> Result<()> {
1244 let uid = rustix::process::getuid();
1245 if !uid.is_root() {
1246 tracing::debug!("Not root, assuming no need to unshare");
1247 return Ok(());
1248 }
1249 let recurse_env = "_ostree_unshared";
1250 let ns_pid1 = std::fs::read_link("/proc/1/ns/mnt").context("Reading /proc/1/ns/mnt")?;
1251 let ns_self = std::fs::read_link("/proc/self/ns/mnt").context("Reading /proc/self/ns/mnt")?;
1252 if ns_pid1 != ns_self {
1254 tracing::debug!("Already in a mount namespace");
1255 return Ok(());
1256 }
1257 if std::env::var_os(recurse_env).is_some() {
1258 let am_pid1 = rustix::process::getpid().is_init();
1259 if am_pid1 {
1260 tracing::debug!("We are pid 1");
1261 return Ok(());
1262 } else {
1263 anyhow::bail!("Failed to unshare mount namespace");
1264 }
1265 }
1266
1267 bootc_utils::reexec::reexec_with_guardenv(
1271 recurse_env,
1272 &["unshare", "-m", "--propagation=slave", "--"],
1273 reexec_env,
1274 )
1275}
1276
1277#[context("Initializing storage")]
1280pub(crate) async fn get_storage() -> Result<crate::store::BootedStorage> {
1281 let env = crate::store::Environment::detect()?;
1282 prepare_for_write()?;
1285 let r = BootedStorage::new(env, crate::store::EspAccess::ReadWrite)
1286 .await?
1287 .ok_or_else(|| anyhow!("System not booted via bootc"))?;
1288 r.require_writable()?;
1289 Ok(r)
1290}
1291
1292#[context("Querying root privilege")]
1293pub(crate) fn require_root(is_container: bool) -> Result<()> {
1294 ensure!(
1295 rustix::process::getuid().is_root(),
1296 if is_container {
1297 "The user inside the container from which you are running this command must be root"
1298 } else {
1299 "This command must be executed as the root user"
1300 }
1301 );
1302
1303 ensure!(
1304 rustix::thread::capability_is_in_bounding_set(rustix::thread::CapabilitySet::SYS_ADMIN)?,
1305 if is_container {
1306 "The container must be executed with full privileges (e.g. --privileged flag)"
1307 } else {
1308 "This command requires full root privileges (CAP_SYS_ADMIN)"
1309 }
1310 );
1311
1312 tracing::trace!("Verified uid 0 with CAP_SYS_ADMIN");
1313
1314 Ok(())
1315}
1316
1317fn has_soft_reboot_capability(deployment: Option<&crate::spec::BootEntry>) -> bool {
1319 deployment.map(|d| d.soft_reboot_capable).unwrap_or(false)
1320}
1321
1322#[context("Preparing soft reboot")]
1324fn prepare_soft_reboot(sysroot: &SysrootLock, deployment: &ostree::Deployment) -> Result<()> {
1325 let cancellable = ostree::gio::Cancellable::NONE;
1326 sysroot
1327 .deployment_set_soft_reboot(deployment, false, cancellable)
1328 .context("Failed to prepare soft-reboot")?;
1329 Ok(())
1330}
1331
1332#[context("Handling soft reboot")]
1334fn handle_soft_reboot<F>(
1335 soft_reboot_mode: Option<SoftRebootMode>,
1336 entry: Option<&crate::spec::BootEntry>,
1337 deployment_type: &str,
1338 execute_soft_reboot: F,
1339) -> Result<()>
1340where
1341 F: FnOnce() -> Result<()>,
1342{
1343 let Some(mode) = soft_reboot_mode else {
1344 return Ok(());
1345 };
1346
1347 let can_soft_reboot = has_soft_reboot_capability(entry);
1348 match mode {
1349 SoftRebootMode::Required => {
1350 if can_soft_reboot {
1351 execute_soft_reboot()?;
1352 } else {
1353 anyhow::bail!(
1354 "Soft reboot was required but {} deployment is not soft-reboot capable",
1355 deployment_type
1356 );
1357 }
1358 }
1359 SoftRebootMode::Auto => {
1360 if can_soft_reboot {
1361 execute_soft_reboot()?;
1362 }
1363 }
1364 }
1365 Ok(())
1366}
1367
1368#[context("Handling staged soft reboot")]
1370fn handle_staged_soft_reboot(
1371 booted_ostree: &BootedOstree<'_>,
1372 soft_reboot_mode: Option<SoftRebootMode>,
1373 host: &crate::spec::Host,
1374) -> Result<()> {
1375 handle_soft_reboot(
1376 soft_reboot_mode,
1377 host.status.staged.as_ref(),
1378 "staged",
1379 || soft_reboot_staged(booted_ostree.sysroot),
1380 )
1381}
1382
1383#[context("Soft reboot staged deployment")]
1385fn soft_reboot_staged(sysroot: &SysrootLock) -> Result<()> {
1386 println!("Staged deployment is soft-reboot capable, preparing for soft-reboot...");
1387
1388 let deployments_list = sysroot.deployments();
1389 let staged_deployment = deployments_list
1390 .iter()
1391 .find(|d| d.is_staged())
1392 .ok_or_else(|| anyhow::anyhow!("Failed to find staged deployment"))?;
1393
1394 prepare_soft_reboot(sysroot, staged_deployment)?;
1395 Ok(())
1396}
1397
1398#[context("Soft reboot rollback deployment")]
1400fn soft_reboot_rollback(booted_ostree: &BootedOstree<'_>) -> Result<()> {
1401 println!("Rollback deployment is soft-reboot capable, preparing for soft-reboot...");
1402
1403 let deployments_list = booted_ostree.sysroot.deployments();
1404 let target_deployment = deployments_list
1405 .first()
1406 .ok_or_else(|| anyhow::anyhow!("No rollback deployment found!"))?;
1407
1408 prepare_soft_reboot(booted_ostree.sysroot, target_deployment)
1409}
1410
1411#[context("Preparing for write")]
1415pub(crate) fn prepare_for_write() -> Result<()> {
1416 use std::sync::atomic::{AtomicBool, Ordering};
1417
1418 static ENTERED: AtomicBool = AtomicBool::new(false);
1424 if ENTERED.load(Ordering::SeqCst) {
1425 return Ok(());
1426 }
1427 if ostree_ext::container_utils::running_in_container() {
1428 anyhow::bail!("Detected container; this command requires a booted host system.");
1429 }
1430 crate::cli::require_root(false)?;
1431 ensure_self_unshared_mount_namespace(&[])?;
1432 if crate::lsm::selinux_enabled() && !crate::lsm::selinux_ensure_install(&[])? {
1433 tracing::debug!("Do not have install_t capabilities");
1434 }
1435 ENTERED.store(true, Ordering::SeqCst);
1436 Ok(())
1437}
1438
1439struct ApplyFromDownloadedOpts {
1440 soft_reboot: Option<SoftRebootMode>,
1441 apply: bool,
1442}
1443
1444async fn apply_from_downloaded_ostree(
1445 storage: &Storage,
1446 booted_ostree: &BootedOstree<'_>,
1447 host: &crate::spec::Host,
1448 opts: &ApplyFromDownloadedOpts,
1449) -> Result<()> {
1450 let ostree = storage.get_ostree()?;
1451 let staged_deployment = ostree
1452 .staged_deployment()
1453 .ok_or_else(|| anyhow::anyhow!("No staged deployment found"))?;
1454
1455 if staged_deployment.is_finalization_locked() {
1456 crate::boundimage::pull_bound_images(storage, &staged_deployment).await?;
1457 ostree.change_finalization(&staged_deployment)?;
1458 println!("Staged deployment will now be applied on reboot");
1459 } else {
1460 println!("Staged deployment is already set to apply on reboot");
1461 }
1462
1463 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &host)?;
1464 if opts.apply {
1465 crate::reboot::reboot()?;
1466 }
1467 return Ok(());
1468}
1469
1470#[context("Upgrading")]
1472async fn upgrade(
1473 opts: UpgradeOpts,
1474 storage: &Storage,
1475 booted_ostree: &BootedOstree<'_>,
1476) -> Result<()> {
1477 let delta = crate::delta::open_opt(opts.from_delta.as_deref())
1478 .await?
1479 .map(std::sync::Arc::new);
1480
1481 let repo = &booted_ostree.repo();
1482
1483 let host = crate::status::get_status(booted_ostree)?.1;
1484 let current_image = host.spec.image.as_ref();
1485
1486 let derived_image = if let Some(ref tag) = opts.tag {
1488 let image = current_image.ok_or_else(|| {
1489 anyhow::anyhow!("--tag requires a booted image with a specified source")
1490 })?;
1491 Some(image.with_tag(tag)?)
1492 } else {
1493 None
1494 };
1495
1496 let imgref = derived_image.as_ref().or(current_image);
1497 let prog: ProgressWriter = opts.progress.try_into()?;
1498
1499 if imgref.is_none() {
1501 let booted_incompatible = host.status.booted.as_ref().is_some_and(|b| b.incompatible);
1502
1503 let staged_incompatible = host.status.staged.as_ref().is_some_and(|b| b.incompatible);
1504
1505 if booted_incompatible || staged_incompatible {
1506 return Err(anyhow::anyhow!(
1507 "Deployment contains local rpm-ostree modifications; cannot upgrade via bootc. You can run `rpm-ostree reset` to undo the modifications."
1508 ));
1509 }
1510 }
1511
1512 let imgref = imgref.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
1513 let spec = RequiredHostSpec { image: imgref };
1515 let booted_image = host
1516 .status
1517 .booted
1518 .as_ref()
1519 .map(|b| b.query_image(repo))
1520 .transpose()?
1521 .flatten();
1522 let staged = host.status.staged.as_ref();
1524 let staged_image = staged.as_ref().and_then(|s| s.image.as_ref());
1525 let mut changed = false;
1526
1527 if opts.download_opts.from_downloaded {
1529 return apply_from_downloaded_ostree(
1530 storage,
1531 booted_ostree,
1532 &host,
1533 &ApplyFromDownloadedOpts {
1534 soft_reboot: opts.soft_reboot,
1535 apply: opts.apply,
1536 },
1537 )
1538 .await;
1539 }
1540
1541 let use_unified = crate::deploy::image_exists_in_unified_storage(storage, imgref).await?;
1545 if let Some(delta) = delta.as_deref() {
1546 crate::delta::reject_unified_storage(delta, use_unified)?;
1547 }
1548
1549 if opts.check {
1550 let ostree_imgref = imgref.clone().into();
1551 let mut imp =
1552 crate::deploy::new_importer(repo, &ostree_imgref, Some(&booted_ostree.deployment))
1553 .await?;
1554 match imp.prepare().await? {
1555 PrepareResult::AlreadyPresent(_) => {
1556 println!("No changes in: {ostree_imgref:#}");
1557 }
1558 PrepareResult::Ready(r) => {
1559 crate::deploy::check_bootc_label(&r.config);
1560 println!("Update available for: {ostree_imgref:#}");
1561 if let Some(version) = r.version() {
1562 println!(" Version: {version}");
1563 }
1564 println!(" Digest: {}", r.manifest_digest);
1565 changed = true;
1566 if let Some(previous_image) = booted_image.as_ref() {
1567 let diff =
1568 ostree_container::ManifestDiff::new(&previous_image.manifest, &r.manifest);
1569 diff.print();
1570 }
1571 }
1572 }
1573 } else {
1574 let fetched = if let Some(delta) = delta.clone() {
1575 crate::deploy::pull_delta(
1576 repo,
1577 imgref,
1578 delta,
1579 opts.quiet,
1580 prog.clone(),
1581 Some(&booted_ostree.deployment),
1582 )
1583 .await?
1584 } else if use_unified {
1585 crate::deploy::pull_unified(
1586 repo,
1587 imgref,
1588 None,
1589 opts.quiet,
1590 prog.clone(),
1591 storage,
1592 Some(&booted_ostree.deployment),
1593 )
1594 .await?
1595 } else {
1596 crate::deploy::pull(
1597 repo,
1598 imgref,
1599 None,
1600 opts.quiet,
1601 prog.clone(),
1602 Some(&booted_ostree.deployment),
1603 )
1604 .await?
1605 };
1606 let staged_digest = staged_image.map(|s| s.digest().expect("valid digest in status"));
1607 let fetched_digest = &fetched.manifest_digest;
1608 tracing::debug!("staged: {staged_digest:?}");
1609 tracing::debug!("fetched: {fetched_digest}");
1610 let staged_unchanged = staged_digest
1611 .as_ref()
1612 .map(|d| d == fetched_digest)
1613 .unwrap_or_default();
1614 let booted_unchanged = booted_image
1615 .as_ref()
1616 .map(|img| &img.manifest_digest == fetched_digest)
1617 .unwrap_or_default();
1618 if staged_unchanged {
1619 let staged_deployment = storage.get_ostree()?.staged_deployment();
1620 let mut download_only_changed = false;
1621
1622 if let Some(staged) = staged_deployment {
1623 if opts.download_opts.download_only {
1625 if !staged.is_finalization_locked() {
1627 storage.get_ostree()?.change_finalization(&staged)?;
1628 println!("Image downloaded, but will not be applied on reboot");
1629 download_only_changed = true;
1630 }
1631 } else if !opts.check {
1632 if staged.is_finalization_locked() {
1635 storage.get_ostree()?.change_finalization(&staged)?;
1636 println!("Staged deployment will now be applied on reboot");
1637 download_only_changed = true;
1638 }
1639 }
1640 } else if opts.download_opts.download_only || opts.apply {
1641 anyhow::bail!("No staged deployment found");
1642 }
1643
1644 if !download_only_changed {
1645 println!("Staged update present, not changed");
1646 }
1647
1648 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &host)?;
1649 if opts.apply {
1650 crate::reboot::reboot()?;
1651 }
1652 } else if booted_unchanged {
1653 println!("No update available.")
1654 } else {
1655 let stateroot = booted_ostree.stateroot();
1656 let from = MergeState::from_stateroot(storage, &stateroot)?;
1657 crate::deploy::stage(
1658 storage,
1659 from,
1660 &fetched,
1661 &spec,
1662 prog.clone(),
1663 opts.download_opts.download_only,
1664 )
1665 .await?;
1666 changed = true;
1667 if let Some(prev) = booted_image.as_ref() {
1668 if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
1669 let diff =
1670 ostree_container::ManifestDiff::new(&prev.manifest, &fetched_manifest);
1671 diff.print();
1672 }
1673 }
1674 }
1675 }
1676 if changed {
1677 storage.update_mtime()?;
1678
1679 if opts.soft_reboot.is_some() {
1680 let updated_host = crate::status::get_status(booted_ostree)?.1;
1683 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &updated_host)?;
1684 }
1685
1686 if opts.apply {
1687 crate::reboot::reboot()?;
1688 }
1689 } else {
1690 tracing::debug!("No changes");
1691 }
1692
1693 Ok(())
1694}
1695
1696#[context("Getting imgref for switch")]
1697pub(crate) fn imgref_for_switch(opts: &SwitchOpts) -> Result<ImageReference> {
1698 let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
1699 let imgref = ostree_container::ImageReference {
1700 transport,
1701 name: opts
1702 .target
1703 .as_ref()
1704 .ok_or_else(|| anyhow!("Target image not found"))?
1705 .to_string(),
1706 };
1707 let sigverify = sigpolicy_from_opt(opts.enforce_container_sigpolicy);
1708 let target = ostree_container::OstreeImageReference { sigverify, imgref };
1709 let target = ImageReference::from(target);
1710
1711 return Ok(target);
1712}
1713
1714#[context("Switching (ostree)")]
1716async fn switch_ostree(
1717 opts: SwitchOpts,
1718 storage: &Storage,
1719 booted_ostree: &BootedOstree<'_>,
1720) -> Result<()> {
1721 let delta = crate::delta::open_opt(opts.from_delta.as_deref())
1722 .await?
1723 .map(std::sync::Arc::new);
1724
1725 let (_, host) = crate::status::get_status(booted_ostree)?;
1726
1727 if opts.download_opts.from_downloaded {
1728 return apply_from_downloaded_ostree(
1729 storage,
1730 booted_ostree,
1731 &host,
1732 &ApplyFromDownloadedOpts {
1733 soft_reboot: opts.soft_reboot,
1734 apply: opts.apply,
1735 },
1736 )
1737 .await;
1738 }
1739
1740 let target = imgref_for_switch(&opts)?;
1741 let prog: ProgressWriter = opts.progress.try_into()?;
1742 let cancellable = gio::Cancellable::NONE;
1743
1744 let repo = &booted_ostree.repo();
1745
1746 let new_spec = {
1747 let mut new_spec = host.spec.clone();
1748 new_spec.image = Some(target.clone());
1749 new_spec
1750 };
1751
1752 if new_spec == host.spec {
1753 println!("Image specification is unchanged.");
1754 if opts.apply && host.status.staged.is_some() {
1755 crate::reboot::reboot()?;
1756 }
1757 return Ok(());
1758 }
1759
1760 const SWITCH_JOURNAL_ID: &str = "7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1";
1762 let old_image = host
1763 .spec
1764 .image
1765 .as_ref()
1766 .map(|i| i.image.as_str())
1767 .unwrap_or("none");
1768
1769 tracing::info!(
1770 message_id = SWITCH_JOURNAL_ID,
1771 bootc.old_image_reference = old_image,
1772 bootc.new_image_reference = &target.image,
1773 bootc.new_image_transport = &target.transport,
1774 "Switching from image {} to {}",
1775 old_image,
1776 target.image
1777 );
1778
1779 let new_spec = RequiredHostSpec::from_spec(&new_spec)?;
1780
1781 let use_unified = if opts.unified_storage_exp {
1785 true
1786 } else {
1787 crate::deploy::image_exists_in_unified_storage(storage, &target).await?
1788 };
1789 if let Some(delta) = delta.as_deref() {
1790 crate::delta::reject_unified_storage(delta, use_unified)?;
1791 }
1792
1793 let fetched = if let Some(delta) = delta.clone() {
1794 crate::deploy::pull_delta(
1795 repo,
1796 &target,
1797 delta,
1798 opts.quiet,
1799 prog.clone(),
1800 Some(&booted_ostree.deployment),
1801 )
1802 .await?
1803 } else if use_unified {
1804 crate::deploy::pull_unified(
1805 repo,
1806 &target,
1807 None,
1808 opts.quiet,
1809 prog.clone(),
1810 storage,
1811 Some(&booted_ostree.deployment),
1812 )
1813 .await?
1814 } else {
1815 crate::deploy::pull(
1816 repo,
1817 &target,
1818 None,
1819 opts.quiet,
1820 prog.clone(),
1821 Some(&booted_ostree.deployment),
1822 )
1823 .await?
1824 };
1825
1826 if !opts.retain {
1827 if let Some(booted_origin) = booted_ostree.deployment.origin() {
1829 if let Some(ostree_ref) = booted_origin.optional_string("origin", "refspec")? {
1830 let (remote, ostree_ref) =
1831 ostree::parse_refspec(&ostree_ref).context("Failed to parse ostree ref")?;
1832 repo.set_ref_immediate(remote.as_deref(), &ostree_ref, None, cancellable)?;
1833 }
1834 }
1835 }
1836
1837 let stateroot = booted_ostree.stateroot();
1838 let from = MergeState::from_stateroot(storage, &stateroot)?;
1839 crate::deploy::stage(
1840 storage,
1841 from,
1842 &fetched,
1843 &new_spec,
1844 prog.clone(),
1845 opts.download_opts.download_only,
1846 )
1847 .await?;
1848
1849 storage.update_mtime()?;
1850
1851 if opts.download_opts.download_only {
1852 return Ok(());
1853 }
1854
1855 if opts.soft_reboot.is_some() {
1856 let updated_host = crate::status::get_status(booted_ostree)?.1;
1859 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &updated_host)?;
1860 }
1861
1862 if opts.apply {
1865 crate::reboot::reboot()?;
1866 }
1867
1868 Ok(())
1869}
1870
1871#[context("Switching")]
1873async fn switch(opts: SwitchOpts) -> Result<()> {
1874 if opts.mutate_in_place {
1878 let target = imgref_for_switch(&opts)?;
1879 let deployid = {
1880 let target = target.clone();
1882 let root = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1883 tokio::task::spawn_blocking(move || {
1884 crate::deploy::switch_origin_inplace(&root, &target)
1885 })
1886 .await??
1887 };
1888 println!("Updated {deployid} to pull from {target}");
1889 return Ok(());
1890 }
1891 let storage = &get_storage().await?;
1892 match storage.kind()? {
1893 BootedStorageKind::Ostree(booted_ostree) => {
1894 if opts.uki_addon_opts.scoped.is_some() || opts.uki_addon_opts.global.is_some() {
1895 anyhow::bail!("UKI Addon options are only supported for composefs backend");
1896 }
1897
1898 switch_ostree(opts, storage, &booted_ostree).await
1899 }
1900 BootedStorageKind::Composefs(booted_cfs) => {
1901 switch_composefs(opts, storage, &booted_cfs).await
1902 }
1903 }
1904}
1905
1906#[context("Rollback (ostree)")]
1908async fn rollback_ostree(
1909 opts: &RollbackOpts,
1910 storage: &Storage,
1911 booted_ostree: &BootedOstree<'_>,
1912) -> Result<()> {
1913 crate::deploy::rollback(storage).await?;
1914
1915 if opts.soft_reboot.is_some() {
1916 let host = crate::status::get_status(booted_ostree)?.1;
1918
1919 handle_soft_reboot(
1920 opts.soft_reboot,
1921 host.status.rollback.as_ref(),
1922 "rollback",
1923 || soft_reboot_rollback(booted_ostree),
1924 )?;
1925 }
1926
1927 Ok(())
1928}
1929
1930#[context("Rollback")]
1932async fn rollback(opts: &RollbackOpts) -> Result<()> {
1933 let storage = &get_storage().await?;
1934 match storage.kind()? {
1935 BootedStorageKind::Ostree(booted_ostree) => {
1936 rollback_ostree(opts, storage, &booted_ostree).await
1937 }
1938 BootedStorageKind::Composefs(booted_cfs) => composefs_rollback(storage, &booted_cfs).await,
1939 }
1940}
1941
1942#[context("Editing spec (ostree)")]
1944async fn edit_ostree(
1945 opts: EditOpts,
1946 storage: &Storage,
1947 booted_ostree: &BootedOstree<'_>,
1948) -> Result<()> {
1949 let repo = &booted_ostree.repo();
1950 let (_, host) = crate::status::get_status(booted_ostree)?;
1951
1952 let new_host: Host = if let Some(filename) = opts.filename {
1953 let mut r = std::io::BufReader::new(std::fs::File::open(filename)?);
1954 serde_yaml::from_reader(&mut r)?
1955 } else {
1956 let tmpf = tempfile::NamedTempFile::with_suffix(".yaml")?;
1957 serde_yaml::to_writer(std::io::BufWriter::new(tmpf.as_file()), &host)?;
1958 crate::utils::spawn_editor(&tmpf)?;
1959 tmpf.as_file().seek(std::io::SeekFrom::Start(0))?;
1960 serde_yaml::from_reader(&mut tmpf.as_file())?
1961 };
1962
1963 if new_host.spec == host.spec {
1964 println!("Edit cancelled, no changes made.");
1965 return Ok(());
1966 }
1967 host.spec.verify_transition(&new_host.spec)?;
1968 let new_spec = RequiredHostSpec::from_spec(&new_host.spec)?;
1969
1970 let prog = ProgressWriter::default();
1971
1972 if host.spec.boot_order != new_host.spec.boot_order {
1975 return crate::deploy::rollback(storage).await;
1976 }
1977
1978 let fetched = crate::deploy::pull(
1979 repo,
1980 new_spec.image,
1981 None,
1982 opts.quiet,
1983 prog.clone(),
1984 Some(&booted_ostree.deployment),
1985 )
1986 .await?;
1987
1988 let stateroot = booted_ostree.stateroot();
1991 let from = MergeState::from_stateroot(storage, &stateroot)?;
1992 crate::deploy::stage(storage, from, &fetched, &new_spec, prog.clone(), false).await?;
1993
1994 storage.update_mtime()?;
1995
1996 Ok(())
1997}
1998
1999#[context("Editing spec")]
2001async fn edit(opts: EditOpts) -> Result<()> {
2002 let storage = &get_storage().await?;
2003 match storage.kind()? {
2004 BootedStorageKind::Ostree(booted_ostree) => {
2005 edit_ostree(opts, storage, &booted_ostree).await
2006 }
2007 BootedStorageKind::Composefs(_) => {
2008 anyhow::bail!("Edit is not yet supported for composefs backend")
2009 }
2010 }
2011}
2012
2013async fn usroverlay(access_mode: FilesystemOverlayAccessMode) -> Result<()> {
2015 let status = get_usr_overlay_status()?;
2016 if status.is_some() {
2017 println!("An overlayfs is already mounted on /usr");
2018 return Ok(());
2019 }
2020
2021 let args = match access_mode {
2024 FilesystemOverlayAccessMode::ReadOnly => ["admin", "unlock", "--transient"].as_slice(),
2026
2027 FilesystemOverlayAccessMode::ReadWrite => ["admin", "unlock"].as_slice(),
2028 };
2029 Err(Command::new("ostree").args(args).exec().into())
2030}
2031
2032fn join_host_ipc_namespace() -> Result<()> {
2045 let caps = rustix::thread::capabilities(None).context("capget")?;
2046 if !caps
2047 .effective
2048 .contains(rustix::thread::CapabilitySet::SYS_ADMIN)
2049 {
2050 return Ok(());
2051 }
2052 let ns_pid1 = match std::fs::read_link("/proc/1/ns/ipc") {
2053 Ok(v) => v,
2054 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
2055 return Ok(());
2056 }
2057 Err(e) => return Err(e).context("reading /proc/1/ns/ipc"),
2058 };
2059 let ns_self = std::fs::read_link("/proc/self/ns/ipc").context("reading /proc/self/ns/ipc")?;
2060 if ns_pid1 != ns_self {
2061 let pid1ipcns = std::fs::File::open("/proc/1/ns/ipc").context("open pid1 ipcns")?;
2062 rustix::thread::move_into_link_name_space(
2063 pid1ipcns.as_fd(),
2064 Some(rustix::thread::LinkNameSpaceType::InterProcessCommunication),
2065 )
2066 .context("setns(ipc)")?;
2067 }
2068 Ok(())
2069}
2070
2071#[allow(unsafe_code)]
2074pub fn global_init() -> Result<()> {
2075 join_host_ipc_namespace()?;
2076 ostree::glib::set_prgname(bootc_utils::NAME.into());
2079 if let Err(e) = rustix::thread::set_name(&CString::new(bootc_utils::NAME).unwrap()) {
2080 eprintln!("failed to set name: {e}");
2082 }
2083 ostree::SePolicy::set_null_log();
2085 let am_root = rustix::process::getuid().is_root();
2086 if std::env::var_os("HOME").is_none() && am_root {
2089 unsafe {
2094 std::env::set_var("HOME", "/root");
2095 }
2096 }
2097 Ok(())
2098}
2099
2100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2102pub enum CliExitStatus {
2103 Success,
2104 PredicateFalse,
2105}
2106
2107#[cfg(feature = "selinux")]
2108fn is_unlabeled_exit_status(
2109 state: crate::lsm::SELinuxLabelState,
2110 path: &Utf8Path,
2111) -> Result<CliExitStatus> {
2112 match state {
2113 crate::lsm::SELinuxLabelState::Unlabeled => Ok(CliExitStatus::Success),
2114 crate::lsm::SELinuxLabelState::Labeled => Ok(CliExitStatus::PredicateFalse),
2115 crate::lsm::SELinuxLabelState::Unsupported => {
2116 anyhow::bail!("SELinux labeling is unsupported for {path}")
2117 }
2118 }
2119}
2120
2121pub async fn run_from_iter<I>(args: I) -> Result<CliExitStatus>
2124where
2125 I: IntoIterator,
2126 I::Item: Into<OsString> + Clone,
2127{
2128 run_from_opt(Opt::parse_including_static(args)).await
2129}
2130
2131fn callname_from_argv0(argv0: &OsStr) -> &str {
2135 let default = "bootc";
2136 std::path::Path::new(argv0)
2137 .file_name()
2138 .and_then(|s| s.to_str())
2139 .filter(|s| !s.is_empty())
2140 .unwrap_or(default)
2141}
2142
2143impl Opt {
2144 fn parse_including_static<I>(args: I) -> Self
2147 where
2148 I: IntoIterator,
2149 I::Item: Into<OsString> + Clone,
2150 {
2151 let mut args = args.into_iter();
2152 let first = if let Some(first) = args.next() {
2153 let first: OsString = first.into();
2154 let argv0 = callname_from_argv0(&first);
2155 tracing::debug!("argv0={argv0:?}");
2156 let mapped = match argv0 {
2157 InternalsOpts::GENERATOR_BIN => {
2158 Some(["bootc", "internals", "systemd-generator"].as_slice())
2159 }
2160 "ostree-container" | "ostree-ima-sign" | "ostree-provisional-repair" => {
2161 Some(["bootc", "internals", "ostree-ext"].as_slice())
2162 }
2163 _ => None,
2164 };
2165 if let Some(base_args) = mapped {
2166 let base_args = base_args.iter().map(OsString::from);
2167 return Opt::parse_from_host(base_args.chain(args.map(|i| i.into())));
2168 }
2169 Some(first)
2170 } else {
2171 None
2172 };
2173 Opt::parse_from_host(first.into_iter().chain(args.map(|i| i.into())))
2174 }
2175
2176 fn parse_from_host<I>(args: I) -> Self
2184 where
2185 I: IntoIterator,
2186 I::Item: Into<OsString> + Clone,
2187 {
2188 use clap::FromArgMatches;
2189 let args: Vec<OsString> = args.into_iter().map(Into::into).collect();
2190 let bootconfig_extra = crate::loader_entries::ostree_supports_bootconfig_extra();
2191 let host_command = || apply_host_feature_gates(Opt::command(), bootconfig_extra);
2192 if host_feature_gate_rejects(&args, bootconfig_extra) {
2196 let err = host_command().error(
2197 clap::error::ErrorKind::InvalidSubcommand,
2198 crate::loader_entries::unsupported_ostree_error(),
2199 );
2200 let _ = err.print();
2202 std::process::exit(EXIT_UNAVAILABLE);
2203 }
2204 let mut matches = host_command().get_matches_from(args);
2205 match Self::from_arg_matches_mut(&mut matches) {
2206 Ok(opt) => opt,
2207 Err(e) => e.format(&mut host_command()).exit(),
2208 }
2209 }
2210}
2211
2212pub const EXIT_UNAVAILABLE: i32 = 77;
2220
2221fn apply_host_feature_gates(cmd: clap::Command, bootconfig_extra: bool) -> clap::Command {
2229 if bootconfig_extra {
2230 return cmd;
2231 }
2232 cmd.mut_subcommand(LOADER_ENTRIES_SUBCOMMAND, |c| c.hide(true))
2233}
2234
2235fn host_feature_gate_rejects(args: &[OsString], bootconfig_extra: bool) -> bool {
2240 if bootconfig_extra {
2241 return false;
2242 }
2243 args.iter()
2244 .skip(1)
2245 .find(|a| !a.to_string_lossy().starts_with('-'))
2246 .is_some_and(|a| a.as_os_str() == OsStr::new(LOADER_ENTRIES_SUBCOMMAND))
2247}
2248
2249async fn run_from_opt(opt: Opt) -> Result<CliExitStatus> {
2251 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2252 let result = match opt {
2253 Opt::Upgrade(opts) => {
2254 let storage = &get_storage().await?;
2255 match storage.kind()? {
2256 BootedStorageKind::Ostree(booted_ostree) => {
2257 if opts.uki_addon_opts.scoped.is_some() || opts.uki_addon_opts.global.is_some()
2258 {
2259 anyhow::bail!("UKI Addon options are only supported for composefs backend");
2260 }
2261
2262 upgrade(opts, storage, &booted_ostree).await
2263 }
2264 BootedStorageKind::Composefs(booted_cfs) => {
2265 upgrade_composefs(opts, storage, &booted_cfs).await
2266 }
2267 }
2268 }
2269 Opt::Switch(opts) => switch(opts).await,
2270 Opt::Rollback(opts) => {
2271 rollback(&opts).await?;
2272 if opts.apply {
2273 crate::reboot::reboot()?;
2274 }
2275 Ok(())
2276 }
2277 Opt::Edit(opts) => edit(opts).await,
2278 Opt::UsrOverlay(opts) => {
2279 use crate::store::Environment;
2280 let env = Environment::detect()?;
2281 let access_mode = if opts.read_only {
2282 FilesystemOverlayAccessMode::ReadOnly
2283 } else {
2284 FilesystemOverlayAccessMode::ReadWrite
2285 };
2286 match env {
2287 Environment::OstreeBooted => usroverlay(access_mode).await,
2288 Environment::ComposefsBooted(_) => composefs_usr_overlay(access_mode),
2289 _ => anyhow::bail!("usroverlay only applies on booted hosts"),
2290 }
2291 }
2292 Opt::Container(opts) => match opts {
2293 ContainerOpts::Inspect {
2294 rootfs,
2295 json,
2296 format,
2297 } => crate::status::container_inspect(&rootfs, json, format),
2298 ContainerOpts::Lint {
2299 rootfs,
2300 fatal_warnings,
2301 list,
2302 skip,
2303 no_truncate,
2304 } => {
2305 if list {
2306 return lints::lint_list(std::io::stdout().lock())
2307 .map(|()| CliExitStatus::Success);
2308 }
2309 let warnings = if fatal_warnings {
2310 lints::WarningDisposition::FatalWarnings
2311 } else {
2312 lints::WarningDisposition::AllowWarnings
2313 };
2314 let root_type = if rootfs == "/" {
2315 lints::RootType::Running
2316 } else {
2317 lints::RootType::Alternative
2318 };
2319
2320 let root = &Dir::open_ambient_dir(rootfs, cap_std::ambient_authority())?;
2321 let skip = skip.iter().map(|s| s.as_str());
2322 lints::lint(
2323 root,
2324 warnings,
2325 root_type,
2326 skip,
2327 std::io::stdout().lock(),
2328 no_truncate,
2329 )?;
2330 Ok(())
2331 }
2332 ContainerOpts::SplitKernelAndRootfs { rootfs, output } => {
2333 let root = Dir::open_ambient_dir(&rootfs, ambient_authority())
2334 .with_context(|| format!("Opening {rootfs}"))?;
2335 std::fs::create_dir_all(&output).with_context(|| format!("Creating {output}"))?;
2336 let output = Dir::open_ambient_dir(&output, ambient_authority())
2337 .with_context(|| format!("Opening {output}"))?;
2338 crate::kernel::split_kernel(&root, &output)?;
2339 Ok(())
2340 }
2341 ContainerOpts::ComputeComposefsDigest {
2342 path,
2343 write_dumpfile_to,
2344 erofs_version,
2345 } => {
2346 let digest = compute_composefs_digest(
2347 &path,
2348 erofs_version.into(),
2349 write_dumpfile_to.as_deref(),
2350 )
2351 .await?;
2352 println!("{digest}");
2353 Ok(())
2354 }
2355 ContainerOpts::ComputeComposefsDigestFromStorage {
2356 write_dumpfile_to,
2357 erofs_version,
2358 image,
2359 } => {
2360 let (_td_guard, repo) = new_temp_composefs_repo(erofs_version.into())?;
2361
2362 let mut proxycfg = crate::deploy::new_proxy_config();
2363
2364 let image = if let Some(image) = image {
2365 image
2366 } else {
2367 let host_container_store = Utf8Path::new("/run/host-container-storage");
2368 let container_info = crate::containerenv::get_container_execution_info(&root)?;
2371 let iid = container_info.imageid;
2372 tracing::debug!("Computing digest of {iid}");
2373
2374 if !host_container_store.try_exists()? {
2375 anyhow::bail!(
2376 "Must be readonly mount of host container store: {host_container_store}"
2377 );
2378 }
2379 let mut cmd = Command::new(bootc_utils::skopeo_bin());
2381 set_additional_image_store(&mut cmd, "/run/host-container-storage");
2382 proxycfg.skopeo_cmd = Some(cmd);
2383 iid
2384 };
2385
2386 let imgref = format!("containers-storage:{image}");
2387 let host_store = std::path::Path::new("/run/host-container-storage");
2388 let opts = composefs_oci::PullOptions {
2389 img_proxy_config: Some(proxycfg),
2390 additional_image_stores: &[host_store],
2391 ..Default::default()
2392 };
2393 let pull_result = composefs_oci::pull(&repo, &imgref, None, opts)
2394 .await
2395 .context("Pulling image")?;
2396 let mut fs = composefs_oci::image::create_filesystem(
2397 &repo,
2398 &pull_result.config_digest,
2399 Some(&pull_result.config_verity),
2400 &composefs_oci::OciTransformOptions::default(),
2401 )
2402 .context("Populating fs")?;
2403 fs.transform_for_boot(&repo).context("Preparing for boot")?;
2404 let id = fs.compute_image_id(repo.erofs_version());
2405 println!("{}", id.to_hex());
2406
2407 if let Some(path) = write_dumpfile_to.as_deref() {
2408 let mut w = File::create(path)
2409 .with_context(|| format!("Opening {path}"))
2410 .map(BufWriter::new)?;
2411 dumpfile::write_dumpfile(&mut w, &fs).context("Writing dumpfile")?;
2412 }
2413
2414 Ok(())
2415 }
2416 ContainerOpts::Ukify {
2417 rootfs,
2418 kargs,
2419 allow_missing_verity,
2420 erofs_version,
2421 write_dumpfile_to,
2422 kernel_dir,
2423 args,
2424 } => {
2425 crate::ukify::build_ukify(
2426 &rootfs,
2427 &kargs,
2428 &args,
2429 kernel_dir.as_deref(),
2430 allow_missing_verity,
2431 erofs_version,
2432 write_dumpfile_to.as_deref(),
2433 )
2434 .await
2435 }
2436 ContainerOpts::Aboot {
2437 rootfs,
2438 allow_missing_verity,
2439 erofs_version,
2440 write_dumpfile_to,
2441 kernel_dir,
2442 out,
2443 } => {
2444 crate::aboot::build_aboot(
2445 &rootfs,
2446 kernel_dir.as_deref(),
2447 out.as_deref(),
2448 allow_missing_verity,
2449 erofs_version,
2450 write_dumpfile_to.as_deref(),
2451 )
2452 .await
2453 }
2454 ContainerOpts::Export {
2455 format,
2456 target,
2457 output,
2458 kernel_in_boot,
2459 selinux,
2460 } => {
2461 crate::container_export::export(
2462 &format,
2463 &target,
2464 output.as_deref(),
2465 kernel_in_boot,
2466 &selinux,
2467 )
2468 .await
2469 }
2470 },
2471 Opt::Completion { shell } => {
2472 use clap_complete::aot::generate;
2473
2474 let mut cmd = Opt::command();
2475 let mut stdout = std::io::stdout();
2476 let bin_name = "bootc";
2477 generate(shell, &mut cmd, bin_name, &mut stdout);
2478 Ok(())
2479 }
2480 Opt::Image(opts) => match opts {
2481 ImageOpts::List {
2482 list_type,
2483 list_format,
2484 } => crate::image::list_entrypoint(list_type, list_format).await,
2485
2486 ImageOpts::CopyToStorage { source, target } => {
2487 let host = get_host().await?;
2489
2490 let storage = get_storage().await?;
2491
2492 match storage.kind()? {
2493 BootedStorageKind::Ostree(..) => {
2494 crate::image::push_entrypoint(
2495 &storage,
2496 &host,
2497 source.as_deref(),
2498 target.as_deref(),
2499 )
2500 .await
2501 }
2502 BootedStorageKind::Composefs(booted) => {
2503 bootc_composefs::export::export_repo_to_image(
2504 &storage,
2505 &booted,
2506 source.as_deref(),
2507 target.as_deref(),
2508 )
2509 .await
2510 }
2511 }
2512 }
2513 ImageOpts::SetUnified => crate::image::set_unified_entrypoint().await,
2514 ImageOpts::PullFromDefaultStorage { image } => {
2515 let storage = get_storage().await?;
2516 storage
2517 .get_ensure_imgstore()?
2518 .pull_from_host_storage(&image)
2519 .await
2520 }
2521 ImageOpts::Cmd(opt) => {
2522 let storage = get_storage().await?;
2523 let imgstore = storage.get_ensure_imgstore()?;
2524 match opt {
2525 ImageCmdOpts::List { args } => {
2526 crate::image::imgcmd_entrypoint(imgstore, "list", &args).await
2527 }
2528 ImageCmdOpts::Build { args } => {
2529 crate::image::imgcmd_entrypoint(imgstore, "build", &args).await
2530 }
2531 ImageCmdOpts::Pull { images } => {
2532 for image in &images {
2533 imgstore.pull_with_progress(image).await?;
2534 }
2535 Ok(())
2536 }
2537 ImageCmdOpts::Push { args } => {
2538 crate::image::imgcmd_entrypoint(imgstore, "push", &args).await
2539 }
2540 }
2541 }
2542 },
2543 Opt::Install(opts) => match opts {
2544 InstallOpts::Mount(opts) => crate::mount::mount(opts).await,
2545 #[cfg(feature = "install-to-disk")]
2546 InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await,
2547 InstallOpts::ToFilesystem(opts) => {
2548 crate::install::install_to_filesystem(opts, false, crate::install::Cleanup::Skip)
2549 .await
2550 }
2551 InstallOpts::ToExistingRoot(opts) => {
2552 crate::install::install_to_existing_root(opts).await
2553 }
2554 InstallOpts::Reset(opts) => crate::install::install_reset(opts).await,
2555 InstallOpts::PrintConfiguration(opts) => crate::install::print_configuration(opts),
2556 InstallOpts::EnsureCompletion {} => {
2557 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2558 crate::install::completion::run_from_anaconda(rootfs).await
2559 }
2560 InstallOpts::Finalize { root_path } => {
2561 crate::install::install_finalize(&root_path).await
2562 }
2563 },
2564 Opt::LoaderEntries(opts) => match opts {
2565 LoaderEntriesOpts::SetOptionsForSource(opts) => {
2566 let storage = get_storage().await?;
2567 let sysroot = storage.get_ostree()?;
2568 crate::loader_entries::set_options_for_source_staged(
2569 sysroot,
2570 &opts.source,
2571 opts.options.as_deref(),
2572 )?;
2573 Ok(())
2574 }
2575 },
2576 Opt::ExecInHostMountNamespace { args } => {
2577 crate::install::exec_in_host_mountns(args.as_slice())
2578 }
2579 Opt::Status(opts) => super::status::status(opts).await,
2580 Opt::Internals(opts) => match opts {
2581 InternalsOpts::ComposefsAbootReconcile => {
2582 let storage = get_storage().await?;
2583 let digest = match storage.kind()? {
2584 BootedStorageKind::Composefs(booted) => booted.cmdline.digest.to_string(),
2585 BootedStorageKind::Ostree(_) => {
2586 anyhow::bail!("Aboot reconciliation requires the composefs backend")
2587 }
2588 };
2589 ensure!(
2590 crate::bootc_composefs::state::read_boot_type(&storage.physical_root, &digest)?
2591 == Some(crate::bootc_composefs::boot::BootType::Aboot),
2592 "Aboot reconciliation requires an aboot deployment"
2593 );
2594 let mut state = aboot::AbootState::open(&storage.physical_root)?;
2595 if state.reconcile(&digest)? {
2596 start_finalize_stated_svc()?;
2597 }
2598 Ok(())
2599 }
2600 InternalsOpts::SystemdGenerator {
2601 normal_dir,
2602 early_dir: _,
2603 late_dir: _,
2604 } => {
2605 let unit_dir = &Dir::open_ambient_dir(normal_dir, cap_std::ambient_authority())?;
2606 crate::generator::generator(root, unit_dir)
2607 }
2608 InternalsOpts::OstreeExt { args } => {
2609 ostree_ext::cli::run_from_iter(["ostree-ext".into()].into_iter().chain(args)).await
2610 }
2611 InternalsOpts::OstreeContainer { args } => {
2612 ostree_ext::cli::run_from_iter(
2613 ["ostree-ext".into(), "container".into()]
2614 .into_iter()
2615 .chain(args),
2616 )
2617 .await
2618 }
2619 InternalsOpts::TestComposefs => {
2620 let storage = get_storage().await?;
2622 let cfs = storage.get_ensure_composefs()?;
2623 let testdata = b"some test data";
2624 let testdata_digest = hex::encode(openssl::sha::sha256(testdata));
2625 let mut w = cfs.create_stream(0)?;
2626 w.write_inline(testdata);
2627 let object = cfs
2628 .write_stream(w, &testdata_digest, Some("testobject"))?
2629 .to_hex();
2630 assert_eq!(
2631 object,
2632 "84245c6936db9939dda9c1fbeafdcbd2b49f7605354c88d4f016c4d941551f45bad0fbcdbee12ba8adfe4fb63541de57ac02729edbacdb556325e342b89d340d"
2633 );
2634 Ok(())
2635 }
2636 InternalsOpts::Fsverity(args) => match args {
2638 FsverityOpts::Measure { path } => {
2639 let fd =
2640 std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
2641 let digest: fsverity::Sha256HashValue = fsverity::measure_verity(&fd)?;
2642 let digest = digest.to_hex();
2643 println!("{digest}");
2644 Ok(())
2645 }
2646 FsverityOpts::Enable { path } => {
2647 let fd =
2648 std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
2649 fsverity::enable_verity_raw::<fsverity::Sha256HashValue>(&fd)?;
2650 Ok(())
2651 }
2652 },
2653 #[cfg(feature = "selinux")]
2654 InternalsOpts::Selinux(SelinuxOpts::IsUnlabeled { path }) => {
2655 ensure!(crate::lsm::selinux_enabled(), "SELinux is not enabled");
2656 let path = path
2657 .strip_prefix("/")
2658 .expect("absolute paths have a root prefix");
2659 match is_unlabeled_exit_status(
2660 crate::lsm::has_security_selinux(&root, path)?,
2661 path,
2662 )? {
2663 CliExitStatus::Success => Ok(()),
2664 CliExitStatus::PredicateFalse => {
2665 return Ok(CliExitStatus::PredicateFalse);
2666 }
2667 }
2668 }
2669 InternalsOpts::Cfs { args } => composefs_ctl::run_from_iter(args.iter()).await,
2670 InternalsOpts::Reboot => crate::reboot::reboot(),
2671 InternalsOpts::Fsck => {
2672 let storage = &get_storage().await?;
2673 crate::fsck::fsck(&storage, std::io::stdout().lock()).await?;
2674 Ok(())
2675 }
2676 InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root),
2677 InternalsOpts::SysusersSync => crate::sysusers_cleanup::run(&root),
2678 InternalsOpts::PrintJsonSchema { of } => {
2679 let schema = match of {
2680 SchemaType::Host => schema_for!(crate::spec::Host),
2681 SchemaType::Progress => schema_for!(crate::progress_jsonl::Event),
2682 };
2683 let mut stdout = std::io::stdout().lock();
2684 serde_json::to_writer_pretty(&mut stdout, &schema)?;
2685 Ok(())
2686 }
2687 InternalsOpts::Cleanup => {
2688 let storage = get_storage().await?;
2689 crate::deploy::cleanup(&storage).await
2690 }
2691 InternalsOpts::Relabel { as_path, path } => {
2692 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2693 let path = path.strip_prefix("/")?;
2694 let sepolicy =
2695 &ostree::SePolicy::new(&gio::File::for_path("/"), gio::Cancellable::NONE)?;
2696 crate::lsm::relabel_recurse(root, path, as_path.as_deref(), sepolicy)?;
2697 Ok(())
2698 }
2699 InternalsOpts::RelabelOverlayMountpoints => {
2700 crate::generator::relabel_overlay_mountpoints()
2701 }
2702 InternalsOpts::BootcInstallCompletion { sysroot, stateroot } => {
2703 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2704 crate::install::completion::run_from_ostree(rootfs, &sysroot, &stateroot).await
2705 }
2706 InternalsOpts::LoopbackCleanupHelper { device } => {
2707 crate::blockdev::run_loopback_cleanup_helper(&device).await
2708 }
2709 InternalsOpts::AllocateCleanupLoopback { file_path: _ } => {
2710 let temp_file =
2712 tempfile::NamedTempFile::new().context("Failed to create temporary file")?;
2713 let temp_path = temp_file.path();
2714
2715 let loopback = crate::blockdev::LoopbackDevice::new(temp_path)
2717 .context("Failed to create loopback device")?;
2718
2719 println!("Created loopback device: {}", loopback.path());
2720
2721 loopback
2723 .close()
2724 .context("Failed to close loopback device")?;
2725
2726 println!("Successfully closed loopback device");
2727 Ok(())
2728 }
2729 #[cfg(feature = "rhsm")]
2730 InternalsOpts::PublishRhsmFacts => crate::rhsm::publish_facts(&root).await,
2731 #[cfg(feature = "docgen")]
2732 InternalsOpts::DumpCliJson => {
2733 use clap::CommandFactory;
2734 let cmd = Opt::command();
2735 let json = crate::cli_json::dump_cli_json(&cmd)?;
2736 println!("{}", json);
2737 Ok(())
2738 }
2739 InternalsOpts::DirDiff {
2740 pristine_etc,
2741 current_etc,
2742 new_etc,
2743 merge,
2744 } => {
2745 let pristine_etc =
2746 Dir::open_ambient_dir(pristine_etc, cap_std::ambient_authority())?;
2747 let current_etc = Dir::open_ambient_dir(current_etc, cap_std::ambient_authority())?;
2748 let new_etc = Dir::open_ambient_dir(new_etc, cap_std::ambient_authority())?;
2749
2750 let (p, c, n) =
2751 etc_merge::traverse_etc(&pristine_etc, ¤t_etc, Some(&new_etc))?;
2752
2753 let n = n
2754 .as_ref()
2755 .ok_or_else(|| anyhow::anyhow!("Failed to get new directory tree"))?;
2756
2757 let diff = compute_diff(&p, &c, &n)?;
2758 print_diff(&diff, &mut std::io::stdout());
2759
2760 if merge {
2761 etc_merge::merge(¤t_etc, &c, &new_etc, &n, &diff)?;
2762 }
2763
2764 Ok(())
2765 }
2766 InternalsOpts::PrepSoftReboot {
2767 deployment,
2768 reboot,
2769 reset,
2770 } => {
2771 let storage = &get_storage().await?;
2772
2773 match storage.kind()? {
2774 BootedStorageKind::Ostree(..) => {
2775 anyhow::bail!("soft-reboot only implemented for composefs")
2777 }
2778
2779 BootedStorageKind::Composefs(booted_cfs) => {
2780 if reset {
2781 return reset_soft_reboot().map(|()| CliExitStatus::Success);
2782 }
2783
2784 prepare_soft_reboot_composefs(
2785 &storage,
2786 &booted_cfs,
2787 deployment.as_deref(),
2788 SoftRebootMode::Required,
2789 reboot,
2790 )
2791 .await
2792 }
2793 }
2794 }
2795 InternalsOpts::ComposefsGC {
2796 dry_run,
2797 assert_no_op,
2798 prune_repo,
2799 } => {
2800 let storage = &get_storage().await?;
2801
2802 match storage.kind()? {
2803 BootedStorageKind::Ostree(..) => {
2804 anyhow::bail!("composefs-gc only works for composefs backend");
2805 }
2806
2807 BootedStorageKind::Composefs(booted_cfs) => {
2808 let dry_run = dry_run || assert_no_op;
2809 let gc_result = composefs_gc(
2810 storage,
2811 &booted_cfs,
2812 GCOpts {
2813 dry_run,
2814 prune_repo,
2815 },
2816 )
2817 .await?;
2818
2819 if dry_run {
2820 println!("Dry run (no files deleted)");
2821 }
2822
2823 println!(
2824 "Objects: {} removed ({} bytes)",
2825 gc_result.objects_removed, gc_result.objects_bytes
2826 );
2827
2828 if gc_result.images_pruned > 0 || gc_result.streams_pruned > 0 {
2829 println!(
2830 "Pruned symlinks: {} images, {} streams",
2831 gc_result.images_pruned, gc_result.streams_pruned
2832 );
2833 }
2834
2835 if assert_no_op {
2836 let is_noop = gc_result.objects_removed == 0
2837 && gc_result.images_pruned == 0
2838 && gc_result.streams_pruned == 0;
2839 if !is_noop {
2840 anyhow::bail!(
2841 "--assert-no-op: GC would remove {} object(s), {} image symlink(s), {} stream symlink(s) (issue #1808)",
2842 gc_result.objects_removed,
2843 gc_result.images_pruned,
2844 gc_result.streams_pruned,
2845 );
2846 }
2847 }
2848
2849 Ok(())
2850 }
2851 }
2852 }
2853 InternalsOpts::Blockdev(opts) => {
2854 let dev = match opts {
2855 BlockdevOpts::Ls { device } => crate::blockdev::list_dev(&device)?,
2856 BlockdevOpts::LsFilesystem { path } => {
2857 let dir = Dir::open_ambient_dir(&path, cap_std::ambient_authority())?;
2858 crate::blockdev::list_dev_by_dir(&dir)?
2859 }
2860 };
2861 serde_json::to_writer_pretty(std::io::stdout().lock(), &dev)?;
2862 println!();
2863 Ok(())
2864 }
2865 InternalsOpts::Uki(uki_opts) => match uki_opts {
2866 UkiSubcommands::Extract { path, output_path } => {
2867 let mut uki_file =
2868 std::fs::File::open(&path).with_context(|| format!("Opening {path}"))?;
2869
2870 let uname =
2871 composefs_boot::uki::get_text_section_buffered(&mut uki_file, ".uname")
2872 .context("Getting uname")?;
2873
2874 std::fs::create_dir_all(&output_path).context("Creating output directory")?;
2875
2876 let output_dir = Dir::open_ambient_dir(&output_path, ambient_authority())
2877 .context("Opening output dir")?;
2878 output_dir.create_dir(&uname)?;
2879
2880 let output_dir = output_dir.open_dir(&uname)?;
2881
2882 for (section_name, file_name) in
2883 [(".linux", "vmlinuz"), (".initrd", "initramfs.img")]
2884 {
2885 uki_file
2886 .seek(SeekFrom::Start(0))
2887 .context("Seeking to start")?;
2888 let section =
2889 composefs_boot::uki::get_section_buffered(&mut uki_file, section_name)
2890 .with_context(|| format!("Getting {section_name} section"))?;
2891 output_dir
2892 .write(file_name, section)
2893 .with_context(|| format!("Writing {file_name}"))?;
2894 }
2895
2896 Ok(())
2897 }
2898 },
2899 },
2900 Opt::State(opts) => match opts {
2901 StateOpts::WipeOstree => {
2902 let sysroot = ostree::Sysroot::new_default();
2903 sysroot.load(gio::Cancellable::NONE)?;
2904 crate::deploy::wipe_ostree(sysroot).await?;
2905 Ok(())
2906 }
2907 },
2908
2909 Opt::ComposefsFinalizeStaged(opts) => {
2910 if opts.hold {
2911 crate::bootc_composefs::finalize::hold_boot()
2915 } else {
2916 let storage = &get_storage().await?;
2917 match storage.kind()? {
2918 BootedStorageKind::Ostree(_) => {
2919 anyhow::bail!(
2920 "ComposefsFinalizeStaged is only supported for composefs backend"
2921 )
2922 }
2923 BootedStorageKind::Composefs(booted_cfs) => {
2924 composefs_backend_finalize(storage, &booted_cfs).await
2925 }
2926 }
2927 }
2928 }
2929
2930 Opt::ConfigDiff => {
2931 let storage = &get_storage().await?;
2932 match storage.kind()? {
2933 BootedStorageKind::Ostree(_) => {
2934 anyhow::bail!("ConfigDiff is only supported for composefs backend")
2935 }
2936 BootedStorageKind::Composefs(booted_cfs) => {
2937 let diff = get_etc_diff(storage, &booted_cfs, None).await?;
2938 print_diff(&diff, &mut std::io::stdout());
2939 Ok(())
2940 }
2941 }
2942 }
2943
2944 Opt::DeleteDeployment { depl_id } => {
2945 let storage = &get_storage().await?;
2946 match storage.kind()? {
2947 BootedStorageKind::Ostree(_) => {
2948 anyhow::bail!("DeleteDeployment is only supported for composefs backend")
2949 }
2950 BootedStorageKind::Composefs(booted_cfs) => {
2951 delete_composefs_deployment(&depl_id, storage, &booted_cfs).await
2952 }
2953 }
2954 }
2955 Opt::UkiAddon(opts) => {
2956 let storage = &get_storage().await?;
2957 match storage.kind()? {
2958 BootedStorageKind::Ostree(_) => {
2959 anyhow::bail!("UKI Addons are only supported for Composefs Backend")
2960 }
2961 BootedStorageKind::Composefs(booted_cfs) => {
2962 let status = get_composefs_status(storage, &booted_cfs).await?;
2963
2964 if status.require_composefs_booted()?.boot_type != BootType::Uki {
2965 anyhow::bail!(
2966 "Cannot perform UKI Addon operations on systems not booted via UKI"
2967 );
2968 }
2969
2970 handle_addon_cli_cmd(storage, &booted_cfs, &status, &opts)
2971 }
2972 }
2973 }
2974 };
2975 result.map(|()| CliExitStatus::Success)
2976}
2977
2978#[cfg(test)]
2979mod tests {
2980 use super::*;
2981
2982 #[test]
2983 fn test_callname() {
2984 use std::os::unix::ffi::OsStrExt;
2985
2986 let mapped_cases = [
2988 ("", "bootc"),
2989 ("/foo/bar", "bar"),
2990 ("/foo/bar/", "bar"),
2991 ("foo/bar", "bar"),
2992 ("../foo/bar", "bar"),
2993 ("usr/bin/ostree-container", "ostree-container"),
2994 ];
2995 for (input, output) in mapped_cases {
2996 assert_eq!(
2997 output,
2998 callname_from_argv0(OsStr::new(input)),
2999 "Handling mapped case {input}"
3000 );
3001 }
3002
3003 assert_eq!("bootc", callname_from_argv0(OsStr::from_bytes(b"foo\x80")));
3005
3006 let ident_cases = ["foo", "bootc"];
3008 for case in ident_cases {
3009 assert_eq!(
3010 case,
3011 callname_from_argv0(OsStr::new(case)),
3012 "Handling ident case {case}"
3013 );
3014 }
3015 }
3016
3017 #[test]
3018 fn test_parse_from_delta() {
3019 let o =
3020 Opt::try_parse_from(["bootc", "switch", "--from-delta", "/d", "quay.io/e/x"]).unwrap();
3021 match o {
3022 Opt::Switch(o) => assert_eq!(o.from_delta.as_deref().unwrap(), "/d"),
3023 o => panic!("Expected switch opts, not {o:?}"),
3024 }
3025
3026 let conflicting: &[&[&str]] = &[
3030 &["switch", "--transport=oci-archive", "quay.io/e/x"],
3031 &["switch", "--mutate-in-place", "quay.io/e/x"],
3032 &["switch", "--experimental-unified-storage", "quay.io/e/x"],
3033 &["switch", "--enforce-container-sigpolicy", "quay.io/e/x"],
3034 &["upgrade", "--check"],
3035 &["upgrade", "--tag=other"],
3036 ];
3037 for args in conflicting {
3038 let (subcommand, rest) = args.split_first().unwrap();
3039 let mut full = vec!["bootc", subcommand, "--from-delta", "/d"];
3040 full.extend_from_slice(rest);
3041 let err = Opt::try_parse_from(&full)
3042 .err()
3043 .unwrap_or_else(|| panic!("{args:?}: expected a conflict"));
3044 assert_eq!(
3045 err.kind(),
3046 clap::error::ErrorKind::ArgumentConflict,
3047 "{args:?}: {err}"
3048 );
3049 }
3050 }
3051
3052 #[test]
3053 fn test_parse_install_args() {
3054 let o = Opt::try_parse_from([
3056 "bootc",
3057 "install",
3058 "to-filesystem",
3059 "--target-no-signature-verification",
3060 "/target",
3061 ])
3062 .unwrap();
3063 let o = match o {
3064 Opt::Install(InstallOpts::ToFilesystem(fsopts)) => fsopts,
3065 o => panic!("Expected filesystem opts, not {o:?}"),
3066 };
3067 assert!(o.target_opts.target_no_signature_verification);
3068 assert_eq!(o.filesystem_opts.root_path.as_str(), "/target");
3069 assert_eq!(
3071 o.config_opts.bound_images,
3072 crate::install::BoundImagesOpt::Stored
3073 );
3074 }
3075
3076 #[test]
3077 fn test_parse_boot_artifact_erofs_version_args() {
3078 for command in ["ukify", "aboot"] {
3079 for (arg, expected) in [
3080 (None, None),
3081 (Some("v1"), Some(BootArtifactErofsVersionArg::V1)),
3082 (Some("v1-only"), Some(BootArtifactErofsVersionArg::V1Only)),
3083 (Some("v2"), Some(BootArtifactErofsVersionArg::V2)),
3084 ] {
3085 let mut args = vec!["bootc", "container", command];
3086 if let Some(arg) = arg {
3087 args.extend(["--erofs-version", arg]);
3088 }
3089 let opt = Opt::try_parse_from(args).unwrap();
3090 let erofs_version = match opt {
3091 Opt::Container(ContainerOpts::Ukify { erofs_version, .. })
3092 | Opt::Container(ContainerOpts::Aboot { erofs_version, .. }) => erofs_version,
3093 _ => panic!("expected container boot artifact options"),
3094 };
3095 assert_eq!(erofs_version, expected, "{command} {arg:?}");
3096 }
3097 }
3098 }
3099
3100 #[test]
3101 fn test_parse_opts() {
3102 assert!(matches!(
3103 Opt::parse_including_static(["bootc", "status"]),
3104 Opt::Status(StatusOpts {
3105 json: false,
3106 format: None,
3107 format_version: None,
3108 booted: false,
3109 verbose: false
3110 })
3111 ));
3112 assert!(matches!(
3113 Opt::parse_including_static(["bootc", "status", "--format-version=0"]),
3114 Opt::Status(StatusOpts {
3115 format_version: Some(0),
3116 ..
3117 })
3118 ));
3119
3120 assert!(matches!(
3122 Opt::parse_including_static(["bootc", "status", "--verbose"]),
3123 Opt::Status(StatusOpts { verbose: true, .. })
3124 ));
3125
3126 assert!(matches!(
3128 Opt::parse_including_static(["bootc", "status", "-v"]),
3129 Opt::Status(StatusOpts { verbose: true, .. })
3130 ));
3131
3132 for (args, hold) in [
3133 (&["bootc", "composefs-finalize-staged"][..], false),
3134 (&["bootc", "composefs-finalize-staged", "--hold"][..], true),
3135 ] {
3136 assert_eq!(
3137 Opt::parse_including_static(args),
3138 Opt::ComposefsFinalizeStaged(ComposefsFinalizeStagedOpts { hold }),
3139 "{args:?}"
3140 );
3141 }
3142
3143 assert!(matches!(
3144 Opt::parse_including_static([
3145 "bootc",
3146 "container",
3147 "aboot",
3148 "--out",
3149 "/out",
3150 ]),
3151 Opt::Container(ContainerOpts::Aboot { out, .. })
3152 if out.as_deref() == Some(Utf8Path::new("/out"))
3153 ));
3154 }
3155
3156 #[test]
3157 #[cfg(feature = "selinux")]
3158 fn test_parse_selinux_is_unlabeled() {
3159 let opt = Opt::try_parse_from([
3160 "bootc",
3161 "internals",
3162 "selinux",
3163 "is-unlabeled",
3164 "/var/lib/probe",
3165 ])
3166 .unwrap();
3167 assert!(matches!(
3168 opt,
3169 Opt::Internals(InternalsOpts::Selinux(SelinuxOpts::IsUnlabeled { path }))
3170 if path == "/var/lib/probe"
3171 ));
3172 assert!(
3173 Opt::try_parse_from(["bootc", "internals", "selinux", "is-unlabeled", "relative",])
3174 .is_err()
3175 );
3176 }
3177
3178 #[test]
3179 #[cfg(feature = "selinux")]
3180 fn test_is_unlabeled_exit_status() {
3181 let path = Utf8Path::new("probe");
3182 let cases = [
3183 (
3184 crate::lsm::SELinuxLabelState::Unlabeled,
3185 CliExitStatus::Success,
3186 ),
3187 (
3188 crate::lsm::SELinuxLabelState::Labeled,
3189 CliExitStatus::PredicateFalse,
3190 ),
3191 ];
3192 for (state, expected) in cases {
3193 assert_eq!(is_unlabeled_exit_status(state, path).unwrap(), expected);
3194 }
3195 assert!(
3196 is_unlabeled_exit_status(crate::lsm::SELinuxLabelState::Unsupported, path)
3197 .unwrap_err()
3198 .to_string()
3199 .contains("unsupported")
3200 );
3201 }
3202
3203 #[test]
3204 fn test_parse_generator() {
3205 assert!(matches!(
3206 Opt::parse_including_static([
3207 "/usr/lib/systemd/system/bootc-systemd-generator",
3208 "/run/systemd/system"
3209 ]),
3210 Opt::Internals(InternalsOpts::SystemdGenerator { normal_dir, .. }) if normal_dir == "/run/systemd/system"
3211 ));
3212 }
3213
3214 #[test]
3215 fn test_parse_ostree_ext() {
3216 assert!(matches!(
3217 Opt::parse_including_static(["bootc", "internals", "ostree-container"]),
3218 Opt::Internals(InternalsOpts::OstreeContainer { .. })
3219 ));
3220
3221 fn peel(o: Opt) -> Vec<OsString> {
3222 match o {
3223 Opt::Internals(InternalsOpts::OstreeExt { args }) => args,
3224 o => panic!("unexpected {o:?}"),
3225 }
3226 }
3227 let args = peel(Opt::parse_including_static([
3228 "/usr/libexec/libostree/ext/ostree-ima-sign",
3229 "ima-sign",
3230 "--repo=foo",
3231 "foo",
3232 "bar",
3233 "baz",
3234 ]));
3235 assert_eq!(
3236 args.as_slice(),
3237 ["ima-sign", "--repo=foo", "foo", "bar", "baz"]
3238 );
3239
3240 let args = peel(Opt::parse_including_static([
3241 "/usr/libexec/libostree/ext/ostree-container",
3242 "container",
3243 "image",
3244 "pull",
3245 ]));
3246 assert_eq!(args.as_slice(), ["container", "image", "pull"]);
3247 }
3248
3249 #[test]
3250 fn test_parse_upgrade_options() {
3251 let o = Opt::try_parse_from(["bootc", "upgrade", "--tag", "v1.1"]).unwrap();
3253 match o {
3254 Opt::Upgrade(opts) => {
3255 assert_eq!(opts.tag, Some("v1.1".to_string()));
3256 }
3257 _ => panic!("Expected Upgrade variant"),
3258 }
3259
3260 let o = Opt::try_parse_from(["bootc", "upgrade", "--tag", "v1.1", "--check"]).unwrap();
3262 match o {
3263 Opt::Upgrade(opts) => {
3264 assert_eq!(opts.tag, Some("v1.1".to_string()));
3265 assert!(opts.check);
3266 }
3267 _ => panic!("Expected Upgrade variant"),
3268 }
3269 }
3270
3271 #[test]
3272 fn test_image_reference_with_tag() {
3273 let current = ImageReference {
3275 image: "quay.io/example/myapp:v1.0".to_string(),
3276 transport: "registry".to_string(),
3277 signature: None,
3278 };
3279 let result = current.with_tag("v1.1").unwrap();
3280 assert_eq!(result.image, "quay.io/example/myapp:v1.1");
3281 assert_eq!(result.transport, "registry");
3282
3283 let current_with_digest = ImageReference {
3285 image: "quay.io/example/myapp:v1.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(),
3286 transport: "registry".to_string(),
3287 signature: None,
3288 };
3289 let result = current_with_digest.with_tag("v2.0").unwrap();
3290 assert_eq!(result.image, "quay.io/example/myapp:v2.0");
3291
3292 let containers_storage = ImageReference {
3294 image: "localhost/myapp:v1.0".to_string(),
3295 transport: "containers-storage".to_string(),
3296 signature: None,
3297 };
3298 let result = containers_storage.with_tag("v1.1").unwrap();
3299 assert_eq!(result.image, "localhost/myapp:v1.1");
3300 assert_eq!(result.transport, "containers-storage");
3301
3302 let containers_storage_with_digest = ImageReference {
3304 image:
3305 "localhost/myapp:v1.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
3306 .to_string(),
3307 transport: "containers-storage".to_string(),
3308 signature: None,
3309 };
3310 let result = containers_storage_with_digest.with_tag("v2.0").unwrap();
3311 assert_eq!(result.image, "localhost/myapp:v2.0");
3312 assert_eq!(result.transport, "containers-storage");
3313
3314 let no_tag = ImageReference {
3316 image: "localhost/myapp".to_string(),
3317 transport: "containers-storage".to_string(),
3318 signature: None,
3319 };
3320 let result = no_tag.with_tag("v1.0").unwrap();
3321 assert_eq!(result.image, "localhost/myapp:v1.0");
3322 assert_eq!(result.transport, "containers-storage");
3323 }
3324
3325 #[test]
3326 fn test_generate_completion_scripts_contain_commands() {
3327 use clap_complete::aot::{Shell, generate};
3328
3329 let want = ["install", "upgrade"];
3338
3339 for shell in [Shell::Bash, Shell::Zsh, Shell::Fish] {
3340 let mut cmd = Opt::command();
3341 let mut buf = Vec::new();
3342 generate(shell, &mut cmd, "bootc", &mut buf);
3343 let s = String::from_utf8(buf).expect("completion should be utf8");
3344 for w in &want {
3345 assert!(s.contains(w), "{shell:?} completion missing {w}");
3346 }
3347 }
3348 }
3349
3350 #[test]
3351 fn test_host_feature_gates() {
3352 let full = Opt::command();
3354 let sub = full
3355 .find_subcommand(LOADER_ENTRIES_SUBCOMMAND)
3356 .expect("loader-entries subcommand");
3357 assert!(!sub.is_hide_set());
3358
3359 let invocation = [
3360 "bootc",
3361 LOADER_ENTRIES_SUBCOMMAND,
3362 "set-options-for-source",
3363 "--source",
3364 "tuned",
3365 ];
3366
3367 for (supported, expect_available) in [(true, true), (false, false)] {
3369 let cmd = apply_host_feature_gates(Opt::command(), supported);
3370 let visible: Vec<_> = cmd
3371 .get_subcommands()
3372 .filter(|c| !c.is_hide_set())
3373 .map(clap::Command::get_name)
3374 .collect();
3375 assert_eq!(
3376 visible.contains(&LOADER_ENTRIES_SUBCOMMAND),
3377 expect_available,
3378 "bootconfig_extra={supported}: {visible:?}"
3379 );
3380
3381 let argv = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
3385 for gated in [
3386 argv(&["bootc", LOADER_ENTRIES_SUBCOMMAND, "--help"]),
3387 argv(&[
3388 "bootc",
3389 LOADER_ENTRIES_SUBCOMMAND,
3390 "set-options-for-source",
3391 "--help",
3392 ]),
3393 argv(&invocation),
3394 ] {
3395 assert_eq!(
3396 host_feature_gate_rejects(&gated, supported),
3397 !expect_available,
3398 "bootconfig_extra={supported}: {gated:?}"
3399 );
3400 }
3401 for other in [
3402 argv(&["bootc", "status"]),
3403 argv(&["bootc", "--version"]),
3404 argv(&["bootc"]),
3405 ] {
3406 assert!(!host_feature_gate_rejects(&other, supported), "{other:?}");
3407 }
3408
3409 if expect_available {
3411 let opt = <Opt as clap::FromArgMatches>::from_arg_matches(
3412 &cmd.clone().try_get_matches_from(invocation).unwrap(),
3413 )
3414 .unwrap();
3415 assert!(matches!(opt, Opt::LoaderEntries(_)));
3416 }
3417 }
3418 }
3419}