1use std::ffi::{CString, OsStr, OsString};
6use std::fs::File;
7use std::io::{BufWriter, Seek};
8use std::os::unix::process::CommandExt;
9use std::process::Command;
10
11use anyhow::{anyhow, ensure, Context, Result};
12use camino::{Utf8Path, Utf8PathBuf};
13use cap_std_ext::cap_std;
14use cap_std_ext::cap_std::fs::Dir;
15use clap::CommandFactory;
16use clap::Parser;
17use clap::ValueEnum;
18use composefs::dumpfile;
19use composefs_boot::BootOps as _;
20use etc_merge::{compute_diff, print_diff};
21use fn_error_context::context;
22use indoc::indoc;
23use ostree::gio;
24use ostree_container::store::PrepareResult;
25use ostree_ext::composefs::fsverity;
26use ostree_ext::composefs::fsverity::FsVerityHashValue;
27use ostree_ext::composefs::splitstream::SplitStreamWriter;
28use ostree_ext::container as ostree_container;
29use ostree_ext::containers_image_proxy::ImageProxyConfig;
30use ostree_ext::keyfileext::KeyFileExt;
31use ostree_ext::ostree;
32use ostree_ext::sysroot::SysrootLock;
33use schemars::schema_for;
34use serde::{Deserialize, Serialize};
35
36use crate::bootc_composefs::delete::delete_composefs_deployment;
37use crate::bootc_composefs::soft_reboot::prepare_soft_reboot_composefs;
38use crate::bootc_composefs::{
39 digest::{compute_composefs_digest, new_temp_composefs_repo},
40 finalize::{composefs_backend_finalize, get_etc_diff},
41 rollback::composefs_rollback,
42 state::composefs_usr_overlay,
43 switch::switch_composefs,
44 update::upgrade_composefs,
45};
46use crate::deploy::{MergeState, RequiredHostSpec};
47use crate::podstorage::set_additional_image_store;
48use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
49use crate::spec::Host;
50use crate::spec::ImageReference;
51use crate::status::get_host;
52use crate::store::{BootedOstree, Storage};
53use crate::store::{BootedStorage, BootedStorageKind};
54use crate::utils::sigpolicy_from_opt;
55use crate::{bootc_composefs, lints};
56
57#[derive(Debug, Parser, PartialEq, Eq)]
59pub(crate) struct ProgressOptions {
60 #[clap(long, hide = true)]
64 pub(crate) progress_fd: Option<RawProgressFd>,
65}
66
67impl TryFrom<ProgressOptions> for ProgressWriter {
68 type Error = anyhow::Error;
69
70 fn try_from(value: ProgressOptions) -> Result<Self> {
71 let r = value
72 .progress_fd
73 .map(TryInto::try_into)
74 .transpose()?
75 .unwrap_or_default();
76 Ok(r)
77 }
78}
79
80#[derive(Debug, Parser, PartialEq, Eq)]
82pub(crate) struct UpgradeOpts {
83 #[clap(long)]
85 pub(crate) quiet: bool,
86
87 #[clap(long, conflicts_with = "apply")]
91 pub(crate) check: bool,
92
93 #[clap(long, conflicts_with = "check")]
97 pub(crate) apply: bool,
98
99 #[clap(long = "soft-reboot", conflicts_with = "check")]
103 pub(crate) soft_reboot: Option<SoftRebootMode>,
104
105 #[clap(long, conflicts_with_all = ["check", "apply"])]
111 pub(crate) download_only: bool,
112
113 #[clap(long, conflicts_with_all = ["check", "download_only"])]
119 pub(crate) from_downloaded: bool,
120
121 #[clap(flatten)]
122 pub(crate) progress: ProgressOptions,
123}
124
125#[derive(Debug, Parser, PartialEq, Eq)]
127pub(crate) struct SwitchOpts {
128 #[clap(long)]
130 pub(crate) quiet: bool,
131
132 #[clap(long)]
136 pub(crate) apply: bool,
137
138 #[clap(long = "soft-reboot")]
142 pub(crate) soft_reboot: Option<SoftRebootMode>,
143
144 #[clap(long, default_value = "registry")]
146 pub(crate) transport: String,
147
148 #[clap(long, hide = true)]
150 pub(crate) no_signature_verification: bool,
151
152 #[clap(long)]
158 pub(crate) enforce_container_sigpolicy: bool,
159
160 #[clap(long, hide = true)]
164 pub(crate) mutate_in_place: bool,
165
166 #[clap(long)]
168 pub(crate) retain: bool,
169
170 #[clap(long = "experimental-unified-storage", hide = true)]
176 pub(crate) unified_storage_exp: bool,
177
178 pub(crate) target: String,
180
181 #[clap(flatten)]
182 pub(crate) progress: ProgressOptions,
183}
184
185#[derive(Debug, Parser, PartialEq, Eq)]
187pub(crate) struct RollbackOpts {
188 #[clap(long)]
194 pub(crate) apply: bool,
195
196 #[clap(long = "soft-reboot")]
200 pub(crate) soft_reboot: Option<SoftRebootMode>,
201}
202
203#[derive(Debug, Parser, PartialEq, Eq)]
205pub(crate) struct EditOpts {
206 #[clap(long, short = 'f')]
208 pub(crate) filename: Option<String>,
209
210 #[clap(long)]
212 pub(crate) quiet: bool,
213}
214
215#[derive(Debug, Clone, ValueEnum, PartialEq, Eq)]
216#[clap(rename_all = "lowercase")]
217pub(crate) enum OutputFormat {
218 HumanReadable,
220 Yaml,
222 Json,
224}
225
226#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
227#[clap(rename_all = "lowercase")]
228pub(crate) enum SoftRebootMode {
229 Required,
231 Auto,
233}
234
235#[derive(Debug, Parser, PartialEq, Eq)]
237pub(crate) struct StatusOpts {
238 #[clap(long, hide = true)]
242 pub(crate) json: bool,
243
244 #[clap(long)]
246 pub(crate) format: Option<OutputFormat>,
247
248 #[clap(long)]
253 pub(crate) format_version: Option<u32>,
254
255 #[clap(long)]
257 pub(crate) booted: bool,
258
259 #[clap(long, short = 'v')]
261 pub(crate) verbose: bool,
262}
263
264#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
265pub(crate) enum InstallOpts {
266 #[cfg(feature = "install-to-disk")]
277 ToDisk(crate::install::InstallToDiskOpts),
278 ToFilesystem(crate::install::InstallToFilesystemOpts),
285 ToExistingRoot(crate::install::InstallToExistingRootOpts),
292 #[clap(hide = true)]
297 Reset(crate::install::InstallResetOpts),
298 Finalize {
301 root_path: Utf8PathBuf,
303 },
304 EnsureCompletion {},
312 PrintConfiguration(crate::install::InstallPrintConfigurationOpts),
319}
320
321#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
323pub(crate) enum ContainerOpts {
324 Inspect {
329 #[clap(long, default_value = "/")]
331 rootfs: Utf8PathBuf,
332
333 #[clap(long)]
335 json: bool,
336
337 #[clap(long, conflicts_with = "json")]
339 format: Option<OutputFormat>,
340 },
341 Lint {
347 #[clap(long, default_value = "/")]
349 rootfs: Utf8PathBuf,
350
351 #[clap(long)]
353 fatal_warnings: bool,
354
355 #[clap(long)]
360 list: bool,
361
362 #[clap(long)]
367 skip: Vec<String>,
368
369 #[clap(long)]
372 no_truncate: bool,
373 },
374 #[clap(hide = true)]
376 ComputeComposefsDigest {
377 #[clap(default_value = "/target")]
379 path: Utf8PathBuf,
380
381 #[clap(long)]
383 write_dumpfile_to: Option<Utf8PathBuf>,
384 },
385 #[clap(hide = true)]
387 ComputeComposefsDigestFromStorage {
388 #[clap(long)]
390 write_dumpfile_to: Option<Utf8PathBuf>,
391
392 image: Option<String>,
394 },
395}
396
397#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
399pub(crate) enum ImageCmdOpts {
400 List {
402 #[clap(allow_hyphen_values = true)]
403 args: Vec<OsString>,
404 },
405 Build {
407 #[clap(allow_hyphen_values = true)]
408 args: Vec<OsString>,
409 },
410 Pull {
412 #[clap(allow_hyphen_values = true)]
413 args: Vec<OsString>,
414 },
415 Push {
417 #[clap(allow_hyphen_values = true)]
418 args: Vec<OsString>,
419 },
420}
421
422#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
423#[serde(rename_all = "kebab-case")]
424pub(crate) enum ImageListType {
425 #[default]
427 All,
428 Logical,
430 Host,
432}
433
434impl std::fmt::Display for ImageListType {
435 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436 self.to_possible_value().unwrap().get_name().fmt(f)
437 }
438}
439
440#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
441#[serde(rename_all = "kebab-case")]
442pub(crate) enum ImageListFormat {
443 #[default]
445 Table,
446 Json,
448}
449impl std::fmt::Display for ImageListFormat {
450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451 self.to_possible_value().unwrap().get_name().fmt(f)
452 }
453}
454
455#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
457pub(crate) enum ImageOpts {
458 List {
462 #[clap(long = "type")]
464 #[arg(default_value_t)]
465 list_type: ImageListType,
466 #[clap(long = "format")]
467 #[arg(default_value_t)]
468 list_format: ImageListFormat,
469 },
470 CopyToStorage {
487 #[clap(long)]
488 source: Option<String>,
490
491 #[clap(long)]
492 target: Option<String>,
495 },
496 SetUnified,
501 PullFromDefaultStorage {
503 image: String,
505 },
506 #[clap(subcommand)]
508 Cmd(ImageCmdOpts),
509}
510
511#[derive(Debug, Clone, clap::ValueEnum, PartialEq, Eq)]
512pub(crate) enum SchemaType {
513 Host,
514 Progress,
515}
516
517#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
519pub(crate) enum FsverityOpts {
520 Measure {
522 path: Utf8PathBuf,
524 },
525 Enable {
527 path: Utf8PathBuf,
529 },
530}
531
532#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
534pub(crate) enum InternalsOpts {
535 SystemdGenerator {
536 normal_dir: Utf8PathBuf,
537 #[allow(dead_code)]
538 early_dir: Option<Utf8PathBuf>,
539 #[allow(dead_code)]
540 late_dir: Option<Utf8PathBuf>,
541 },
542 FixupEtcFstab,
543 PrintJsonSchema {
545 #[clap(long)]
546 of: SchemaType,
547 },
548 #[clap(subcommand)]
549 Fsverity(FsverityOpts),
550 Fsck,
552 Cleanup,
554 Relabel {
555 #[clap(long)]
556 as_path: Option<Utf8PathBuf>,
558
559 path: Utf8PathBuf,
561 },
562 OstreeExt {
564 #[clap(allow_hyphen_values = true)]
565 args: Vec<OsString>,
566 },
567 Cfs {
569 #[clap(allow_hyphen_values = true)]
570 args: Vec<OsString>,
571 },
572 OstreeContainer {
574 #[clap(allow_hyphen_values = true)]
575 args: Vec<OsString>,
576 },
577 TestComposefs,
579 LoopbackCleanupHelper {
581 #[clap(long)]
583 device: String,
584 },
585 AllocateCleanupLoopback {
587 #[clap(long)]
589 file_path: Utf8PathBuf,
590 },
591 BootcInstallCompletion {
593 sysroot: Utf8PathBuf,
595
596 stateroot: String,
598 },
599 Reboot,
602 #[cfg(feature = "rhsm")]
603 PublishRhsmFacts,
605 DirDiff {
607 pristine_etc: Utf8PathBuf,
609 current_etc: Utf8PathBuf,
611 new_etc: Utf8PathBuf,
613 #[clap(long)]
615 merge: bool,
616 },
617 #[cfg(feature = "docgen")]
618 DumpCliJson,
620 PrepSoftReboot {
621 deployment: String,
622 #[clap(long)]
623 reboot: bool,
624 },
625}
626
627#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
628pub(crate) enum StateOpts {
629 WipeOstree,
631}
632
633impl InternalsOpts {
634 const GENERATOR_BIN: &'static str = "bootc-systemd-generator";
636}
637
638#[derive(Debug, Parser, PartialEq, Eq)]
646#[clap(name = "bootc")]
647#[clap(rename_all = "kebab-case")]
648#[clap(version,long_version=clap::crate_version!())]
649#[allow(clippy::large_enum_variant)]
650pub(crate) enum Opt {
651 #[clap(alias = "update")]
664 Upgrade(UpgradeOpts),
665 Switch(SwitchOpts),
676 #[command(after_help = indoc! {r#"
688 Note on Rollbacks and the `/etc` Directory:
689
690 When you perform a rollback (e.g., with `bootc rollback`), any
691 changes made to files in the `/etc` directory won't carry over
692 to the rolled-back deployment. The `/etc` files will revert
693 to their state from that previous deployment instead.
694
695 This is because `bootc rollback` just reorders the existing
696 deployments. It doesn't create new deployments. The `/etc`
697 merges happen when new deployments are created.
698 "#})]
699 Rollback(RollbackOpts),
700 Edit(EditOpts),
710 Status(StatusOpts),
714 #[clap(alias = "usroverlay")]
718 UsrOverlay,
719 #[clap(subcommand)]
723 Install(InstallOpts),
724 #[clap(subcommand)]
726 Container(ContainerOpts),
727 #[clap(subcommand, hide = true)]
731 Image(ImageOpts),
732 #[clap(hide = true)]
734 ExecInHostMountNamespace {
735 #[clap(trailing_var_arg = true, allow_hyphen_values = true)]
736 args: Vec<OsString>,
737 },
738 #[clap(hide = true)]
740 #[clap(subcommand)]
741 State(StateOpts),
742 #[clap(subcommand)]
743 #[clap(hide = true)]
744 Internals(InternalsOpts),
745 ComposefsFinalizeStaged,
746 #[clap(hide = true)]
748 ConfigDiff,
749 #[clap(hide = true)]
753 Completion {
754 #[clap(value_enum)]
756 shell: clap_complete::aot::Shell,
757 },
758 #[clap(hide = true)]
759 DeleteDeployment {
760 depl_id: String,
761 },
762}
763
764#[context("Ensuring mountns")]
769pub(crate) fn ensure_self_unshared_mount_namespace() -> Result<()> {
770 let uid = rustix::process::getuid();
771 if !uid.is_root() {
772 tracing::debug!("Not root, assuming no need to unshare");
773 return Ok(());
774 }
775 let recurse_env = "_ostree_unshared";
776 let ns_pid1 = std::fs::read_link("/proc/1/ns/mnt").context("Reading /proc/1/ns/mnt")?;
777 let ns_self = std::fs::read_link("/proc/self/ns/mnt").context("Reading /proc/self/ns/mnt")?;
778 if ns_pid1 != ns_self {
780 tracing::debug!("Already in a mount namespace");
781 return Ok(());
782 }
783 if std::env::var_os(recurse_env).is_some() {
784 let am_pid1 = rustix::process::getpid().is_init();
785 if am_pid1 {
786 tracing::debug!("We are pid 1");
787 return Ok(());
788 } else {
789 anyhow::bail!("Failed to unshare mount namespace");
790 }
791 }
792 bootc_utils::reexec::reexec_with_guardenv(recurse_env, &["unshare", "-m", "--"])
793}
794
795#[context("Initializing storage")]
798pub(crate) async fn get_storage() -> Result<crate::store::BootedStorage> {
799 let env = crate::store::Environment::detect()?;
800 prepare_for_write()?;
803 let r = BootedStorage::new(env)
804 .await?
805 .ok_or_else(|| anyhow!("System not booted via bootc"))?;
806 Ok(r)
807}
808
809#[context("Querying root privilege")]
810pub(crate) fn require_root(is_container: bool) -> Result<()> {
811 ensure!(
812 rustix::process::getuid().is_root(),
813 if is_container {
814 "The user inside the container from which you are running this command must be root"
815 } else {
816 "This command must be executed as the root user"
817 }
818 );
819
820 ensure!(
821 rustix::thread::capability_is_in_bounding_set(rustix::thread::CapabilitySet::SYS_ADMIN)?,
822 if is_container {
823 "The container must be executed with full privileges (e.g. --privileged flag)"
824 } else {
825 "This command requires full root privileges (CAP_SYS_ADMIN)"
826 }
827 );
828
829 tracing::trace!("Verified uid 0 with CAP_SYS_ADMIN");
830
831 Ok(())
832}
833
834fn has_soft_reboot_capability(deployment: Option<&crate::spec::BootEntry>) -> bool {
836 deployment.map(|d| d.soft_reboot_capable).unwrap_or(false)
837}
838
839#[context("Preparing soft reboot")]
841fn prepare_soft_reboot(sysroot: &SysrootLock, deployment: &ostree::Deployment) -> Result<()> {
842 let cancellable = ostree::gio::Cancellable::NONE;
843 sysroot
844 .deployment_set_soft_reboot(deployment, false, cancellable)
845 .context("Failed to prepare soft-reboot")?;
846 Ok(())
847}
848
849#[context("Handling soft reboot")]
851fn handle_soft_reboot<F>(
852 soft_reboot_mode: Option<SoftRebootMode>,
853 entry: Option<&crate::spec::BootEntry>,
854 deployment_type: &str,
855 execute_soft_reboot: F,
856) -> Result<()>
857where
858 F: FnOnce() -> Result<()>,
859{
860 let Some(mode) = soft_reboot_mode else {
861 return Ok(());
862 };
863
864 let can_soft_reboot = has_soft_reboot_capability(entry);
865 match mode {
866 SoftRebootMode::Required => {
867 if can_soft_reboot {
868 execute_soft_reboot()?;
869 } else {
870 anyhow::bail!(
871 "Soft reboot was required but {} deployment is not soft-reboot capable",
872 deployment_type
873 );
874 }
875 }
876 SoftRebootMode::Auto => {
877 if can_soft_reboot {
878 execute_soft_reboot()?;
879 }
880 }
881 }
882 Ok(())
883}
884
885#[context("Handling staged soft reboot")]
887fn handle_staged_soft_reboot(
888 booted_ostree: &BootedOstree<'_>,
889 soft_reboot_mode: Option<SoftRebootMode>,
890 host: &crate::spec::Host,
891) -> Result<()> {
892 handle_soft_reboot(
893 soft_reboot_mode,
894 host.status.staged.as_ref(),
895 "staged",
896 || soft_reboot_staged(booted_ostree.sysroot),
897 )
898}
899
900#[context("Soft reboot staged deployment")]
902fn soft_reboot_staged(sysroot: &SysrootLock) -> Result<()> {
903 println!("Staged deployment is soft-reboot capable, preparing for soft-reboot...");
904
905 let deployments_list = sysroot.deployments();
906 let staged_deployment = deployments_list
907 .iter()
908 .find(|d| d.is_staged())
909 .ok_or_else(|| anyhow::anyhow!("Failed to find staged deployment"))?;
910
911 prepare_soft_reboot(sysroot, staged_deployment)?;
912 Ok(())
913}
914
915#[context("Soft reboot rollback deployment")]
917fn soft_reboot_rollback(booted_ostree: &BootedOstree<'_>) -> Result<()> {
918 println!("Rollback deployment is soft-reboot capable, preparing for soft-reboot...");
919
920 let deployments_list = booted_ostree.sysroot.deployments();
921 let target_deployment = deployments_list
922 .first()
923 .ok_or_else(|| anyhow::anyhow!("No rollback deployment found!"))?;
924
925 prepare_soft_reboot(booted_ostree.sysroot, target_deployment)
926}
927
928#[context("Preparing for write")]
932pub(crate) fn prepare_for_write() -> Result<()> {
933 use std::sync::atomic::{AtomicBool, Ordering};
934
935 static ENTERED: AtomicBool = AtomicBool::new(false);
941 if ENTERED.load(Ordering::SeqCst) {
942 return Ok(());
943 }
944 if ostree_ext::container_utils::running_in_container() {
945 anyhow::bail!("Detected container; this command requires a booted host system.");
946 }
947 crate::cli::require_root(false)?;
948 ensure_self_unshared_mount_namespace()?;
949 if crate::lsm::selinux_enabled()? && !crate::lsm::selinux_ensure_install()? {
950 tracing::debug!("Do not have install_t capabilities");
951 }
952 ENTERED.store(true, Ordering::SeqCst);
953 Ok(())
954}
955
956#[context("Upgrading")]
958async fn upgrade(
959 opts: UpgradeOpts,
960 storage: &Storage,
961 booted_ostree: &BootedOstree<'_>,
962) -> Result<()> {
963 let repo = &booted_ostree.repo();
964
965 let host = crate::status::get_status(booted_ostree)?.1;
966 let imgref = host.spec.image.as_ref();
967 let prog: ProgressWriter = opts.progress.try_into()?;
968
969 if imgref.is_none() {
971 let booted_incompatible = host.status.booted.as_ref().is_some_and(|b| b.incompatible);
972
973 let staged_incompatible = host.status.staged.as_ref().is_some_and(|b| b.incompatible);
974
975 if booted_incompatible || staged_incompatible {
976 return Err(anyhow::anyhow!(
977 "Deployment contains local rpm-ostree modifications; cannot upgrade via bootc. You can run `rpm-ostree reset` to undo the modifications."
978 ));
979 }
980 }
981
982 let spec = RequiredHostSpec::from_spec(&host.spec)?;
983 let booted_image = host
984 .status
985 .booted
986 .as_ref()
987 .map(|b| b.query_image(repo))
988 .transpose()?
989 .flatten();
990 let imgref = imgref.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
991 let staged = host.status.staged.as_ref();
993 let staged_image = staged.as_ref().and_then(|s| s.image.as_ref());
994 let mut changed = false;
995
996 if opts.from_downloaded {
998 let ostree = storage.get_ostree()?;
999 let staged_deployment = ostree
1000 .staged_deployment()
1001 .ok_or_else(|| anyhow::anyhow!("No staged deployment found"))?;
1002
1003 if staged_deployment.is_finalization_locked() {
1004 ostree.change_finalization(&staged_deployment)?;
1005 println!("Staged deployment will now be applied on reboot");
1006 } else {
1007 println!("Staged deployment is already set to apply on reboot");
1008 }
1009
1010 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &host)?;
1011 if opts.apply {
1012 crate::reboot::reboot()?;
1013 }
1014 return Ok(());
1015 }
1016
1017 if opts.check {
1018 let imgref = imgref.clone().into();
1019 let mut imp = crate::deploy::new_importer(repo, &imgref).await?;
1020 match imp.prepare().await? {
1021 PrepareResult::AlreadyPresent(_) => {
1022 println!("No changes in: {imgref:#}");
1023 }
1024 PrepareResult::Ready(r) => {
1025 crate::deploy::check_bootc_label(&r.config);
1026 println!("Update available for: {imgref:#}");
1027 if let Some(version) = r.version() {
1028 println!(" Version: {version}");
1029 }
1030 println!(" Digest: {}", r.manifest_digest);
1031 changed = true;
1032 if let Some(previous_image) = booted_image.as_ref() {
1033 let diff =
1034 ostree_container::ManifestDiff::new(&previous_image.manifest, &r.manifest);
1035 diff.print();
1036 }
1037 }
1038 }
1039 } else {
1040 let use_unified = crate::deploy::image_exists_in_unified_storage(storage, imgref).await?;
1042
1043 let fetched = if use_unified {
1044 crate::deploy::pull_unified(repo, imgref, None, opts.quiet, prog.clone(), storage)
1045 .await?
1046 } else {
1047 crate::deploy::pull(repo, imgref, None, opts.quiet, prog.clone()).await?
1048 };
1049 let staged_digest = staged_image.map(|s| s.digest().expect("valid digest in status"));
1050 let fetched_digest = &fetched.manifest_digest;
1051 tracing::debug!("staged: {staged_digest:?}");
1052 tracing::debug!("fetched: {fetched_digest}");
1053 let staged_unchanged = staged_digest
1054 .as_ref()
1055 .map(|d| d == fetched_digest)
1056 .unwrap_or_default();
1057 let booted_unchanged = booted_image
1058 .as_ref()
1059 .map(|img| &img.manifest_digest == fetched_digest)
1060 .unwrap_or_default();
1061 if staged_unchanged {
1062 let staged_deployment = storage.get_ostree()?.staged_deployment();
1063 let mut download_only_changed = false;
1064
1065 if let Some(staged) = staged_deployment {
1066 if opts.download_only {
1068 if !staged.is_finalization_locked() {
1070 storage.get_ostree()?.change_finalization(&staged)?;
1071 println!("Image downloaded, but will not be applied on reboot");
1072 download_only_changed = true;
1073 }
1074 } else if !opts.check {
1075 if staged.is_finalization_locked() {
1078 storage.get_ostree()?.change_finalization(&staged)?;
1079 println!("Staged deployment will now be applied on reboot");
1080 download_only_changed = true;
1081 }
1082 }
1083 } else if opts.download_only || opts.apply {
1084 anyhow::bail!("No staged deployment found");
1085 }
1086
1087 if !download_only_changed {
1088 println!("Staged update present, not changed");
1089 }
1090
1091 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &host)?;
1092 if opts.apply {
1093 crate::reboot::reboot()?;
1094 }
1095 } else if booted_unchanged {
1096 println!("No update available.")
1097 } else {
1098 let stateroot = booted_ostree.stateroot();
1099 let from = MergeState::from_stateroot(storage, &stateroot)?;
1100 crate::deploy::stage(
1101 storage,
1102 from,
1103 &fetched,
1104 &spec,
1105 prog.clone(),
1106 opts.download_only,
1107 )
1108 .await?;
1109 changed = true;
1110 if let Some(prev) = booted_image.as_ref() {
1111 if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
1112 let diff =
1113 ostree_container::ManifestDiff::new(&prev.manifest, &fetched_manifest);
1114 diff.print();
1115 }
1116 }
1117 }
1118 }
1119 if changed {
1120 storage.update_mtime()?;
1121
1122 if opts.soft_reboot.is_some() {
1123 let updated_host = crate::status::get_status(booted_ostree)?.1;
1126 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &updated_host)?;
1127 }
1128
1129 if opts.apply {
1130 crate::reboot::reboot()?;
1131 }
1132 } else {
1133 tracing::debug!("No changes");
1134 }
1135
1136 Ok(())
1137}
1138
1139pub(crate) fn imgref_for_switch(opts: &SwitchOpts) -> Result<ImageReference> {
1140 let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
1141 let imgref = ostree_container::ImageReference {
1142 transport,
1143 name: opts.target.to_string(),
1144 };
1145 let sigverify = sigpolicy_from_opt(opts.enforce_container_sigpolicy);
1146 let target = ostree_container::OstreeImageReference { sigverify, imgref };
1147 let target = ImageReference::from(target);
1148
1149 return Ok(target);
1150}
1151
1152#[context("Switching (ostree)")]
1154async fn switch_ostree(
1155 opts: SwitchOpts,
1156 storage: &Storage,
1157 booted_ostree: &BootedOstree<'_>,
1158) -> Result<()> {
1159 let target = imgref_for_switch(&opts)?;
1160 let prog: ProgressWriter = opts.progress.try_into()?;
1161 let cancellable = gio::Cancellable::NONE;
1162
1163 let repo = &booted_ostree.repo();
1164 let (_, host) = crate::status::get_status(booted_ostree)?;
1165
1166 let new_spec = {
1167 let mut new_spec = host.spec.clone();
1168 new_spec.image = Some(target.clone());
1169 new_spec
1170 };
1171
1172 if new_spec == host.spec {
1173 println!("Image specification is unchanged.");
1174 return Ok(());
1175 }
1176
1177 const SWITCH_JOURNAL_ID: &str = "7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1";
1179 let old_image = host
1180 .spec
1181 .image
1182 .as_ref()
1183 .map(|i| i.image.as_str())
1184 .unwrap_or("none");
1185
1186 tracing::info!(
1187 message_id = SWITCH_JOURNAL_ID,
1188 bootc.old_image_reference = old_image,
1189 bootc.new_image_reference = &target.image,
1190 bootc.new_image_transport = &target.transport,
1191 "Switching from image {} to {}",
1192 old_image,
1193 target.image
1194 );
1195
1196 let new_spec = RequiredHostSpec::from_spec(&new_spec)?;
1197
1198 let use_unified = if opts.unified_storage_exp {
1202 true
1203 } else {
1204 crate::deploy::image_exists_in_unified_storage(storage, &target).await?
1205 };
1206
1207 let fetched = if use_unified {
1208 crate::deploy::pull_unified(repo, &target, None, opts.quiet, prog.clone(), storage).await?
1209 } else {
1210 crate::deploy::pull(repo, &target, None, opts.quiet, prog.clone()).await?
1211 };
1212
1213 if !opts.retain {
1214 if let Some(booted_origin) = booted_ostree.deployment.origin() {
1216 if let Some(ostree_ref) = booted_origin.optional_string("origin", "refspec")? {
1217 let (remote, ostree_ref) =
1218 ostree::parse_refspec(&ostree_ref).context("Failed to parse ostree ref")?;
1219 repo.set_ref_immediate(remote.as_deref(), &ostree_ref, None, cancellable)?;
1220 }
1221 }
1222 }
1223
1224 let stateroot = booted_ostree.stateroot();
1225 let from = MergeState::from_stateroot(storage, &stateroot)?;
1226 crate::deploy::stage(storage, from, &fetched, &new_spec, prog.clone(), false).await?;
1227
1228 storage.update_mtime()?;
1229
1230 if opts.soft_reboot.is_some() {
1231 let updated_host = crate::status::get_status(booted_ostree)?.1;
1234 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &updated_host)?;
1235 }
1236
1237 if opts.apply {
1238 crate::reboot::reboot()?;
1239 }
1240
1241 Ok(())
1242}
1243
1244#[context("Switching")]
1246async fn switch(opts: SwitchOpts) -> Result<()> {
1247 if opts.mutate_in_place {
1251 let target = imgref_for_switch(&opts)?;
1252 let deployid = {
1253 let target = target.clone();
1255 let root = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1256 tokio::task::spawn_blocking(move || {
1257 crate::deploy::switch_origin_inplace(&root, &target)
1258 })
1259 .await??
1260 };
1261 println!("Updated {deployid} to pull from {target}");
1262 return Ok(());
1263 }
1264 let storage = &get_storage().await?;
1265 match storage.kind()? {
1266 BootedStorageKind::Ostree(booted_ostree) => {
1267 switch_ostree(opts, storage, &booted_ostree).await
1268 }
1269 BootedStorageKind::Composefs(booted_cfs) => {
1270 switch_composefs(opts, storage, &booted_cfs).await
1271 }
1272 }
1273}
1274
1275#[context("Rollback (ostree)")]
1277async fn rollback_ostree(
1278 opts: &RollbackOpts,
1279 storage: &Storage,
1280 booted_ostree: &BootedOstree<'_>,
1281) -> Result<()> {
1282 crate::deploy::rollback(storage).await?;
1283
1284 if opts.soft_reboot.is_some() {
1285 let host = crate::status::get_status(booted_ostree)?.1;
1287
1288 handle_soft_reboot(
1289 opts.soft_reboot,
1290 host.status.rollback.as_ref(),
1291 "rollback",
1292 || soft_reboot_rollback(booted_ostree),
1293 )?;
1294 }
1295
1296 Ok(())
1297}
1298
1299#[context("Rollback")]
1301async fn rollback(opts: &RollbackOpts) -> Result<()> {
1302 let storage = &get_storage().await?;
1303 match storage.kind()? {
1304 BootedStorageKind::Ostree(booted_ostree) => {
1305 rollback_ostree(opts, storage, &booted_ostree).await
1306 }
1307 BootedStorageKind::Composefs(booted_cfs) => composefs_rollback(storage, &booted_cfs).await,
1308 }
1309}
1310
1311#[context("Editing spec (ostree)")]
1313async fn edit_ostree(
1314 opts: EditOpts,
1315 storage: &Storage,
1316 booted_ostree: &BootedOstree<'_>,
1317) -> Result<()> {
1318 let repo = &booted_ostree.repo();
1319 let (_, host) = crate::status::get_status(booted_ostree)?;
1320
1321 let new_host: Host = if let Some(filename) = opts.filename {
1322 let mut r = std::io::BufReader::new(std::fs::File::open(filename)?);
1323 serde_yaml::from_reader(&mut r)?
1324 } else {
1325 let tmpf = tempfile::NamedTempFile::with_suffix(".yaml")?;
1326 serde_yaml::to_writer(std::io::BufWriter::new(tmpf.as_file()), &host)?;
1327 crate::utils::spawn_editor(&tmpf)?;
1328 tmpf.as_file().seek(std::io::SeekFrom::Start(0))?;
1329 serde_yaml::from_reader(&mut tmpf.as_file())?
1330 };
1331
1332 if new_host.spec == host.spec {
1333 println!("Edit cancelled, no changes made.");
1334 return Ok(());
1335 }
1336 host.spec.verify_transition(&new_host.spec)?;
1337 let new_spec = RequiredHostSpec::from_spec(&new_host.spec)?;
1338
1339 let prog = ProgressWriter::default();
1340
1341 if host.spec.boot_order != new_host.spec.boot_order {
1344 return crate::deploy::rollback(storage).await;
1345 }
1346
1347 let fetched = crate::deploy::pull(repo, new_spec.image, None, opts.quiet, prog.clone()).await?;
1348
1349 let stateroot = booted_ostree.stateroot();
1352 let from = MergeState::from_stateroot(storage, &stateroot)?;
1353 crate::deploy::stage(storage, from, &fetched, &new_spec, prog.clone(), false).await?;
1354
1355 storage.update_mtime()?;
1356
1357 Ok(())
1358}
1359
1360#[context("Editing spec")]
1362async fn edit(opts: EditOpts) -> Result<()> {
1363 let storage = &get_storage().await?;
1364 match storage.kind()? {
1365 BootedStorageKind::Ostree(booted_ostree) => {
1366 edit_ostree(opts, storage, &booted_ostree).await
1367 }
1368 BootedStorageKind::Composefs(_) => {
1369 anyhow::bail!("Edit is not yet supported for composefs backend")
1370 }
1371 }
1372}
1373
1374async fn usroverlay() -> Result<()> {
1376 Err(Command::new("ostree")
1379 .args(["admin", "unlock"])
1380 .exec()
1381 .into())
1382}
1383
1384pub fn global_init() -> Result<()> {
1387 ostree::glib::set_prgname(bootc_utils::NAME.into());
1390 if let Err(e) = rustix::thread::set_name(&CString::new(bootc_utils::NAME).unwrap()) {
1391 eprintln!("failed to set name: {e}");
1393 }
1394 ostree::SePolicy::set_null_log();
1396 let am_root = rustix::process::getuid().is_root();
1397 if std::env::var_os("HOME").is_none() && am_root {
1400 std::env::set_var("HOME", "/root");
1404 }
1405 Ok(())
1406}
1407
1408pub async fn run_from_iter<I>(args: I) -> Result<()>
1411where
1412 I: IntoIterator,
1413 I::Item: Into<OsString> + Clone,
1414{
1415 run_from_opt(Opt::parse_including_static(args)).await
1416}
1417
1418fn callname_from_argv0(argv0: &OsStr) -> &str {
1422 let default = "bootc";
1423 std::path::Path::new(argv0)
1424 .file_name()
1425 .and_then(|s| s.to_str())
1426 .filter(|s| !s.is_empty())
1427 .unwrap_or(default)
1428}
1429
1430impl Opt {
1431 fn parse_including_static<I>(args: I) -> Self
1434 where
1435 I: IntoIterator,
1436 I::Item: Into<OsString> + Clone,
1437 {
1438 let mut args = args.into_iter();
1439 let first = if let Some(first) = args.next() {
1440 let first: OsString = first.into();
1441 let argv0 = callname_from_argv0(&first);
1442 tracing::debug!("argv0={argv0:?}");
1443 let mapped = match argv0 {
1444 InternalsOpts::GENERATOR_BIN => {
1445 Some(["bootc", "internals", "systemd-generator"].as_slice())
1446 }
1447 "ostree-container" | "ostree-ima-sign" | "ostree-provisional-repair" => {
1448 Some(["bootc", "internals", "ostree-ext"].as_slice())
1449 }
1450 _ => None,
1451 };
1452 if let Some(base_args) = mapped {
1453 let base_args = base_args.iter().map(OsString::from);
1454 return Opt::parse_from(base_args.chain(args.map(|i| i.into())));
1455 }
1456 Some(first)
1457 } else {
1458 None
1459 };
1460 Opt::parse_from(first.into_iter().chain(args.map(|i| i.into())))
1461 }
1462}
1463
1464async fn run_from_opt(opt: Opt) -> Result<()> {
1466 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1467 match opt {
1468 Opt::Upgrade(opts) => {
1469 let storage = &get_storage().await?;
1470 match storage.kind()? {
1471 BootedStorageKind::Ostree(booted_ostree) => {
1472 upgrade(opts, storage, &booted_ostree).await
1473 }
1474 BootedStorageKind::Composefs(booted_cfs) => {
1475 upgrade_composefs(opts, storage, &booted_cfs).await
1476 }
1477 }
1478 }
1479 Opt::Switch(opts) => switch(opts).await,
1480 Opt::Rollback(opts) => {
1481 rollback(&opts).await?;
1482 if opts.apply {
1483 crate::reboot::reboot()?;
1484 }
1485 Ok(())
1486 }
1487 Opt::Edit(opts) => edit(opts).await,
1488 Opt::UsrOverlay => {
1489 use crate::store::Environment;
1490 let env = Environment::detect()?;
1491 match env {
1492 Environment::OstreeBooted => usroverlay().await,
1493 Environment::ComposefsBooted(_) => composefs_usr_overlay(),
1494 _ => anyhow::bail!("usroverlay only applies on booted hosts"),
1495 }
1496 }
1497 Opt::Container(opts) => match opts {
1498 ContainerOpts::Inspect {
1499 rootfs,
1500 json,
1501 format,
1502 } => crate::status::container_inspect(&rootfs, json, format),
1503 ContainerOpts::Lint {
1504 rootfs,
1505 fatal_warnings,
1506 list,
1507 skip,
1508 no_truncate,
1509 } => {
1510 if list {
1511 return lints::lint_list(std::io::stdout().lock());
1512 }
1513 let warnings = if fatal_warnings {
1514 lints::WarningDisposition::FatalWarnings
1515 } else {
1516 lints::WarningDisposition::AllowWarnings
1517 };
1518 let root_type = if rootfs == "/" {
1519 lints::RootType::Running
1520 } else {
1521 lints::RootType::Alternative
1522 };
1523
1524 let root = &Dir::open_ambient_dir(rootfs, cap_std::ambient_authority())?;
1525 let skip = skip.iter().map(|s| s.as_str());
1526 lints::lint(
1527 root,
1528 warnings,
1529 root_type,
1530 skip,
1531 std::io::stdout().lock(),
1532 no_truncate,
1533 )?;
1534 Ok(())
1535 }
1536 ContainerOpts::ComputeComposefsDigest {
1537 path,
1538 write_dumpfile_to,
1539 } => {
1540 let digest = compute_composefs_digest(&path, write_dumpfile_to.as_deref())?;
1541 println!("{digest}");
1542 Ok(())
1543 }
1544 ContainerOpts::ComputeComposefsDigestFromStorage {
1545 write_dumpfile_to,
1546 image,
1547 } => {
1548 let (_td_guard, repo) = new_temp_composefs_repo()?;
1549
1550 let mut proxycfg = ImageProxyConfig::default();
1551
1552 let image = if let Some(image) = image {
1553 image
1554 } else {
1555 let host_container_store = Utf8Path::new("/run/host-container-storage");
1556 let container_info = crate::containerenv::get_container_execution_info(&root)?;
1559 let iid = container_info.imageid;
1560 tracing::debug!("Computing digest of {iid}");
1561
1562 if !host_container_store.try_exists()? {
1563 anyhow::bail!(
1564 "Must be readonly mount of host container store: {host_container_store}"
1565 );
1566 }
1567 let mut cmd = Command::new("skopeo");
1569 set_additional_image_store(&mut cmd, "/run/host-container-storage");
1570 proxycfg.skopeo_cmd = Some(cmd);
1571 iid
1572 };
1573
1574 let imgref = format!("containers-storage:{image}");
1575 let (imgid, verity) = composefs_oci::pull(&repo, &imgref, None, Some(proxycfg))
1576 .await
1577 .context("Pulling image")?;
1578 let imgid = hex::encode(imgid);
1579 let mut fs = composefs_oci::image::create_filesystem(&repo, &imgid, Some(&verity))
1580 .context("Populating fs")?;
1581 fs.transform_for_boot(&repo).context("Preparing for boot")?;
1582 let id = fs.compute_image_id();
1583 println!("{}", id.to_hex());
1584
1585 if let Some(path) = write_dumpfile_to.as_deref() {
1586 let mut w = File::create(path)
1587 .with_context(|| format!("Opening {path}"))
1588 .map(BufWriter::new)?;
1589 dumpfile::write_dumpfile(&mut w, &fs).context("Writing dumpfile")?;
1590 }
1591
1592 Ok(())
1593 }
1594 },
1595 Opt::Completion { shell } => {
1596 use clap_complete::aot::generate;
1597
1598 let mut cmd = Opt::command();
1599 let mut stdout = std::io::stdout();
1600 let bin_name = "bootc";
1601 generate(shell, &mut cmd, bin_name, &mut stdout);
1602 Ok(())
1603 }
1604 Opt::Image(opts) => match opts {
1605 ImageOpts::List {
1606 list_type,
1607 list_format,
1608 } => crate::image::list_entrypoint(list_type, list_format).await,
1609
1610 ImageOpts::CopyToStorage { source, target } => {
1611 let host = get_host().await?;
1613
1614 let storage = get_storage().await?;
1615
1616 match storage.kind()? {
1617 BootedStorageKind::Ostree(..) => {
1618 crate::image::push_entrypoint(
1619 &storage,
1620 &host,
1621 source.as_deref(),
1622 target.as_deref(),
1623 )
1624 .await
1625 }
1626 BootedStorageKind::Composefs(booted) => {
1627 bootc_composefs::export::export_repo_to_image(
1628 &storage,
1629 &booted,
1630 source.as_deref(),
1631 target.as_deref(),
1632 )
1633 .await
1634 }
1635 }
1636 }
1637 ImageOpts::SetUnified => crate::image::set_unified_entrypoint().await,
1638 ImageOpts::PullFromDefaultStorage { image } => {
1639 let storage = get_storage().await?;
1640 storage
1641 .get_ensure_imgstore()?
1642 .pull_from_host_storage(&image)
1643 .await
1644 }
1645 ImageOpts::Cmd(opt) => {
1646 let storage = get_storage().await?;
1647 let imgstore = storage.get_ensure_imgstore()?;
1648 match opt {
1649 ImageCmdOpts::List { args } => {
1650 crate::image::imgcmd_entrypoint(imgstore, "list", &args).await
1651 }
1652 ImageCmdOpts::Build { args } => {
1653 crate::image::imgcmd_entrypoint(imgstore, "build", &args).await
1654 }
1655 ImageCmdOpts::Pull { args } => {
1656 crate::image::imgcmd_entrypoint(imgstore, "pull", &args).await
1657 }
1658 ImageCmdOpts::Push { args } => {
1659 crate::image::imgcmd_entrypoint(imgstore, "push", &args).await
1660 }
1661 }
1662 }
1663 },
1664 Opt::Install(opts) => match opts {
1665 #[cfg(feature = "install-to-disk")]
1666 InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await,
1667 InstallOpts::ToFilesystem(opts) => {
1668 crate::install::install_to_filesystem(opts, false, crate::install::Cleanup::Skip)
1669 .await
1670 }
1671 InstallOpts::ToExistingRoot(opts) => {
1672 crate::install::install_to_existing_root(opts).await
1673 }
1674 InstallOpts::Reset(opts) => crate::install::install_reset(opts).await,
1675 InstallOpts::PrintConfiguration(opts) => crate::install::print_configuration(opts),
1676 InstallOpts::EnsureCompletion {} => {
1677 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1678 crate::install::completion::run_from_anaconda(rootfs).await
1679 }
1680 InstallOpts::Finalize { root_path } => {
1681 crate::install::install_finalize(&root_path).await
1682 }
1683 },
1684 Opt::ExecInHostMountNamespace { args } => {
1685 crate::install::exec_in_host_mountns(args.as_slice())
1686 }
1687 Opt::Status(opts) => super::status::status(opts).await,
1688 Opt::Internals(opts) => match opts {
1689 InternalsOpts::SystemdGenerator {
1690 normal_dir,
1691 early_dir: _,
1692 late_dir: _,
1693 } => {
1694 let unit_dir = &Dir::open_ambient_dir(normal_dir, cap_std::ambient_authority())?;
1695 crate::generator::generator(root, unit_dir)
1696 }
1697 InternalsOpts::OstreeExt { args } => {
1698 ostree_ext::cli::run_from_iter(["ostree-ext".into()].into_iter().chain(args)).await
1699 }
1700 InternalsOpts::OstreeContainer { args } => {
1701 ostree_ext::cli::run_from_iter(
1702 ["ostree-ext".into(), "container".into()]
1703 .into_iter()
1704 .chain(args),
1705 )
1706 .await
1707 }
1708 InternalsOpts::TestComposefs => {
1709 let storage = get_storage().await?;
1711 let cfs = storage.get_ensure_composefs()?;
1712 let testdata = b"some test data";
1713 let testdata_digest = openssl::sha::sha256(testdata);
1714 let mut w = SplitStreamWriter::new(&cfs, None, Some(testdata_digest));
1715 w.write_inline(testdata);
1716 let object = cfs.write_stream(w, Some("testobject"))?.to_hex();
1717 assert_eq!(
1718 object,
1719 "5d94ceb0b2bb3a78237e0a74bc030a262239ab5f47754a5eb2e42941056b64cb21035d64a8f7c2f156e34b820802fa51884de2b1f7dc3a41b9878fc543cd9b07"
1720 );
1721 Ok(())
1722 }
1723 InternalsOpts::Fsverity(args) => match args {
1725 FsverityOpts::Measure { path } => {
1726 let fd =
1727 std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
1728 let digest: fsverity::Sha256HashValue = fsverity::measure_verity(&fd)?;
1729 let digest = digest.to_hex();
1730 println!("{digest}");
1731 Ok(())
1732 }
1733 FsverityOpts::Enable { path } => {
1734 let fd =
1735 std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
1736 fsverity::enable_verity_raw::<fsverity::Sha256HashValue>(&fd)?;
1737 Ok(())
1738 }
1739 },
1740 InternalsOpts::Cfs { args } => crate::cfsctl::run_from_iter(args.iter()).await,
1741 InternalsOpts::Reboot => crate::reboot::reboot(),
1742 InternalsOpts::Fsck => {
1743 let storage = &get_storage().await?;
1744 crate::fsck::fsck(&storage, std::io::stdout().lock()).await?;
1745 Ok(())
1746 }
1747 InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root),
1748 InternalsOpts::PrintJsonSchema { of } => {
1749 let schema = match of {
1750 SchemaType::Host => schema_for!(crate::spec::Host),
1751 SchemaType::Progress => schema_for!(crate::progress_jsonl::Event),
1752 };
1753 let mut stdout = std::io::stdout().lock();
1754 serde_json::to_writer_pretty(&mut stdout, &schema)?;
1755 Ok(())
1756 }
1757 InternalsOpts::Cleanup => {
1758 let storage = get_storage().await?;
1759 crate::deploy::cleanup(&storage).await
1760 }
1761 InternalsOpts::Relabel { as_path, path } => {
1762 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1763 let path = path.strip_prefix("/")?;
1764 let sepolicy =
1765 &ostree::SePolicy::new(&gio::File::for_path("/"), gio::Cancellable::NONE)?;
1766 crate::lsm::relabel_recurse(root, path, as_path.as_deref(), sepolicy)?;
1767 Ok(())
1768 }
1769 InternalsOpts::BootcInstallCompletion { sysroot, stateroot } => {
1770 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1771 crate::install::completion::run_from_ostree(rootfs, &sysroot, &stateroot).await
1772 }
1773 InternalsOpts::LoopbackCleanupHelper { device } => {
1774 crate::blockdev::run_loopback_cleanup_helper(&device).await
1775 }
1776 InternalsOpts::AllocateCleanupLoopback { file_path: _ } => {
1777 let temp_file =
1779 tempfile::NamedTempFile::new().context("Failed to create temporary file")?;
1780 let temp_path = temp_file.path();
1781
1782 let loopback = crate::blockdev::LoopbackDevice::new(temp_path)
1784 .context("Failed to create loopback device")?;
1785
1786 println!("Created loopback device: {}", loopback.path());
1787
1788 loopback
1790 .close()
1791 .context("Failed to close loopback device")?;
1792
1793 println!("Successfully closed loopback device");
1794 Ok(())
1795 }
1796 #[cfg(feature = "rhsm")]
1797 InternalsOpts::PublishRhsmFacts => crate::rhsm::publish_facts(&root).await,
1798 #[cfg(feature = "docgen")]
1799 InternalsOpts::DumpCliJson => {
1800 use clap::CommandFactory;
1801 let cmd = Opt::command();
1802 let json = crate::cli_json::dump_cli_json(&cmd)?;
1803 println!("{}", json);
1804 Ok(())
1805 }
1806 InternalsOpts::DirDiff {
1807 pristine_etc,
1808 current_etc,
1809 new_etc,
1810 merge,
1811 } => {
1812 let pristine_etc =
1813 Dir::open_ambient_dir(pristine_etc, cap_std::ambient_authority())?;
1814 let current_etc = Dir::open_ambient_dir(current_etc, cap_std::ambient_authority())?;
1815 let new_etc = Dir::open_ambient_dir(new_etc, cap_std::ambient_authority())?;
1816
1817 let (p, c, n) =
1818 etc_merge::traverse_etc(&pristine_etc, ¤t_etc, Some(&new_etc))?;
1819
1820 let diff = compute_diff(&p, &c)?;
1821 print_diff(&diff, &mut std::io::stdout());
1822
1823 if merge {
1824 let n =
1825 n.ok_or_else(|| anyhow::anyhow!("Failed to get dirtree for new etc"))?;
1826 etc_merge::merge(¤t_etc, &c, &new_etc, &n, diff)?;
1827 }
1828
1829 Ok(())
1830 }
1831 InternalsOpts::PrepSoftReboot { deployment, reboot } => {
1832 let storage = &get_storage().await?;
1833
1834 match storage.kind()? {
1835 BootedStorageKind::Ostree(..) => {
1836 anyhow::bail!("soft-reboot only implemented for composefs")
1838 }
1839 BootedStorageKind::Composefs(booted_cfs) => {
1840 prepare_soft_reboot_composefs(&storage, &booted_cfs, &deployment, reboot)
1841 .await
1842 }
1843 }
1844 }
1845 },
1846 Opt::State(opts) => match opts {
1847 StateOpts::WipeOstree => {
1848 let sysroot = ostree::Sysroot::new_default();
1849 sysroot.load(gio::Cancellable::NONE)?;
1850 crate::deploy::wipe_ostree(sysroot).await?;
1851 Ok(())
1852 }
1853 },
1854
1855 Opt::ComposefsFinalizeStaged => {
1856 let storage = &get_storage().await?;
1857 match storage.kind()? {
1858 BootedStorageKind::Ostree(_) => {
1859 anyhow::bail!("ComposefsFinalizeStaged is only supported for composefs backend")
1860 }
1861 BootedStorageKind::Composefs(booted_cfs) => {
1862 composefs_backend_finalize(storage, &booted_cfs).await
1863 }
1864 }
1865 }
1866
1867 Opt::ConfigDiff => {
1868 let storage = &get_storage().await?;
1869 match storage.kind()? {
1870 BootedStorageKind::Ostree(_) => {
1871 anyhow::bail!("ConfigDiff is only supported for composefs backend")
1872 }
1873 BootedStorageKind::Composefs(booted_cfs) => {
1874 get_etc_diff(storage, &booted_cfs).await
1875 }
1876 }
1877 }
1878
1879 Opt::DeleteDeployment { depl_id } => {
1880 let storage = &get_storage().await?;
1881 match storage.kind()? {
1882 BootedStorageKind::Ostree(_) => {
1883 anyhow::bail!("DeleteDeployment is only supported for composefs backend")
1884 }
1885 BootedStorageKind::Composefs(booted_cfs) => {
1886 delete_composefs_deployment(&depl_id, storage, &booted_cfs).await
1887 }
1888 }
1889 }
1890 }
1891}
1892
1893#[cfg(test)]
1894mod tests {
1895 use super::*;
1896
1897 #[test]
1898 fn test_callname() {
1899 use std::os::unix::ffi::OsStrExt;
1900
1901 let mapped_cases = [
1903 ("", "bootc"),
1904 ("/foo/bar", "bar"),
1905 ("/foo/bar/", "bar"),
1906 ("foo/bar", "bar"),
1907 ("../foo/bar", "bar"),
1908 ("usr/bin/ostree-container", "ostree-container"),
1909 ];
1910 for (input, output) in mapped_cases {
1911 assert_eq!(
1912 output,
1913 callname_from_argv0(OsStr::new(input)),
1914 "Handling mapped case {input}"
1915 );
1916 }
1917
1918 assert_eq!("bootc", callname_from_argv0(OsStr::from_bytes(b"foo\x80")));
1920
1921 let ident_cases = ["foo", "bootc"];
1923 for case in ident_cases {
1924 assert_eq!(
1925 case,
1926 callname_from_argv0(OsStr::new(case)),
1927 "Handling ident case {case}"
1928 );
1929 }
1930 }
1931
1932 #[test]
1933 fn test_parse_install_args() {
1934 let o = Opt::try_parse_from([
1936 "bootc",
1937 "install",
1938 "to-filesystem",
1939 "--target-no-signature-verification",
1940 "/target",
1941 ])
1942 .unwrap();
1943 let o = match o {
1944 Opt::Install(InstallOpts::ToFilesystem(fsopts)) => fsopts,
1945 o => panic!("Expected filesystem opts, not {o:?}"),
1946 };
1947 assert!(o.target_opts.target_no_signature_verification);
1948 assert_eq!(o.filesystem_opts.root_path.as_str(), "/target");
1949 assert_eq!(
1951 o.config_opts.bound_images,
1952 crate::install::BoundImagesOpt::Stored
1953 );
1954 }
1955
1956 #[test]
1957 fn test_parse_opts() {
1958 assert!(matches!(
1959 Opt::parse_including_static(["bootc", "status"]),
1960 Opt::Status(StatusOpts {
1961 json: false,
1962 format: None,
1963 format_version: None,
1964 booted: false,
1965 verbose: false
1966 })
1967 ));
1968 assert!(matches!(
1969 Opt::parse_including_static(["bootc", "status", "--format-version=0"]),
1970 Opt::Status(StatusOpts {
1971 format_version: Some(0),
1972 ..
1973 })
1974 ));
1975
1976 assert!(matches!(
1978 Opt::parse_including_static(["bootc", "status", "--verbose"]),
1979 Opt::Status(StatusOpts { verbose: true, .. })
1980 ));
1981
1982 assert!(matches!(
1984 Opt::parse_including_static(["bootc", "status", "-v"]),
1985 Opt::Status(StatusOpts { verbose: true, .. })
1986 ));
1987 }
1988
1989 #[test]
1990 fn test_parse_generator() {
1991 assert!(matches!(
1992 Opt::parse_including_static([
1993 "/usr/lib/systemd/system/bootc-systemd-generator",
1994 "/run/systemd/system"
1995 ]),
1996 Opt::Internals(InternalsOpts::SystemdGenerator { normal_dir, .. }) if normal_dir == "/run/systemd/system"
1997 ));
1998 }
1999
2000 #[test]
2001 fn test_parse_ostree_ext() {
2002 assert!(matches!(
2003 Opt::parse_including_static(["bootc", "internals", "ostree-container"]),
2004 Opt::Internals(InternalsOpts::OstreeContainer { .. })
2005 ));
2006
2007 fn peel(o: Opt) -> Vec<OsString> {
2008 match o {
2009 Opt::Internals(InternalsOpts::OstreeExt { args }) => args,
2010 o => panic!("unexpected {o:?}"),
2011 }
2012 }
2013 let args = peel(Opt::parse_including_static([
2014 "/usr/libexec/libostree/ext/ostree-ima-sign",
2015 "ima-sign",
2016 "--repo=foo",
2017 "foo",
2018 "bar",
2019 "baz",
2020 ]));
2021 assert_eq!(
2022 args.as_slice(),
2023 ["ima-sign", "--repo=foo", "foo", "bar", "baz"]
2024 );
2025
2026 let args = peel(Opt::parse_including_static([
2027 "/usr/libexec/libostree/ext/ostree-container",
2028 "container",
2029 "image",
2030 "pull",
2031 ]));
2032 assert_eq!(args.as_slice(), ["container", "image", "pull"]);
2033 }
2034
2035 #[test]
2036 fn test_generate_completion_scripts_contain_commands() {
2037 use clap_complete::aot::{generate, Shell};
2038
2039 let want = ["install", "upgrade"];
2048
2049 for shell in [Shell::Bash, Shell::Zsh, Shell::Fish] {
2050 let mut cmd = Opt::command();
2051 let mut buf = Vec::new();
2052 generate(shell, &mut cmd, "bootc", &mut buf);
2053 let s = String::from_utf8(buf).expect("completion should be utf8");
2054 for w in &want {
2055 assert!(s.contains(w), "{shell:?} completion missing {w}");
2056 }
2057 }
2058 }
2059}