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