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::{Context, Result, anyhow, ensure};
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;
29
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, reset_soft_reboot};
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 Ukify {
404 #[clap(long, default_value = "/")]
406 rootfs: Utf8PathBuf,
407
408 #[clap(long = "karg", hide = true)]
412 kargs: Vec<String>,
413
414 #[clap(last = true)]
416 args: Vec<OsString>,
417 },
418}
419
420#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
422pub(crate) enum ImageCmdOpts {
423 List {
425 #[clap(allow_hyphen_values = true)]
426 args: Vec<OsString>,
427 },
428 Build {
430 #[clap(allow_hyphen_values = true)]
431 args: Vec<OsString>,
432 },
433 Pull {
435 #[clap(allow_hyphen_values = true)]
436 args: Vec<OsString>,
437 },
438 Push {
440 #[clap(allow_hyphen_values = true)]
441 args: Vec<OsString>,
442 },
443}
444
445#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
446#[serde(rename_all = "kebab-case")]
447pub(crate) enum ImageListType {
448 #[default]
450 All,
451 Logical,
453 Host,
455}
456
457impl std::fmt::Display for ImageListType {
458 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
459 self.to_possible_value().unwrap().get_name().fmt(f)
460 }
461}
462
463#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
464#[serde(rename_all = "kebab-case")]
465pub(crate) enum ImageListFormat {
466 #[default]
468 Table,
469 Json,
471}
472impl std::fmt::Display for ImageListFormat {
473 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
474 self.to_possible_value().unwrap().get_name().fmt(f)
475 }
476}
477
478#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
480pub(crate) enum ImageOpts {
481 List {
485 #[clap(long = "type")]
487 #[arg(default_value_t)]
488 list_type: ImageListType,
489 #[clap(long = "format")]
490 #[arg(default_value_t)]
491 list_format: ImageListFormat,
492 },
493 CopyToStorage {
510 #[clap(long)]
511 source: Option<String>,
513
514 #[clap(long)]
515 target: Option<String>,
518 },
519 SetUnified,
524 PullFromDefaultStorage {
526 image: String,
528 },
529 #[clap(subcommand)]
531 Cmd(ImageCmdOpts),
532}
533
534#[derive(Debug, Clone, clap::ValueEnum, PartialEq, Eq)]
535pub(crate) enum SchemaType {
536 Host,
537 Progress,
538}
539
540#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
542pub(crate) enum FsverityOpts {
543 Measure {
545 path: Utf8PathBuf,
547 },
548 Enable {
550 path: Utf8PathBuf,
552 },
553}
554
555#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
557pub(crate) enum InternalsOpts {
558 SystemdGenerator {
559 normal_dir: Utf8PathBuf,
560 #[allow(dead_code)]
561 early_dir: Option<Utf8PathBuf>,
562 #[allow(dead_code)]
563 late_dir: Option<Utf8PathBuf>,
564 },
565 FixupEtcFstab,
566 PrintJsonSchema {
568 #[clap(long)]
569 of: SchemaType,
570 },
571 #[clap(subcommand)]
572 Fsverity(FsverityOpts),
573 Fsck,
575 Cleanup,
577 Relabel {
578 #[clap(long)]
579 as_path: Option<Utf8PathBuf>,
581
582 path: Utf8PathBuf,
584 },
585 OstreeExt {
587 #[clap(allow_hyphen_values = true)]
588 args: Vec<OsString>,
589 },
590 Cfs {
592 #[clap(allow_hyphen_values = true)]
593 args: Vec<OsString>,
594 },
595 OstreeContainer {
597 #[clap(allow_hyphen_values = true)]
598 args: Vec<OsString>,
599 },
600 TestComposefs,
602 LoopbackCleanupHelper {
604 #[clap(long)]
606 device: String,
607 },
608 AllocateCleanupLoopback {
610 #[clap(long)]
612 file_path: Utf8PathBuf,
613 },
614 BootcInstallCompletion {
616 sysroot: Utf8PathBuf,
618
619 stateroot: String,
621 },
622 Reboot,
625 #[cfg(feature = "rhsm")]
626 PublishRhsmFacts,
628 DirDiff {
630 pristine_etc: Utf8PathBuf,
632 current_etc: Utf8PathBuf,
634 new_etc: Utf8PathBuf,
636 #[clap(long)]
638 merge: bool,
639 },
640 #[cfg(feature = "docgen")]
641 DumpCliJson,
643 PrepSoftReboot {
644 #[clap(required_unless_present = "reset")]
645 deployment: Option<String>,
646 #[clap(long, conflicts_with = "reset")]
647 reboot: bool,
648 #[clap(long, conflicts_with = "reboot")]
649 reset: bool,
650 },
651}
652
653#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
654pub(crate) enum StateOpts {
655 WipeOstree,
657}
658
659impl InternalsOpts {
660 const GENERATOR_BIN: &'static str = "bootc-systemd-generator";
662}
663
664#[derive(Debug, Parser, PartialEq, Eq)]
672#[clap(name = "bootc")]
673#[clap(rename_all = "kebab-case")]
674#[clap(version,long_version=clap::crate_version!())]
675#[allow(clippy::large_enum_variant)]
676pub(crate) enum Opt {
677 #[clap(alias = "update")]
690 Upgrade(UpgradeOpts),
691 Switch(SwitchOpts),
702 #[command(after_help = indoc! {r#"
714 Note on Rollbacks and the `/etc` Directory:
715
716 When you perform a rollback (e.g., with `bootc rollback`), any
717 changes made to files in the `/etc` directory won't carry over
718 to the rolled-back deployment. The `/etc` files will revert
719 to their state from that previous deployment instead.
720
721 This is because `bootc rollback` just reorders the existing
722 deployments. It doesn't create new deployments. The `/etc`
723 merges happen when new deployments are created.
724 "#})]
725 Rollback(RollbackOpts),
726 Edit(EditOpts),
736 Status(StatusOpts),
740 #[clap(alias = "usroverlay")]
744 UsrOverlay,
745 #[clap(subcommand)]
749 Install(InstallOpts),
750 #[clap(subcommand)]
752 Container(ContainerOpts),
753 #[clap(subcommand, hide = true)]
757 Image(ImageOpts),
758 #[clap(hide = true)]
760 ExecInHostMountNamespace {
761 #[clap(trailing_var_arg = true, allow_hyphen_values = true)]
762 args: Vec<OsString>,
763 },
764 #[clap(hide = true)]
766 #[clap(subcommand)]
767 State(StateOpts),
768 #[clap(subcommand)]
769 #[clap(hide = true)]
770 Internals(InternalsOpts),
771 ComposefsFinalizeStaged,
772 #[clap(hide = true)]
774 ConfigDiff,
775 #[clap(hide = true)]
779 Completion {
780 #[clap(value_enum)]
782 shell: clap_complete::aot::Shell,
783 },
784 #[clap(hide = true)]
785 DeleteDeployment {
786 depl_id: String,
787 },
788}
789
790#[context("Ensuring mountns")]
795pub(crate) fn ensure_self_unshared_mount_namespace() -> Result<()> {
796 let uid = rustix::process::getuid();
797 if !uid.is_root() {
798 tracing::debug!("Not root, assuming no need to unshare");
799 return Ok(());
800 }
801 let recurse_env = "_ostree_unshared";
802 let ns_pid1 = std::fs::read_link("/proc/1/ns/mnt").context("Reading /proc/1/ns/mnt")?;
803 let ns_self = std::fs::read_link("/proc/self/ns/mnt").context("Reading /proc/self/ns/mnt")?;
804 if ns_pid1 != ns_self {
806 tracing::debug!("Already in a mount namespace");
807 return Ok(());
808 }
809 if std::env::var_os(recurse_env).is_some() {
810 let am_pid1 = rustix::process::getpid().is_init();
811 if am_pid1 {
812 tracing::debug!("We are pid 1");
813 return Ok(());
814 } else {
815 anyhow::bail!("Failed to unshare mount namespace");
816 }
817 }
818 bootc_utils::reexec::reexec_with_guardenv(recurse_env, &["unshare", "-m", "--"])
819}
820
821#[context("Initializing storage")]
824pub(crate) async fn get_storage() -> Result<crate::store::BootedStorage> {
825 let env = crate::store::Environment::detect()?;
826 prepare_for_write()?;
829 let r = BootedStorage::new(env)
830 .await?
831 .ok_or_else(|| anyhow!("System not booted via bootc"))?;
832 Ok(r)
833}
834
835#[context("Querying root privilege")]
836pub(crate) fn require_root(is_container: bool) -> Result<()> {
837 ensure!(
838 rustix::process::getuid().is_root(),
839 if is_container {
840 "The user inside the container from which you are running this command must be root"
841 } else {
842 "This command must be executed as the root user"
843 }
844 );
845
846 ensure!(
847 rustix::thread::capability_is_in_bounding_set(rustix::thread::CapabilitySet::SYS_ADMIN)?,
848 if is_container {
849 "The container must be executed with full privileges (e.g. --privileged flag)"
850 } else {
851 "This command requires full root privileges (CAP_SYS_ADMIN)"
852 }
853 );
854
855 tracing::trace!("Verified uid 0 with CAP_SYS_ADMIN");
856
857 Ok(())
858}
859
860fn has_soft_reboot_capability(deployment: Option<&crate::spec::BootEntry>) -> bool {
862 deployment.map(|d| d.soft_reboot_capable).unwrap_or(false)
863}
864
865#[context("Preparing soft reboot")]
867fn prepare_soft_reboot(sysroot: &SysrootLock, deployment: &ostree::Deployment) -> Result<()> {
868 let cancellable = ostree::gio::Cancellable::NONE;
869 sysroot
870 .deployment_set_soft_reboot(deployment, false, cancellable)
871 .context("Failed to prepare soft-reboot")?;
872 Ok(())
873}
874
875#[context("Handling soft reboot")]
877fn handle_soft_reboot<F>(
878 soft_reboot_mode: Option<SoftRebootMode>,
879 entry: Option<&crate::spec::BootEntry>,
880 deployment_type: &str,
881 execute_soft_reboot: F,
882) -> Result<()>
883where
884 F: FnOnce() -> Result<()>,
885{
886 let Some(mode) = soft_reboot_mode else {
887 return Ok(());
888 };
889
890 let can_soft_reboot = has_soft_reboot_capability(entry);
891 match mode {
892 SoftRebootMode::Required => {
893 if can_soft_reboot {
894 execute_soft_reboot()?;
895 } else {
896 anyhow::bail!(
897 "Soft reboot was required but {} deployment is not soft-reboot capable",
898 deployment_type
899 );
900 }
901 }
902 SoftRebootMode::Auto => {
903 if can_soft_reboot {
904 execute_soft_reboot()?;
905 }
906 }
907 }
908 Ok(())
909}
910
911#[context("Handling staged soft reboot")]
913fn handle_staged_soft_reboot(
914 booted_ostree: &BootedOstree<'_>,
915 soft_reboot_mode: Option<SoftRebootMode>,
916 host: &crate::spec::Host,
917) -> Result<()> {
918 handle_soft_reboot(
919 soft_reboot_mode,
920 host.status.staged.as_ref(),
921 "staged",
922 || soft_reboot_staged(booted_ostree.sysroot),
923 )
924}
925
926#[context("Soft reboot staged deployment")]
928fn soft_reboot_staged(sysroot: &SysrootLock) -> Result<()> {
929 println!("Staged deployment is soft-reboot capable, preparing for soft-reboot...");
930
931 let deployments_list = sysroot.deployments();
932 let staged_deployment = deployments_list
933 .iter()
934 .find(|d| d.is_staged())
935 .ok_or_else(|| anyhow::anyhow!("Failed to find staged deployment"))?;
936
937 prepare_soft_reboot(sysroot, staged_deployment)?;
938 Ok(())
939}
940
941#[context("Soft reboot rollback deployment")]
943fn soft_reboot_rollback(booted_ostree: &BootedOstree<'_>) -> Result<()> {
944 println!("Rollback deployment is soft-reboot capable, preparing for soft-reboot...");
945
946 let deployments_list = booted_ostree.sysroot.deployments();
947 let target_deployment = deployments_list
948 .first()
949 .ok_or_else(|| anyhow::anyhow!("No rollback deployment found!"))?;
950
951 prepare_soft_reboot(booted_ostree.sysroot, target_deployment)
952}
953
954#[context("Preparing for write")]
958pub(crate) fn prepare_for_write() -> Result<()> {
959 use std::sync::atomic::{AtomicBool, Ordering};
960
961 static ENTERED: AtomicBool = AtomicBool::new(false);
967 if ENTERED.load(Ordering::SeqCst) {
968 return Ok(());
969 }
970 if ostree_ext::container_utils::running_in_container() {
971 anyhow::bail!("Detected container; this command requires a booted host system.");
972 }
973 crate::cli::require_root(false)?;
974 ensure_self_unshared_mount_namespace()?;
975 if crate::lsm::selinux_enabled()? && !crate::lsm::selinux_ensure_install()? {
976 tracing::debug!("Do not have install_t capabilities");
977 }
978 ENTERED.store(true, Ordering::SeqCst);
979 Ok(())
980}
981
982#[context("Upgrading")]
984async fn upgrade(
985 opts: UpgradeOpts,
986 storage: &Storage,
987 booted_ostree: &BootedOstree<'_>,
988) -> Result<()> {
989 let repo = &booted_ostree.repo();
990
991 let host = crate::status::get_status(booted_ostree)?.1;
992 let imgref = host.spec.image.as_ref();
993 let prog: ProgressWriter = opts.progress.try_into()?;
994
995 if imgref.is_none() {
997 let booted_incompatible = host.status.booted.as_ref().is_some_and(|b| b.incompatible);
998
999 let staged_incompatible = host.status.staged.as_ref().is_some_and(|b| b.incompatible);
1000
1001 if booted_incompatible || staged_incompatible {
1002 return Err(anyhow::anyhow!(
1003 "Deployment contains local rpm-ostree modifications; cannot upgrade via bootc. You can run `rpm-ostree reset` to undo the modifications."
1004 ));
1005 }
1006 }
1007
1008 let spec = RequiredHostSpec::from_spec(&host.spec)?;
1009 let booted_image = host
1010 .status
1011 .booted
1012 .as_ref()
1013 .map(|b| b.query_image(repo))
1014 .transpose()?
1015 .flatten();
1016 let imgref = imgref.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
1017 let staged = host.status.staged.as_ref();
1019 let staged_image = staged.as_ref().and_then(|s| s.image.as_ref());
1020 let mut changed = false;
1021
1022 if opts.from_downloaded {
1024 let ostree = storage.get_ostree()?;
1025 let staged_deployment = ostree
1026 .staged_deployment()
1027 .ok_or_else(|| anyhow::anyhow!("No staged deployment found"))?;
1028
1029 if staged_deployment.is_finalization_locked() {
1030 ostree.change_finalization(&staged_deployment)?;
1031 println!("Staged deployment will now be applied on reboot");
1032 } else {
1033 println!("Staged deployment is already set to apply on reboot");
1034 }
1035
1036 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &host)?;
1037 if opts.apply {
1038 crate::reboot::reboot()?;
1039 }
1040 return Ok(());
1041 }
1042
1043 if opts.check {
1044 let imgref = imgref.clone().into();
1045 let mut imp = crate::deploy::new_importer(repo, &imgref).await?;
1046 match imp.prepare().await? {
1047 PrepareResult::AlreadyPresent(_) => {
1048 println!("No changes in: {imgref:#}");
1049 }
1050 PrepareResult::Ready(r) => {
1051 crate::deploy::check_bootc_label(&r.config);
1052 println!("Update available for: {imgref:#}");
1053 if let Some(version) = r.version() {
1054 println!(" Version: {version}");
1055 }
1056 println!(" Digest: {}", r.manifest_digest);
1057 changed = true;
1058 if let Some(previous_image) = booted_image.as_ref() {
1059 let diff =
1060 ostree_container::ManifestDiff::new(&previous_image.manifest, &r.manifest);
1061 diff.print();
1062 }
1063 }
1064 }
1065 } else {
1066 let use_unified = crate::deploy::image_exists_in_unified_storage(storage, imgref).await?;
1068
1069 let fetched = if use_unified {
1070 crate::deploy::pull_unified(repo, imgref, None, opts.quiet, prog.clone(), storage)
1071 .await?
1072 } else {
1073 crate::deploy::pull(repo, imgref, None, opts.quiet, prog.clone()).await?
1074 };
1075 let staged_digest = staged_image.map(|s| s.digest().expect("valid digest in status"));
1076 let fetched_digest = &fetched.manifest_digest;
1077 tracing::debug!("staged: {staged_digest:?}");
1078 tracing::debug!("fetched: {fetched_digest}");
1079 let staged_unchanged = staged_digest
1080 .as_ref()
1081 .map(|d| d == fetched_digest)
1082 .unwrap_or_default();
1083 let booted_unchanged = booted_image
1084 .as_ref()
1085 .map(|img| &img.manifest_digest == fetched_digest)
1086 .unwrap_or_default();
1087 if staged_unchanged {
1088 let staged_deployment = storage.get_ostree()?.staged_deployment();
1089 let mut download_only_changed = false;
1090
1091 if let Some(staged) = staged_deployment {
1092 if opts.download_only {
1094 if !staged.is_finalization_locked() {
1096 storage.get_ostree()?.change_finalization(&staged)?;
1097 println!("Image downloaded, but will not be applied on reboot");
1098 download_only_changed = true;
1099 }
1100 } else if !opts.check {
1101 if staged.is_finalization_locked() {
1104 storage.get_ostree()?.change_finalization(&staged)?;
1105 println!("Staged deployment will now be applied on reboot");
1106 download_only_changed = true;
1107 }
1108 }
1109 } else if opts.download_only || opts.apply {
1110 anyhow::bail!("No staged deployment found");
1111 }
1112
1113 if !download_only_changed {
1114 println!("Staged update present, not changed");
1115 }
1116
1117 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &host)?;
1118 if opts.apply {
1119 crate::reboot::reboot()?;
1120 }
1121 } else if booted_unchanged {
1122 println!("No update available.")
1123 } else {
1124 let stateroot = booted_ostree.stateroot();
1125 let from = MergeState::from_stateroot(storage, &stateroot)?;
1126 crate::deploy::stage(
1127 storage,
1128 from,
1129 &fetched,
1130 &spec,
1131 prog.clone(),
1132 opts.download_only,
1133 )
1134 .await?;
1135 changed = true;
1136 if let Some(prev) = booted_image.as_ref() {
1137 if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
1138 let diff =
1139 ostree_container::ManifestDiff::new(&prev.manifest, &fetched_manifest);
1140 diff.print();
1141 }
1142 }
1143 }
1144 }
1145 if changed {
1146 storage.update_mtime()?;
1147
1148 if opts.soft_reboot.is_some() {
1149 let updated_host = crate::status::get_status(booted_ostree)?.1;
1152 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &updated_host)?;
1153 }
1154
1155 if opts.apply {
1156 crate::reboot::reboot()?;
1157 }
1158 } else {
1159 tracing::debug!("No changes");
1160 }
1161
1162 Ok(())
1163}
1164
1165pub(crate) fn imgref_for_switch(opts: &SwitchOpts) -> Result<ImageReference> {
1166 let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
1167 let imgref = ostree_container::ImageReference {
1168 transport,
1169 name: opts.target.to_string(),
1170 };
1171 let sigverify = sigpolicy_from_opt(opts.enforce_container_sigpolicy);
1172 let target = ostree_container::OstreeImageReference { sigverify, imgref };
1173 let target = ImageReference::from(target);
1174
1175 return Ok(target);
1176}
1177
1178#[context("Switching (ostree)")]
1180async fn switch_ostree(
1181 opts: SwitchOpts,
1182 storage: &Storage,
1183 booted_ostree: &BootedOstree<'_>,
1184) -> Result<()> {
1185 let target = imgref_for_switch(&opts)?;
1186 let prog: ProgressWriter = opts.progress.try_into()?;
1187 let cancellable = gio::Cancellable::NONE;
1188
1189 let repo = &booted_ostree.repo();
1190 let (_, host) = crate::status::get_status(booted_ostree)?;
1191
1192 let new_spec = {
1193 let mut new_spec = host.spec.clone();
1194 new_spec.image = Some(target.clone());
1195 new_spec
1196 };
1197
1198 if new_spec == host.spec {
1199 println!("Image specification is unchanged.");
1200 return Ok(());
1201 }
1202
1203 const SWITCH_JOURNAL_ID: &str = "7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1";
1205 let old_image = host
1206 .spec
1207 .image
1208 .as_ref()
1209 .map(|i| i.image.as_str())
1210 .unwrap_or("none");
1211
1212 tracing::info!(
1213 message_id = SWITCH_JOURNAL_ID,
1214 bootc.old_image_reference = old_image,
1215 bootc.new_image_reference = &target.image,
1216 bootc.new_image_transport = &target.transport,
1217 "Switching from image {} to {}",
1218 old_image,
1219 target.image
1220 );
1221
1222 let new_spec = RequiredHostSpec::from_spec(&new_spec)?;
1223
1224 let use_unified = if opts.unified_storage_exp {
1228 true
1229 } else {
1230 crate::deploy::image_exists_in_unified_storage(storage, &target).await?
1231 };
1232
1233 let fetched = if use_unified {
1234 crate::deploy::pull_unified(repo, &target, None, opts.quiet, prog.clone(), storage).await?
1235 } else {
1236 crate::deploy::pull(repo, &target, None, opts.quiet, prog.clone()).await?
1237 };
1238
1239 if !opts.retain {
1240 if let Some(booted_origin) = booted_ostree.deployment.origin() {
1242 if let Some(ostree_ref) = booted_origin.optional_string("origin", "refspec")? {
1243 let (remote, ostree_ref) =
1244 ostree::parse_refspec(&ostree_ref).context("Failed to parse ostree ref")?;
1245 repo.set_ref_immediate(remote.as_deref(), &ostree_ref, None, cancellable)?;
1246 }
1247 }
1248 }
1249
1250 let stateroot = booted_ostree.stateroot();
1251 let from = MergeState::from_stateroot(storage, &stateroot)?;
1252 crate::deploy::stage(storage, from, &fetched, &new_spec, prog.clone(), false).await?;
1253
1254 storage.update_mtime()?;
1255
1256 if opts.soft_reboot.is_some() {
1257 let updated_host = crate::status::get_status(booted_ostree)?.1;
1260 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &updated_host)?;
1261 }
1262
1263 if opts.apply {
1264 crate::reboot::reboot()?;
1265 }
1266
1267 Ok(())
1268}
1269
1270#[context("Switching")]
1272async fn switch(opts: SwitchOpts) -> Result<()> {
1273 if opts.mutate_in_place {
1277 let target = imgref_for_switch(&opts)?;
1278 let deployid = {
1279 let target = target.clone();
1281 let root = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1282 tokio::task::spawn_blocking(move || {
1283 crate::deploy::switch_origin_inplace(&root, &target)
1284 })
1285 .await??
1286 };
1287 println!("Updated {deployid} to pull from {target}");
1288 return Ok(());
1289 }
1290 let storage = &get_storage().await?;
1291 match storage.kind()? {
1292 BootedStorageKind::Ostree(booted_ostree) => {
1293 switch_ostree(opts, storage, &booted_ostree).await
1294 }
1295 BootedStorageKind::Composefs(booted_cfs) => {
1296 switch_composefs(opts, storage, &booted_cfs).await
1297 }
1298 }
1299}
1300
1301#[context("Rollback (ostree)")]
1303async fn rollback_ostree(
1304 opts: &RollbackOpts,
1305 storage: &Storage,
1306 booted_ostree: &BootedOstree<'_>,
1307) -> Result<()> {
1308 crate::deploy::rollback(storage).await?;
1309
1310 if opts.soft_reboot.is_some() {
1311 let host = crate::status::get_status(booted_ostree)?.1;
1313
1314 handle_soft_reboot(
1315 opts.soft_reboot,
1316 host.status.rollback.as_ref(),
1317 "rollback",
1318 || soft_reboot_rollback(booted_ostree),
1319 )?;
1320 }
1321
1322 Ok(())
1323}
1324
1325#[context("Rollback")]
1327async fn rollback(opts: &RollbackOpts) -> Result<()> {
1328 let storage = &get_storage().await?;
1329 match storage.kind()? {
1330 BootedStorageKind::Ostree(booted_ostree) => {
1331 rollback_ostree(opts, storage, &booted_ostree).await
1332 }
1333 BootedStorageKind::Composefs(booted_cfs) => composefs_rollback(storage, &booted_cfs).await,
1334 }
1335}
1336
1337#[context("Editing spec (ostree)")]
1339async fn edit_ostree(
1340 opts: EditOpts,
1341 storage: &Storage,
1342 booted_ostree: &BootedOstree<'_>,
1343) -> Result<()> {
1344 let repo = &booted_ostree.repo();
1345 let (_, host) = crate::status::get_status(booted_ostree)?;
1346
1347 let new_host: Host = if let Some(filename) = opts.filename {
1348 let mut r = std::io::BufReader::new(std::fs::File::open(filename)?);
1349 serde_yaml::from_reader(&mut r)?
1350 } else {
1351 let tmpf = tempfile::NamedTempFile::with_suffix(".yaml")?;
1352 serde_yaml::to_writer(std::io::BufWriter::new(tmpf.as_file()), &host)?;
1353 crate::utils::spawn_editor(&tmpf)?;
1354 tmpf.as_file().seek(std::io::SeekFrom::Start(0))?;
1355 serde_yaml::from_reader(&mut tmpf.as_file())?
1356 };
1357
1358 if new_host.spec == host.spec {
1359 println!("Edit cancelled, no changes made.");
1360 return Ok(());
1361 }
1362 host.spec.verify_transition(&new_host.spec)?;
1363 let new_spec = RequiredHostSpec::from_spec(&new_host.spec)?;
1364
1365 let prog = ProgressWriter::default();
1366
1367 if host.spec.boot_order != new_host.spec.boot_order {
1370 return crate::deploy::rollback(storage).await;
1371 }
1372
1373 let fetched = crate::deploy::pull(repo, new_spec.image, None, opts.quiet, prog.clone()).await?;
1374
1375 let stateroot = booted_ostree.stateroot();
1378 let from = MergeState::from_stateroot(storage, &stateroot)?;
1379 crate::deploy::stage(storage, from, &fetched, &new_spec, prog.clone(), false).await?;
1380
1381 storage.update_mtime()?;
1382
1383 Ok(())
1384}
1385
1386#[context("Editing spec")]
1388async fn edit(opts: EditOpts) -> Result<()> {
1389 let storage = &get_storage().await?;
1390 match storage.kind()? {
1391 BootedStorageKind::Ostree(booted_ostree) => {
1392 edit_ostree(opts, storage, &booted_ostree).await
1393 }
1394 BootedStorageKind::Composefs(_) => {
1395 anyhow::bail!("Edit is not yet supported for composefs backend")
1396 }
1397 }
1398}
1399
1400async fn usroverlay() -> Result<()> {
1402 Err(Command::new("ostree")
1405 .args(["admin", "unlock"])
1406 .exec()
1407 .into())
1408}
1409
1410#[allow(unsafe_code)]
1413pub fn global_init() -> Result<()> {
1414 ostree::glib::set_prgname(bootc_utils::NAME.into());
1417 if let Err(e) = rustix::thread::set_name(&CString::new(bootc_utils::NAME).unwrap()) {
1418 eprintln!("failed to set name: {e}");
1420 }
1421 ostree::SePolicy::set_null_log();
1423 let am_root = rustix::process::getuid().is_root();
1424 if std::env::var_os("HOME").is_none() && am_root {
1427 unsafe {
1432 std::env::set_var("HOME", "/root");
1433 }
1434 }
1435 Ok(())
1436}
1437
1438pub async fn run_from_iter<I>(args: I) -> Result<()>
1441where
1442 I: IntoIterator,
1443 I::Item: Into<OsString> + Clone,
1444{
1445 run_from_opt(Opt::parse_including_static(args)).await
1446}
1447
1448fn callname_from_argv0(argv0: &OsStr) -> &str {
1452 let default = "bootc";
1453 std::path::Path::new(argv0)
1454 .file_name()
1455 .and_then(|s| s.to_str())
1456 .filter(|s| !s.is_empty())
1457 .unwrap_or(default)
1458}
1459
1460impl Opt {
1461 fn parse_including_static<I>(args: I) -> Self
1464 where
1465 I: IntoIterator,
1466 I::Item: Into<OsString> + Clone,
1467 {
1468 let mut args = args.into_iter();
1469 let first = if let Some(first) = args.next() {
1470 let first: OsString = first.into();
1471 let argv0 = callname_from_argv0(&first);
1472 tracing::debug!("argv0={argv0:?}");
1473 let mapped = match argv0 {
1474 InternalsOpts::GENERATOR_BIN => {
1475 Some(["bootc", "internals", "systemd-generator"].as_slice())
1476 }
1477 "ostree-container" | "ostree-ima-sign" | "ostree-provisional-repair" => {
1478 Some(["bootc", "internals", "ostree-ext"].as_slice())
1479 }
1480 _ => None,
1481 };
1482 if let Some(base_args) = mapped {
1483 let base_args = base_args.iter().map(OsString::from);
1484 return Opt::parse_from(base_args.chain(args.map(|i| i.into())));
1485 }
1486 Some(first)
1487 } else {
1488 None
1489 };
1490 Opt::parse_from(first.into_iter().chain(args.map(|i| i.into())))
1491 }
1492}
1493
1494async fn run_from_opt(opt: Opt) -> Result<()> {
1496 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1497 match opt {
1498 Opt::Upgrade(opts) => {
1499 let storage = &get_storage().await?;
1500 match storage.kind()? {
1501 BootedStorageKind::Ostree(booted_ostree) => {
1502 upgrade(opts, storage, &booted_ostree).await
1503 }
1504 BootedStorageKind::Composefs(booted_cfs) => {
1505 upgrade_composefs(opts, storage, &booted_cfs).await
1506 }
1507 }
1508 }
1509 Opt::Switch(opts) => switch(opts).await,
1510 Opt::Rollback(opts) => {
1511 rollback(&opts).await?;
1512 if opts.apply {
1513 crate::reboot::reboot()?;
1514 }
1515 Ok(())
1516 }
1517 Opt::Edit(opts) => edit(opts).await,
1518 Opt::UsrOverlay => {
1519 use crate::store::Environment;
1520 let env = Environment::detect()?;
1521 match env {
1522 Environment::OstreeBooted => usroverlay().await,
1523 Environment::ComposefsBooted(_) => composefs_usr_overlay(),
1524 _ => anyhow::bail!("usroverlay only applies on booted hosts"),
1525 }
1526 }
1527 Opt::Container(opts) => match opts {
1528 ContainerOpts::Inspect {
1529 rootfs,
1530 json,
1531 format,
1532 } => crate::status::container_inspect(&rootfs, json, format),
1533 ContainerOpts::Lint {
1534 rootfs,
1535 fatal_warnings,
1536 list,
1537 skip,
1538 no_truncate,
1539 } => {
1540 if list {
1541 return lints::lint_list(std::io::stdout().lock());
1542 }
1543 let warnings = if fatal_warnings {
1544 lints::WarningDisposition::FatalWarnings
1545 } else {
1546 lints::WarningDisposition::AllowWarnings
1547 };
1548 let root_type = if rootfs == "/" {
1549 lints::RootType::Running
1550 } else {
1551 lints::RootType::Alternative
1552 };
1553
1554 let root = &Dir::open_ambient_dir(rootfs, cap_std::ambient_authority())?;
1555 let skip = skip.iter().map(|s| s.as_str());
1556 lints::lint(
1557 root,
1558 warnings,
1559 root_type,
1560 skip,
1561 std::io::stdout().lock(),
1562 no_truncate,
1563 )?;
1564 Ok(())
1565 }
1566 ContainerOpts::ComputeComposefsDigest {
1567 path,
1568 write_dumpfile_to,
1569 } => {
1570 let digest = compute_composefs_digest(&path, write_dumpfile_to.as_deref())?;
1571 println!("{digest}");
1572 Ok(())
1573 }
1574 ContainerOpts::ComputeComposefsDigestFromStorage {
1575 write_dumpfile_to,
1576 image,
1577 } => {
1578 let (_td_guard, repo) = new_temp_composefs_repo()?;
1579
1580 let mut proxycfg = crate::deploy::new_proxy_config();
1581
1582 let image = if let Some(image) = image {
1583 image
1584 } else {
1585 let host_container_store = Utf8Path::new("/run/host-container-storage");
1586 let container_info = crate::containerenv::get_container_execution_info(&root)?;
1589 let iid = container_info.imageid;
1590 tracing::debug!("Computing digest of {iid}");
1591
1592 if !host_container_store.try_exists()? {
1593 anyhow::bail!(
1594 "Must be readonly mount of host container store: {host_container_store}"
1595 );
1596 }
1597 let mut cmd = Command::new("skopeo");
1599 set_additional_image_store(&mut cmd, "/run/host-container-storage");
1600 proxycfg.skopeo_cmd = Some(cmd);
1601 iid
1602 };
1603
1604 let imgref = format!("containers-storage:{image}");
1605 let (imgid, verity) = composefs_oci::pull(&repo, &imgref, None, Some(proxycfg))
1606 .await
1607 .context("Pulling image")?;
1608 let imgid = hex::encode(imgid);
1609 let mut fs = composefs_oci::image::create_filesystem(&repo, &imgid, Some(&verity))
1610 .context("Populating fs")?;
1611 fs.transform_for_boot(&repo).context("Preparing for boot")?;
1612 let id = fs.compute_image_id();
1613 println!("{}", id.to_hex());
1614
1615 if let Some(path) = write_dumpfile_to.as_deref() {
1616 let mut w = File::create(path)
1617 .with_context(|| format!("Opening {path}"))
1618 .map(BufWriter::new)?;
1619 dumpfile::write_dumpfile(&mut w, &fs).context("Writing dumpfile")?;
1620 }
1621
1622 Ok(())
1623 }
1624 ContainerOpts::Ukify {
1625 rootfs,
1626 kargs,
1627 args,
1628 } => crate::ukify::build_ukify(&rootfs, &kargs, &args),
1629 },
1630 Opt::Completion { shell } => {
1631 use clap_complete::aot::generate;
1632
1633 let mut cmd = Opt::command();
1634 let mut stdout = std::io::stdout();
1635 let bin_name = "bootc";
1636 generate(shell, &mut cmd, bin_name, &mut stdout);
1637 Ok(())
1638 }
1639 Opt::Image(opts) => match opts {
1640 ImageOpts::List {
1641 list_type,
1642 list_format,
1643 } => crate::image::list_entrypoint(list_type, list_format).await,
1644
1645 ImageOpts::CopyToStorage { source, target } => {
1646 let host = get_host().await?;
1648
1649 let storage = get_storage().await?;
1650
1651 match storage.kind()? {
1652 BootedStorageKind::Ostree(..) => {
1653 crate::image::push_entrypoint(
1654 &storage,
1655 &host,
1656 source.as_deref(),
1657 target.as_deref(),
1658 )
1659 .await
1660 }
1661 BootedStorageKind::Composefs(booted) => {
1662 bootc_composefs::export::export_repo_to_image(
1663 &storage,
1664 &booted,
1665 source.as_deref(),
1666 target.as_deref(),
1667 )
1668 .await
1669 }
1670 }
1671 }
1672 ImageOpts::SetUnified => crate::image::set_unified_entrypoint().await,
1673 ImageOpts::PullFromDefaultStorage { image } => {
1674 let storage = get_storage().await?;
1675 storage
1676 .get_ensure_imgstore()?
1677 .pull_from_host_storage(&image)
1678 .await
1679 }
1680 ImageOpts::Cmd(opt) => {
1681 let storage = get_storage().await?;
1682 let imgstore = storage.get_ensure_imgstore()?;
1683 match opt {
1684 ImageCmdOpts::List { args } => {
1685 crate::image::imgcmd_entrypoint(imgstore, "list", &args).await
1686 }
1687 ImageCmdOpts::Build { args } => {
1688 crate::image::imgcmd_entrypoint(imgstore, "build", &args).await
1689 }
1690 ImageCmdOpts::Pull { args } => {
1691 crate::image::imgcmd_entrypoint(imgstore, "pull", &args).await
1692 }
1693 ImageCmdOpts::Push { args } => {
1694 crate::image::imgcmd_entrypoint(imgstore, "push", &args).await
1695 }
1696 }
1697 }
1698 },
1699 Opt::Install(opts) => match opts {
1700 #[cfg(feature = "install-to-disk")]
1701 InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await,
1702 InstallOpts::ToFilesystem(opts) => {
1703 crate::install::install_to_filesystem(opts, false, crate::install::Cleanup::Skip)
1704 .await
1705 }
1706 InstallOpts::ToExistingRoot(opts) => {
1707 crate::install::install_to_existing_root(opts).await
1708 }
1709 InstallOpts::Reset(opts) => crate::install::install_reset(opts).await,
1710 InstallOpts::PrintConfiguration(opts) => crate::install::print_configuration(opts),
1711 InstallOpts::EnsureCompletion {} => {
1712 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1713 crate::install::completion::run_from_anaconda(rootfs).await
1714 }
1715 InstallOpts::Finalize { root_path } => {
1716 crate::install::install_finalize(&root_path).await
1717 }
1718 },
1719 Opt::ExecInHostMountNamespace { args } => {
1720 crate::install::exec_in_host_mountns(args.as_slice())
1721 }
1722 Opt::Status(opts) => super::status::status(opts).await,
1723 Opt::Internals(opts) => match opts {
1724 InternalsOpts::SystemdGenerator {
1725 normal_dir,
1726 early_dir: _,
1727 late_dir: _,
1728 } => {
1729 let unit_dir = &Dir::open_ambient_dir(normal_dir, cap_std::ambient_authority())?;
1730 crate::generator::generator(root, unit_dir)
1731 }
1732 InternalsOpts::OstreeExt { args } => {
1733 ostree_ext::cli::run_from_iter(["ostree-ext".into()].into_iter().chain(args)).await
1734 }
1735 InternalsOpts::OstreeContainer { args } => {
1736 ostree_ext::cli::run_from_iter(
1737 ["ostree-ext".into(), "container".into()]
1738 .into_iter()
1739 .chain(args),
1740 )
1741 .await
1742 }
1743 InternalsOpts::TestComposefs => {
1744 let storage = get_storage().await?;
1746 let cfs = storage.get_ensure_composefs()?;
1747 let testdata = b"some test data";
1748 let testdata_digest = hex::encode(openssl::sha::sha256(testdata));
1749 let mut w = SplitStreamWriter::new(&cfs, 0);
1750 w.write_inline(testdata);
1751 let object = cfs
1752 .write_stream(w, &testdata_digest, Some("testobject"))?
1753 .to_hex();
1754 assert_eq!(
1755 object,
1756 "dc31ae5d2f637e98d2171821d60d2fcafb8084d6a4bb3bd9cdc7ad41decce6e48f85d5413d22371d36b223945042f53a2a6ab449b8e45d8896ba7d8694a16681"
1757 );
1758 Ok(())
1759 }
1760 InternalsOpts::Fsverity(args) => match args {
1762 FsverityOpts::Measure { path } => {
1763 let fd =
1764 std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
1765 let digest: fsverity::Sha256HashValue = fsverity::measure_verity(&fd)?;
1766 let digest = digest.to_hex();
1767 println!("{digest}");
1768 Ok(())
1769 }
1770 FsverityOpts::Enable { path } => {
1771 let fd =
1772 std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
1773 fsverity::enable_verity_raw::<fsverity::Sha256HashValue>(&fd)?;
1774 Ok(())
1775 }
1776 },
1777 InternalsOpts::Cfs { args } => crate::cfsctl::run_from_iter(args.iter()).await,
1778 InternalsOpts::Reboot => crate::reboot::reboot(),
1779 InternalsOpts::Fsck => {
1780 let storage = &get_storage().await?;
1781 crate::fsck::fsck(&storage, std::io::stdout().lock()).await?;
1782 Ok(())
1783 }
1784 InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root),
1785 InternalsOpts::PrintJsonSchema { of } => {
1786 let schema = match of {
1787 SchemaType::Host => schema_for!(crate::spec::Host),
1788 SchemaType::Progress => schema_for!(crate::progress_jsonl::Event),
1789 };
1790 let mut stdout = std::io::stdout().lock();
1791 serde_json::to_writer_pretty(&mut stdout, &schema)?;
1792 Ok(())
1793 }
1794 InternalsOpts::Cleanup => {
1795 let storage = get_storage().await?;
1796 crate::deploy::cleanup(&storage).await
1797 }
1798 InternalsOpts::Relabel { as_path, path } => {
1799 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1800 let path = path.strip_prefix("/")?;
1801 let sepolicy =
1802 &ostree::SePolicy::new(&gio::File::for_path("/"), gio::Cancellable::NONE)?;
1803 crate::lsm::relabel_recurse(root, path, as_path.as_deref(), sepolicy)?;
1804 Ok(())
1805 }
1806 InternalsOpts::BootcInstallCompletion { sysroot, stateroot } => {
1807 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1808 crate::install::completion::run_from_ostree(rootfs, &sysroot, &stateroot).await
1809 }
1810 InternalsOpts::LoopbackCleanupHelper { device } => {
1811 crate::blockdev::run_loopback_cleanup_helper(&device).await
1812 }
1813 InternalsOpts::AllocateCleanupLoopback { file_path: _ } => {
1814 let temp_file =
1816 tempfile::NamedTempFile::new().context("Failed to create temporary file")?;
1817 let temp_path = temp_file.path();
1818
1819 let loopback = crate::blockdev::LoopbackDevice::new(temp_path)
1821 .context("Failed to create loopback device")?;
1822
1823 println!("Created loopback device: {}", loopback.path());
1824
1825 loopback
1827 .close()
1828 .context("Failed to close loopback device")?;
1829
1830 println!("Successfully closed loopback device");
1831 Ok(())
1832 }
1833 #[cfg(feature = "rhsm")]
1834 InternalsOpts::PublishRhsmFacts => crate::rhsm::publish_facts(&root).await,
1835 #[cfg(feature = "docgen")]
1836 InternalsOpts::DumpCliJson => {
1837 use clap::CommandFactory;
1838 let cmd = Opt::command();
1839 let json = crate::cli_json::dump_cli_json(&cmd)?;
1840 println!("{}", json);
1841 Ok(())
1842 }
1843 InternalsOpts::DirDiff {
1844 pristine_etc,
1845 current_etc,
1846 new_etc,
1847 merge,
1848 } => {
1849 let pristine_etc =
1850 Dir::open_ambient_dir(pristine_etc, cap_std::ambient_authority())?;
1851 let current_etc = Dir::open_ambient_dir(current_etc, cap_std::ambient_authority())?;
1852 let new_etc = Dir::open_ambient_dir(new_etc, cap_std::ambient_authority())?;
1853
1854 let (p, c, n) =
1855 etc_merge::traverse_etc(&pristine_etc, ¤t_etc, Some(&new_etc))?;
1856
1857 let n = n
1858 .as_ref()
1859 .ok_or_else(|| anyhow::anyhow!("Failed to get new directory tree"))?;
1860
1861 let diff = compute_diff(&p, &c, &n)?;
1862 print_diff(&diff, &mut std::io::stdout());
1863
1864 if merge {
1865 etc_merge::merge(¤t_etc, &c, &new_etc, &n, &diff)?;
1866 }
1867
1868 Ok(())
1869 }
1870 InternalsOpts::PrepSoftReboot {
1871 deployment,
1872 reboot,
1873 reset,
1874 } => {
1875 let storage = &get_storage().await?;
1876
1877 match storage.kind()? {
1878 BootedStorageKind::Ostree(..) => {
1879 anyhow::bail!("soft-reboot only implemented for composefs")
1881 }
1882
1883 BootedStorageKind::Composefs(booted_cfs) => {
1884 if reset {
1885 return reset_soft_reboot();
1886 }
1887
1888 prepare_soft_reboot_composefs(
1889 &storage,
1890 &booted_cfs,
1891 deployment.as_deref(),
1892 SoftRebootMode::Required,
1893 reboot,
1894 )
1895 .await
1896 }
1897 }
1898 }
1899 },
1900 Opt::State(opts) => match opts {
1901 StateOpts::WipeOstree => {
1902 let sysroot = ostree::Sysroot::new_default();
1903 sysroot.load(gio::Cancellable::NONE)?;
1904 crate::deploy::wipe_ostree(sysroot).await?;
1905 Ok(())
1906 }
1907 },
1908
1909 Opt::ComposefsFinalizeStaged => {
1910 let storage = &get_storage().await?;
1911 match storage.kind()? {
1912 BootedStorageKind::Ostree(_) => {
1913 anyhow::bail!("ComposefsFinalizeStaged is only supported for composefs backend")
1914 }
1915 BootedStorageKind::Composefs(booted_cfs) => {
1916 composefs_backend_finalize(storage, &booted_cfs).await
1917 }
1918 }
1919 }
1920
1921 Opt::ConfigDiff => {
1922 let storage = &get_storage().await?;
1923 match storage.kind()? {
1924 BootedStorageKind::Ostree(_) => {
1925 anyhow::bail!("ConfigDiff is only supported for composefs backend")
1926 }
1927 BootedStorageKind::Composefs(booted_cfs) => {
1928 get_etc_diff(storage, &booted_cfs).await
1929 }
1930 }
1931 }
1932
1933 Opt::DeleteDeployment { depl_id } => {
1934 let storage = &get_storage().await?;
1935 match storage.kind()? {
1936 BootedStorageKind::Ostree(_) => {
1937 anyhow::bail!("DeleteDeployment is only supported for composefs backend")
1938 }
1939 BootedStorageKind::Composefs(booted_cfs) => {
1940 delete_composefs_deployment(&depl_id, storage, &booted_cfs).await
1941 }
1942 }
1943 }
1944 }
1945}
1946
1947#[cfg(test)]
1948mod tests {
1949 use super::*;
1950
1951 #[test]
1952 fn test_callname() {
1953 use std::os::unix::ffi::OsStrExt;
1954
1955 let mapped_cases = [
1957 ("", "bootc"),
1958 ("/foo/bar", "bar"),
1959 ("/foo/bar/", "bar"),
1960 ("foo/bar", "bar"),
1961 ("../foo/bar", "bar"),
1962 ("usr/bin/ostree-container", "ostree-container"),
1963 ];
1964 for (input, output) in mapped_cases {
1965 assert_eq!(
1966 output,
1967 callname_from_argv0(OsStr::new(input)),
1968 "Handling mapped case {input}"
1969 );
1970 }
1971
1972 assert_eq!("bootc", callname_from_argv0(OsStr::from_bytes(b"foo\x80")));
1974
1975 let ident_cases = ["foo", "bootc"];
1977 for case in ident_cases {
1978 assert_eq!(
1979 case,
1980 callname_from_argv0(OsStr::new(case)),
1981 "Handling ident case {case}"
1982 );
1983 }
1984 }
1985
1986 #[test]
1987 fn test_parse_install_args() {
1988 let o = Opt::try_parse_from([
1990 "bootc",
1991 "install",
1992 "to-filesystem",
1993 "--target-no-signature-verification",
1994 "/target",
1995 ])
1996 .unwrap();
1997 let o = match o {
1998 Opt::Install(InstallOpts::ToFilesystem(fsopts)) => fsopts,
1999 o => panic!("Expected filesystem opts, not {o:?}"),
2000 };
2001 assert!(o.target_opts.target_no_signature_verification);
2002 assert_eq!(o.filesystem_opts.root_path.as_str(), "/target");
2003 assert_eq!(
2005 o.config_opts.bound_images,
2006 crate::install::BoundImagesOpt::Stored
2007 );
2008 }
2009
2010 #[test]
2011 fn test_parse_opts() {
2012 assert!(matches!(
2013 Opt::parse_including_static(["bootc", "status"]),
2014 Opt::Status(StatusOpts {
2015 json: false,
2016 format: None,
2017 format_version: None,
2018 booted: false,
2019 verbose: false
2020 })
2021 ));
2022 assert!(matches!(
2023 Opt::parse_including_static(["bootc", "status", "--format-version=0"]),
2024 Opt::Status(StatusOpts {
2025 format_version: Some(0),
2026 ..
2027 })
2028 ));
2029
2030 assert!(matches!(
2032 Opt::parse_including_static(["bootc", "status", "--verbose"]),
2033 Opt::Status(StatusOpts { verbose: true, .. })
2034 ));
2035
2036 assert!(matches!(
2038 Opt::parse_including_static(["bootc", "status", "-v"]),
2039 Opt::Status(StatusOpts { verbose: true, .. })
2040 ));
2041 }
2042
2043 #[test]
2044 fn test_parse_generator() {
2045 assert!(matches!(
2046 Opt::parse_including_static([
2047 "/usr/lib/systemd/system/bootc-systemd-generator",
2048 "/run/systemd/system"
2049 ]),
2050 Opt::Internals(InternalsOpts::SystemdGenerator { normal_dir, .. }) if normal_dir == "/run/systemd/system"
2051 ));
2052 }
2053
2054 #[test]
2055 fn test_parse_ostree_ext() {
2056 assert!(matches!(
2057 Opt::parse_including_static(["bootc", "internals", "ostree-container"]),
2058 Opt::Internals(InternalsOpts::OstreeContainer { .. })
2059 ));
2060
2061 fn peel(o: Opt) -> Vec<OsString> {
2062 match o {
2063 Opt::Internals(InternalsOpts::OstreeExt { args }) => args,
2064 o => panic!("unexpected {o:?}"),
2065 }
2066 }
2067 let args = peel(Opt::parse_including_static([
2068 "/usr/libexec/libostree/ext/ostree-ima-sign",
2069 "ima-sign",
2070 "--repo=foo",
2071 "foo",
2072 "bar",
2073 "baz",
2074 ]));
2075 assert_eq!(
2076 args.as_slice(),
2077 ["ima-sign", "--repo=foo", "foo", "bar", "baz"]
2078 );
2079
2080 let args = peel(Opt::parse_including_static([
2081 "/usr/libexec/libostree/ext/ostree-container",
2082 "container",
2083 "image",
2084 "pull",
2085 ]));
2086 assert_eq!(args.as_slice(), ["container", "image", "pull"]);
2087 }
2088
2089 #[test]
2090 fn test_generate_completion_scripts_contain_commands() {
2091 use clap_complete::aot::{Shell, generate};
2092
2093 let want = ["install", "upgrade"];
2102
2103 for shell in [Shell::Bash, Shell::Zsh, Shell::Fish] {
2104 let mut cmd = Opt::command();
2105 let mut buf = Vec::new();
2106 generate(shell, &mut cmd, "bootc", &mut buf);
2107 let s = String::from_utf8(buf).expect("completion should be utf8");
2108 for w in &want {
2109 assert!(s.contains(w), "{shell:?} completion missing {w}");
2110 }
2111 }
2112 }
2113}