1
0
mirror of https://github.com/containers/bootc.git synced 2026-02-06 09:45:32 +01:00
Files
bootc/lib/src/cli.rs
ckyrouac 85b2419f09 install: Add cleanup option to install to-existing-root
When set, the bootc-destructive-cleanup flag is added to /sysroot/etc
which enables the bootc-destructive-cleanup systemd service to remove
the previous installation's rpm packages and podman containers/images.

The service is only installed on fedora based systems.

Signed-off-by: ckyrouac <ckyrouac@redhat.com>
2025-05-01 18:28:27 -04:00

1389 lines
51 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! # Bootable container image CLI
//!
//! Command line tool to manage bootable ostree-based containers.
use std::ffi::{CString, OsStr, OsString};
use std::io::Seek;
use std::os::unix::process::CommandExt;
use std::process::Command;
use anyhow::{ensure, Context, Result};
use camino::Utf8PathBuf;
use cap_std_ext::cap_std;
use cap_std_ext::cap_std::fs::Dir;
use clap::Parser;
use clap::ValueEnum;
use fn_error_context::context;
use indoc::indoc;
use ostree::gio;
use ostree_container::store::PrepareResult;
use ostree_ext::composefs::fsverity;
use ostree_ext::container as ostree_container;
use ostree_ext::container_utils::ostree_booted;
use ostree_ext::keyfileext::KeyFileExt;
use ostree_ext::ostree;
use schemars::schema_for;
use serde::{Deserialize, Serialize};
use crate::deploy::RequiredHostSpec;
use crate::lints;
use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
use crate::spec::Host;
use crate::spec::ImageReference;
use crate::utils::sigpolicy_from_opt;
/// Shared progress options
#[derive(Debug, Parser, PartialEq, Eq)]
pub(crate) struct ProgressOptions {
/// File descriptor number which must refer to an open pipe (anonymous or named).
///
/// Interactive progress will be written to this file descriptor as "JSON lines"
/// format, where each value is separated by a newline.
#[clap(long, hide = true)]
pub(crate) progress_fd: Option<RawProgressFd>,
}
impl TryFrom<ProgressOptions> for ProgressWriter {
type Error = anyhow::Error;
fn try_from(value: ProgressOptions) -> Result<Self> {
let r = value
.progress_fd
.map(TryInto::try_into)
.transpose()?
.unwrap_or_default();
Ok(r)
}
}
/// Perform an upgrade operation
#[derive(Debug, Parser, PartialEq, Eq)]
pub(crate) struct UpgradeOpts {
/// Don't display progress
#[clap(long)]
pub(crate) quiet: bool,
/// Check if an update is available without applying it.
///
/// This only downloads an updated manifest and image configuration (i.e. typically kilobyte-sized metadata)
/// as opposed to the image layers.
#[clap(long, conflicts_with = "apply")]
pub(crate) check: bool,
/// Restart or reboot into the new target image.
///
/// Currently, this option always reboots. In the future this command
/// will detect the case where no kernel changes are queued, and perform
/// a userspace-only restart.
#[clap(long, conflicts_with = "check")]
pub(crate) apply: bool,
#[clap(flatten)]
pub(crate) progress: ProgressOptions,
}
/// Perform an switch operation
#[derive(Debug, Parser, PartialEq, Eq)]
pub(crate) struct SwitchOpts {
/// Don't display progress
#[clap(long)]
pub(crate) quiet: bool,
/// Restart or reboot into the new target image.
///
/// Currently, this option always reboots. In the future this command
/// will detect the case where no kernel changes are queued, and perform
/// a userspace-only restart.
#[clap(long)]
pub(crate) apply: bool,
/// The transport; e.g. oci, oci-archive, containers-storage. Defaults to `registry`.
#[clap(long, default_value = "registry")]
pub(crate) transport: String,
/// This argument is deprecated and does nothing.
#[clap(long, hide = true)]
pub(crate) no_signature_verification: bool,
/// This is the inverse of the previous `--target-no-signature-verification` (which is now
/// a no-op).
///
/// Enabling this option enforces that `/etc/containers/policy.json` includes a
/// default policy which requires signatures.
#[clap(long)]
pub(crate) enforce_container_sigpolicy: bool,
/// Don't create a new deployment, but directly mutate the booted state.
/// This is hidden because it's not something we generally expect to be done,
/// but this can be used in e.g. Anaconda %post to fixup
#[clap(long, hide = true)]
pub(crate) mutate_in_place: bool,
/// Retain reference to currently booted image
#[clap(long)]
pub(crate) retain: bool,
/// Target image to use for the next boot.
pub(crate) target: String,
#[clap(flatten)]
pub(crate) progress: ProgressOptions,
}
/// Options controlling rollback
#[derive(Debug, Parser, PartialEq, Eq)]
pub(crate) struct RollbackOpts {
/// Restart or reboot into the rollback image.
///
/// Currently, this option always reboots. In the future this command
/// will detect the case where no kernel changes are queued, and perform
/// a userspace-only restart.
#[clap(long)]
pub(crate) apply: bool,
}
/// Perform an edit operation
#[derive(Debug, Parser, PartialEq, Eq)]
pub(crate) struct EditOpts {
/// Use filename to edit system specification
#[clap(long, short = 'f')]
pub(crate) filename: Option<String>,
/// Don't display progress
#[clap(long)]
pub(crate) quiet: bool,
}
#[derive(Debug, Clone, ValueEnum, PartialEq, Eq)]
#[clap(rename_all = "lowercase")]
pub(crate) enum OutputFormat {
/// Output in Human Readable format.
HumanReadable,
/// Output in YAML format.
Yaml,
/// Output in JSON format.
Json,
}
/// Perform an status operation
#[derive(Debug, Parser, PartialEq, Eq)]
pub(crate) struct StatusOpts {
/// Output in JSON format.
///
/// Superceded by the `format` option.
#[clap(long, hide = true)]
pub(crate) json: bool,
/// The output format.
#[clap(long)]
pub(crate) format: Option<OutputFormat>,
/// The desired format version. There is currently one supported
/// version, which is exposed as both `0` and `1`. Pass this
/// option to explicitly request it; it is possible that another future
/// version 2 or newer will be supported in the future.
#[clap(long)]
pub(crate) format_version: Option<u32>,
/// Only display status for the booted deployment.
#[clap(long)]
pub(crate) booted: bool,
}
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum InstallOpts {
/// Install to the target block device.
///
/// This command must be invoked inside of the container, which will be
/// installed. The container must be run in `--privileged` mode, and hence
/// will be able to see all block devices on the system.
///
/// The default storage layout uses the root filesystem type configured
/// in the container image, alongside any required system partitions such as
/// the EFI system partition. Use `install to-filesystem` for anything more
/// complex such as RAID, LVM, LUKS etc.
#[cfg(feature = "install-to-disk")]
ToDisk(crate::install::InstallToDiskOpts),
/// Install to an externally created filesystem structure.
///
/// In this variant of installation, the root filesystem alongside any necessary
/// platform partitions (such as the EFI system partition) are prepared and mounted by an
/// external tool or script. The root filesystem is currently expected to be empty
/// by default.
ToFilesystem(crate::install::InstallToFilesystemOpts),
/// Install to the host root filesystem.
///
/// This is a variant of `install to-filesystem` that is designed to install "alongside"
/// the running host root filesystem. Currently, the host root filesystem's `/boot` partition
/// will be wiped, but the content of the existing root will otherwise be retained, and will
/// need to be cleaned up if desired when rebooted into the new root.
ToExistingRoot(crate::install::InstallToExistingRootOpts),
/// Execute this as the penultimate step of an installation using `install to-filesystem`.
///
Finalize {
/// Path to the mounted root filesystem.
root_path: Utf8PathBuf,
},
/// Intended for use in environments that are performing an ostree-based installation, not bootc.
///
/// In this scenario the installation may be missing bootc specific features such as
/// kernel arguments, logically bound images and more. This command can be used to attempt
/// to reconcile. At the current time, the only tested environment is Anaconda using `ostreecontainer`
/// and it is recommended to avoid usage outside of that environment. Instead, ensure your
/// code is using `bootc install to-filesystem` from the start.
EnsureCompletion {},
/// Output JSON to stdout that contains the merged installation configuration
/// as it may be relevant to calling processes using `install to-filesystem`
/// that in particular want to discover the desired root filesystem type from the container image.
///
/// At the current time, the only output key is `root-fs-type` which is a string-valued
/// filesystem name suitable for passing to `mkfs.$type`.
PrintConfiguration,
}
/// Options for man page generation
#[derive(Debug, Parser, PartialEq, Eq)]
pub(crate) struct ManOpts {
#[clap(long)]
/// Output to this directory
pub(crate) directory: Utf8PathBuf,
}
/// Subcommands which can be executed as part of a container build.
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum ContainerOpts {
/// Perform relatively inexpensive static analysis checks as part of a container
/// build.
///
/// This is intended to be invoked via e.g. `RUN bootc container lint` as part
/// of a build process; it will error if any problems are detected.
Lint {
/// Operate on the provided rootfs.
#[clap(long, default_value = "/")]
rootfs: Utf8PathBuf,
/// Make warnings fatal.
#[clap(long)]
fatal_warnings: bool,
/// Instead of executing the lints, just print all available lints.
/// At the current time, this will output in YAML format because it's
/// reasonably human friendly. However, there is no commitment to
/// maintaining this exact format; do not parse it via code or scripts.
#[clap(long)]
list: bool,
/// Skip checking the targeted lints, by name. Use `--list` to discover the set
/// of available lints.
///
/// Example: --skip nonempty-boot --skip baseimage-root
#[clap(long)]
skip: Vec<String>,
},
}
/// Subcommands which operate on images.
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum ImageCmdOpts {
/// Wrapper for `podman image list` in bootc storage.
List {
#[clap(allow_hyphen_values = true)]
args: Vec<OsString>,
},
/// Wrapper for `podman image build` in bootc storage.
Build {
#[clap(allow_hyphen_values = true)]
args: Vec<OsString>,
},
/// Wrapper for `podman image pull` in bootc storage.
Pull {
#[clap(allow_hyphen_values = true)]
args: Vec<OsString>,
},
/// Wrapper for `podman image push` in bootc storage.
Push {
#[clap(allow_hyphen_values = true)]
args: Vec<OsString>,
},
}
#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum ImageListType {
/// List all images
#[default]
All,
/// List only logically bound images
Logical,
/// List only host images
Host,
}
impl std::fmt::Display for ImageListType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.to_possible_value().unwrap().get_name().fmt(f)
}
}
#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum ImageListFormat {
/// Human readable table format
#[default]
Table,
/// JSON format
Json,
}
impl std::fmt::Display for ImageListFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.to_possible_value().unwrap().get_name().fmt(f)
}
}
/// Subcommands which operate on images.
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum ImageOpts {
/// List fetched images stored in the bootc storage.
///
/// Note that these are distinct from images stored via e.g. `podman`.
List {
/// Type of image to list
#[clap(long = "type")]
#[arg(default_value_t)]
list_type: ImageListType,
#[clap(long = "format")]
#[arg(default_value_t)]
list_format: ImageListFormat,
},
/// Copy a container image from the bootc storage to `containers-storage:`.
///
/// The source and target are both optional; if both are left unspecified,
/// via a simple invocation of `bootc image copy-to-storage`, then the default is to
/// push the currently booted image to `containers-storage` (as used by podman, etc.)
/// and tagged with the image name `localhost/bootc`,
///
/// ## Copying a non-default container image
///
/// It is also possible to copy an image other than the currently booted one by
/// specifying `--source`.
///
/// ## Pulling images
///
/// At the current time there is no explicit support for pulling images other than indirectly
/// via e.g. `bootc switch` or `bootc upgrade`.
CopyToStorage {
#[clap(long)]
/// The source image; if not specified, the booted image will be used.
source: Option<String>,
#[clap(long)]
/// The destination; if not specified, then the default is to push to `containers-storage:localhost/bootc`;
/// this will make the image accessible via e.g. `podman run localhost/bootc` and for builds.
target: Option<String>,
},
/// Copy a container image from the default `containers-storage:` to the bootc-owned container storage.
PullFromDefaultStorage {
/// The image to pull
image: String,
},
/// List fetched images stored in the bootc storage.
///
/// Note that these are distinct from images stored via e.g. `podman`.
#[clap(subcommand)]
Cmd(ImageCmdOpts),
}
#[derive(Debug, Clone, clap::ValueEnum, PartialEq, Eq)]
pub(crate) enum SchemaType {
Host,
Progress,
}
/// Options for consistency checking
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum FsverityOpts {
/// Measure the fsverity digest of the target file.
Measure {
/// Path to file
path: Utf8PathBuf,
},
/// Enable fsverity on the target file.
Enable {
/// Ptah to file
path: Utf8PathBuf,
},
}
/// Hidden, internal only options
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum InternalsOpts {
SystemdGenerator {
normal_dir: Utf8PathBuf,
#[allow(dead_code)]
early_dir: Option<Utf8PathBuf>,
#[allow(dead_code)]
late_dir: Option<Utf8PathBuf>,
},
FixupEtcFstab,
/// Should only be used by `make update-generated`
PrintJsonSchema {
#[clap(long)]
of: SchemaType,
},
#[clap(subcommand)]
Fsverity(FsverityOpts),
/// Perform consistency checking.
Fsck,
/// Perform cleanup actions
Cleanup,
Relabel {
#[clap(long)]
/// Relabel using this path as root
as_path: Option<Utf8PathBuf>,
/// Relabel this path
path: Utf8PathBuf,
},
/// Proxy frontend for the `ostree-ext` CLI.
OstreeExt {
#[clap(allow_hyphen_values = true)]
args: Vec<OsString>,
},
/// Proxy frontend for the legacy `ostree container` CLI.
OstreeContainer {
#[clap(allow_hyphen_values = true)]
args: Vec<OsString>,
},
/// Invoked from ostree-ext to complete an installation.
BootcInstallCompletion {
/// Path to the sysroot
sysroot: Utf8PathBuf,
// The stateroot
stateroot: String,
},
#[cfg(feature = "rhsm")]
/// Publish subscription-manager facts to /etc/rhsm/facts/bootc.facts
PublishRhsmFacts,
}
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum StateOpts {
/// Remove all ostree deployments from this system
WipeOstree,
}
impl InternalsOpts {
/// The name of the binary we inject into /usr/lib/systemd/system-generators
const GENERATOR_BIN: &'static str = "bootc-systemd-generator";
}
/// Deploy and transactionally in-place with bootable container images.
///
/// The `bootc` project currently uses ostree-containers as a backend
/// to support a model of bootable container images. Once installed,
/// whether directly via `bootc install` (executed as part of a container)
/// or via another mechanism such as an OS installer tool, further
/// updates can be pulled and `bootc upgrade`.
#[derive(Debug, Parser, PartialEq, Eq)]
#[clap(name = "bootc")]
#[clap(rename_all = "kebab-case")]
#[clap(version,long_version=clap::crate_version!())]
#[allow(clippy::large_enum_variant)]
pub(crate) enum Opt {
/// Download and queue an updated container image to apply.
///
/// This does not affect the running system; updates operate in an "A/B" style by default.
///
/// A queued update is visible as `staged` in `bootc status`.
///
/// Currently by default, the update will be applied at shutdown time via `ostree-finalize-staged.service`.
/// There is also an explicit `bootc upgrade --apply` verb which will automatically take action (rebooting)
/// if the system has changed.
///
/// However, in the future this is likely to change such that reboots outside of a `bootc upgrade --apply`
/// do *not* automatically apply the update in addition.
#[clap(alias = "update")]
Upgrade(UpgradeOpts),
/// Target a new container image reference to boot.
///
/// This is almost exactly the same operation as `upgrade`, but additionally changes the container image reference
/// instead.
///
/// ## Usage
///
/// A common pattern is to have a management agent control operating system updates via container image tags;
/// for example, `quay.io/exampleos/someuser:v1.0` and `quay.io/exampleos/someuser:v1.1` where some machines
/// are tracking `:v1.0`, and as a rollout progresses, machines can be switched to `v:1.1`.
Switch(SwitchOpts),
/// Change the bootloader entry ordering; the deployment under `rollback` will be queued for the next boot,
/// and the current will become rollback. If there is a `staged` entry (an unapplied, queued upgrade)
/// then it will be discarded.
///
/// Note that absent any additional control logic, if there is an active agent doing automated upgrades
/// (such as the default `bootc-fetch-apply-updates.timer` and associated `.service`) the
/// change here may be reverted. It's recommended to only use this in concert with an agent that
/// is in active control.
///
/// A systemd journal message will be logged with `MESSAGE_ID=26f3b1eb24464d12aa5e7b544a6b5468` in
/// order to detect a rollback invocation.
#[command(after_help = indoc! {r#"
Note on Rollbacks and the `/etc` Directory:
When you perform a rollback (e.g., with `bootc rollback`), any
changes made to files in the `/etc` directory wont carry over
to the rolled-back deployment. The `/etc` files will revert
to their state from that previous deployment instead.
This is because `bootc rollback` just reorders the existing
deployments. It doesn't create new deployments. The `/etc`
merges happen when new deployments are created.
"#})]
Rollback(RollbackOpts),
/// Apply full changes to the host specification.
///
/// This command operates very similarly to `kubectl apply`; if invoked interactively,
/// then the current host specification will be presented in the system default `$EDITOR`
/// for interactive changes.
///
/// It is also possible to directly provide new contents via `bootc edit --filename`.
///
/// Only changes to the `spec` section are honored.
Edit(EditOpts),
/// Display status
///
/// If standard output is a terminal, this will output a description of the bootc system state.
/// If standard output is not a terminal, output a YAML-formatted object using a schema
/// intended to match a Kubernetes resource that describes the state of the booted system.
///
/// ## Parsing output via programs
///
/// Either the default YAML format or `--format=json` can be used. Do not attempt to
/// explicitly parse the output of `--format=humanreadable` as it will very likely
/// change over time.
///
/// ## Programmatically detecting whether the system is deployed via bootc
///
/// Invoke e.g. `bootc status --json`, and check if `status.booted` is not `null`.
Status(StatusOpts),
/// Adds a transient writable overlayfs on `/usr` that will be discarded on reboot.
///
/// ## Use cases
///
/// A common pattern is wanting to use tracing/debugging tools, such as `strace`
/// that may not be in the base image. A system package manager such as `apt` or
/// `dnf` can apply changes into this transient overlay that will be discarded on
/// reboot.
///
/// ## /etc and /var
///
/// However, this command has no effect on `/etc` and `/var` - changes written
/// there will persist. It is common for package installations to modify these
/// directories.
///
/// ## Unmounting
///
/// Almost always, a system process will hold a reference to the open mount point.
/// You can however invoke `umount -l /usr` to perform a "lazy unmount".
///
#[clap(alias = "usroverlay")]
UsrOverlay,
/// Install the running container to a target.
///
/// ## Understanding installations
///
/// OCI containers are effectively layers of tarballs with JSON for metadata; they
/// cannot be booted directly. The `bootc install` flow is a highly opinionated
/// method to take the contents of the container image and install it to a target
/// block device (or an existing filesystem) in such a way that it can be booted.
///
/// For example, a Linux partition table and filesystem is used, and the bootloader and kernel
/// embedded in the container image are also prepared.
///
/// A bootc installed container currently uses OSTree as a backend, and this sets
/// it up such that a subsequent `bootc upgrade` can perform in-place updates.
///
/// An installation is not simply a copy of the container filesystem, but includes
/// other setup and metadata.
#[clap(subcommand)]
Install(InstallOpts),
/// Operations which can be executed as part of a container build.
#[clap(subcommand)]
Container(ContainerOpts),
/// Operations on container images
///
/// Stability: This interface is not declared stable and may change or be removed
/// at any point in the future.
#[clap(subcommand, hide = true)]
Image(ImageOpts),
/// Execute the given command in the host mount namespace
#[clap(hide = true)]
ExecInHostMountNamespace {
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<OsString>,
},
/// Modify the state of the system
#[clap(hide = true)]
#[clap(subcommand)]
State(StateOpts),
#[clap(subcommand)]
#[clap(hide = true)]
Internals(InternalsOpts),
#[clap(hide(true))]
#[cfg(feature = "docgen")]
Man(ManOpts),
}
/// Ensure we've entered a mount namespace, so that we can remount
/// `/sysroot` read-write
/// TODO use https://github.com/ostreedev/ostree/pull/2779 once
/// we can depend on a new enough ostree
#[context("Ensuring mountns")]
pub(crate) fn ensure_self_unshared_mount_namespace() -> Result<()> {
let uid = rustix::process::getuid();
if !uid.is_root() {
tracing::debug!("Not root, assuming no need to unshare");
return Ok(());
}
let recurse_env = "_ostree_unshared";
let ns_pid1 = std::fs::read_link("/proc/1/ns/mnt").context("Reading /proc/1/ns/mnt")?;
let ns_self = std::fs::read_link("/proc/self/ns/mnt").context("Reading /proc/self/ns/mnt")?;
// If we already appear to be in a mount namespace, or we're already pid1, we're done
if ns_pid1 != ns_self {
tracing::debug!("Already in a mount namespace");
return Ok(());
}
if std::env::var_os(recurse_env).is_some() {
let am_pid1 = rustix::process::getpid().is_init();
if am_pid1 {
tracing::debug!("We are pid 1");
return Ok(());
} else {
anyhow::bail!("Failed to unshare mount namespace");
}
}
crate::reexec::reexec_with_guardenv(recurse_env, &["unshare", "-m", "--"])
}
/// Acquire a locked sysroot.
/// TODO drain this and the above into SysrootLock
#[context("Acquiring sysroot")]
pub(crate) async fn get_locked_sysroot() -> Result<ostree_ext::sysroot::SysrootLock> {
prepare_for_write()?;
let sysroot = ostree::Sysroot::new_default();
sysroot.set_mount_namespace_in_use();
let sysroot = ostree_ext::sysroot::SysrootLock::new_from_sysroot(&sysroot).await?;
sysroot.load(gio::Cancellable::NONE)?;
Ok(sysroot)
}
/// Load global storage state, expecting that we're booted into a bootc system.
#[context("Initializing storage")]
pub(crate) async fn get_storage() -> Result<crate::store::Storage> {
let global_run = Dir::open_ambient_dir("/run", cap_std::ambient_authority())?;
let sysroot = get_locked_sysroot().await?;
crate::store::Storage::new(sysroot, &global_run)
}
#[context("Querying root privilege")]
pub(crate) fn require_root(is_container: bool) -> Result<()> {
ensure!(
rustix::process::getuid().is_root(),
if is_container {
"The user inside the container from which you are running this command must be root"
} else {
"This command must be executed as the root user"
}
);
ensure!(
rustix::thread::capability_is_in_bounding_set(rustix::thread::Capability::SystemAdmin)?,
if is_container {
"The container must be executed with full privileges (e.g. --privileged flag)"
} else {
"This command requires full root privileges (CAP_SYS_ADMIN)"
}
);
tracing::trace!("Verified uid 0 with CAP_SYS_ADMIN");
Ok(())
}
/// A few process changes that need to be made for writing.
/// IMPORTANT: This may end up re-executing the current process,
/// so anything that happens before this should be idempotent.
#[context("Preparing for write")]
fn prepare_for_write() -> Result<()> {
use std::sync::atomic::{AtomicBool, Ordering};
// This is intending to give "at most once" semantics to this
// function. We should never invoke this from multiple threads
// at the same time, but verifying "on main thread" is messy.
// Yes, using SeqCst is likely overkill, but there is nothing perf
// sensitive about this.
static ENTERED: AtomicBool = AtomicBool::new(false);
if ENTERED.load(Ordering::SeqCst) {
return Ok(());
}
if ostree_ext::container_utils::is_ostree_container()? {
anyhow::bail!(
"Detected container (ostree base); this command requires a booted host system."
);
}
if ostree_ext::container_utils::running_in_container() {
anyhow::bail!("Detected container; this command requires a booted host system.");
}
anyhow::ensure!(
ostree_booted()?,
"This command requires an ostree-booted host system"
);
crate::cli::require_root(false)?;
ensure_self_unshared_mount_namespace()?;
if crate::lsm::selinux_enabled()? && !crate::lsm::selinux_ensure_install()? {
tracing::warn!("Do not have install_t capabilities");
}
ENTERED.store(true, Ordering::SeqCst);
Ok(())
}
/// Implementation of the `bootc upgrade` CLI command.
#[context("Upgrading")]
async fn upgrade(opts: UpgradeOpts) -> Result<()> {
let sysroot = &get_storage().await?;
let repo = &sysroot.repo();
let (booted_deployment, _deployments, host) =
crate::status::get_status_require_booted(sysroot)?;
let imgref = host.spec.image.as_ref();
let prog: ProgressWriter = opts.progress.try_into()?;
// If there's no specified image, let's be nice and check if the booted system is using rpm-ostree
if imgref.is_none() {
let booted_incompatible = host
.status
.booted
.as_ref()
.map_or(false, |b| b.incompatible);
let staged_incompatible = host
.status
.staged
.as_ref()
.map_or(false, |b| b.incompatible);
if booted_incompatible || staged_incompatible {
return Err(anyhow::anyhow!(
"Deployment contains local rpm-ostree modifications; cannot upgrade via bootc. You can run `rpm-ostree reset` to undo the modifications."
));
}
}
let spec = RequiredHostSpec::from_spec(&host.spec)?;
let booted_image = host
.status
.booted
.map(|b| b.query_image(repo))
.transpose()?
.flatten();
let imgref = imgref.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
// Find the currently queued digest, if any before we pull
let staged = host.status.staged.as_ref();
let staged_image = staged.as_ref().and_then(|s| s.image.as_ref());
let mut changed = false;
if opts.check {
let imgref = imgref.clone().into();
let mut imp = crate::deploy::new_importer(repo, &imgref).await?;
match imp.prepare().await? {
PrepareResult::AlreadyPresent(_) => {
println!("No changes in: {imgref:#}");
}
PrepareResult::Ready(r) => {
crate::deploy::check_bootc_label(&r.config);
println!("Update available for: {imgref:#}");
if let Some(version) = r.version() {
println!(" Version: {version}");
}
println!(" Digest: {}", r.manifest_digest);
changed = true;
if let Some(previous_image) = booted_image.as_ref() {
let diff =
ostree_container::ManifestDiff::new(&previous_image.manifest, &r.manifest);
diff.print();
}
}
}
} else {
let fetched = crate::deploy::pull(repo, imgref, None, opts.quiet, prog.clone()).await?;
let staged_digest = staged_image.map(|s| s.digest().expect("valid digest in status"));
let fetched_digest = &fetched.manifest_digest;
tracing::debug!("staged: {staged_digest:?}");
tracing::debug!("fetched: {fetched_digest}");
let staged_unchanged = staged_digest
.as_ref()
.map(|d| d == fetched_digest)
.unwrap_or_default();
let booted_unchanged = booted_image
.as_ref()
.map(|img| &img.manifest_digest == fetched_digest)
.unwrap_or_default();
if staged_unchanged {
println!("Staged update present, not changed.");
if opts.apply {
crate::reboot::reboot()?;
}
} else if booted_unchanged {
println!("No update available.")
} else {
let osname = booted_deployment.osname();
crate::deploy::stage(sysroot, &osname, &fetched, &spec, prog.clone()).await?;
changed = true;
if let Some(prev) = booted_image.as_ref() {
if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
let diff =
ostree_container::ManifestDiff::new(&prev.manifest, &fetched_manifest);
diff.print();
}
}
}
}
if changed {
sysroot.update_mtime()?;
if opts.apply {
crate::reboot::reboot()?;
}
} else {
tracing::debug!("No changes");
}
Ok(())
}
/// Implementation of the `bootc switch` CLI command.
#[context("Switching")]
async fn switch(opts: SwitchOpts) -> Result<()> {
let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
let imgref = ostree_container::ImageReference {
transport,
name: opts.target.to_string(),
};
let sigverify = sigpolicy_from_opt(opts.enforce_container_sigpolicy);
let target = ostree_container::OstreeImageReference { sigverify, imgref };
let target = ImageReference::from(target);
let prog: ProgressWriter = opts.progress.try_into()?;
// If we're doing an in-place mutation, we shortcut most of the rest of the work here
if opts.mutate_in_place {
let deployid = {
// Clone to pass into helper thread
let target = target.clone();
let root = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
tokio::task::spawn_blocking(move || {
crate::deploy::switch_origin_inplace(&root, &target)
})
.await??
};
println!("Updated {deployid} to pull from {target}");
return Ok(());
}
let cancellable = gio::Cancellable::NONE;
let sysroot = &get_storage().await?;
let repo = &sysroot.repo();
let (booted_deployment, _deployments, host) =
crate::status::get_status_require_booted(sysroot)?;
let new_spec = {
let mut new_spec = host.spec.clone();
new_spec.image = Some(target.clone());
new_spec
};
if new_spec == host.spec {
println!("Image specification is unchanged.");
return Ok(());
}
let new_spec = RequiredHostSpec::from_spec(&new_spec)?;
let fetched = crate::deploy::pull(repo, &target, None, opts.quiet, prog.clone()).await?;
if !opts.retain {
// By default, we prune the previous ostree ref so it will go away after later upgrades
if let Some(booted_origin) = booted_deployment.origin() {
if let Some(ostree_ref) = booted_origin.optional_string("origin", "refspec")? {
let (remote, ostree_ref) =
ostree::parse_refspec(&ostree_ref).context("Failed to parse ostree ref")?;
repo.set_ref_immediate(remote.as_deref(), &ostree_ref, None, cancellable)?;
}
}
}
let stateroot = booted_deployment.osname();
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
sysroot.update_mtime()?;
if opts.apply {
crate::reboot::reboot()?;
}
Ok(())
}
/// Implementation of the `bootc rollback` CLI command.
#[context("Rollback")]
async fn rollback(opts: RollbackOpts) -> Result<()> {
let sysroot = &get_storage().await?;
crate::deploy::rollback(sysroot).await?;
if opts.apply {
crate::reboot::reboot()?;
}
Ok(())
}
/// Implementation of the `bootc edit` CLI command.
#[context("Editing spec")]
async fn edit(opts: EditOpts) -> Result<()> {
let sysroot = &get_storage().await?;
let repo = &sysroot.repo();
let (booted_deployment, _deployments, host) =
crate::status::get_status_require_booted(sysroot)?;
let new_host: Host = if let Some(filename) = opts.filename {
let mut r = std::io::BufReader::new(std::fs::File::open(filename)?);
serde_yaml::from_reader(&mut r)?
} else {
let tmpf = tempfile::NamedTempFile::new()?;
serde_yaml::to_writer(std::io::BufWriter::new(tmpf.as_file()), &host)?;
crate::utils::spawn_editor(&tmpf)?;
tmpf.as_file().seek(std::io::SeekFrom::Start(0))?;
serde_yaml::from_reader(&mut tmpf.as_file())?
};
if new_host.spec == host.spec {
println!("Edit cancelled, no changes made.");
return Ok(());
}
host.spec.verify_transition(&new_host.spec)?;
let new_spec = RequiredHostSpec::from_spec(&new_host.spec)?;
let prog = ProgressWriter::default();
// We only support two state transitions right now; switching the image,
// or flipping the bootloader ordering.
if host.spec.boot_order != new_host.spec.boot_order {
return crate::deploy::rollback(sysroot).await;
}
let fetched = crate::deploy::pull(repo, new_spec.image, None, opts.quiet, prog.clone()).await?;
// TODO gc old layers here
let stateroot = booted_deployment.osname();
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
sysroot.update_mtime()?;
Ok(())
}
/// Implementation of `bootc usroverlay`
async fn usroverlay() -> Result<()> {
// This is just a pass-through today. At some point we may make this a libostree API
// or even oxidize it.
Err(Command::new("ostree")
.args(["admin", "unlock"])
.exec()
.into())
}
/// Perform process global initialization. This should be called as early as possible
/// in the standard `main` function.
pub fn global_init() -> Result<()> {
// In some cases we re-exec with a temporary binary,
// so ensure that the syslog identifier is set.
let name = "bootc";
ostree::glib::set_prgname(name.into());
if let Err(e) = rustix::thread::set_name(&CString::new(name).unwrap()) {
// This shouldn't ever happen
eprintln!("failed to set name: {e}");
}
let am_root = rustix::process::getuid().is_root();
// Work around bootc-image-builder not setting HOME, in combination with podman (really c/common)
// bombing out if it is unset.
if std::env::var_os("HOME").is_none() && am_root {
// Setting the environment is thread-unsafe, but we ask calling code
// to invoke this as early as possible. (In practice, that's just the cli's `main.rs`)
// xref https://internals.rust-lang.org/t/synchronized-ffi-access-to-posix-environment-variable-functions/15475
std::env::set_var("HOME", "/root");
}
Ok(())
}
/// Parse the provided arguments and execute.
/// Calls [`clap::Error::exit`] on failure, printing the error message and aborting the program.
pub async fn run_from_iter<I>(args: I) -> Result<()>
where
I: IntoIterator,
I::Item: Into<OsString> + Clone,
{
run_from_opt(Opt::parse_including_static(args)).await
}
/// Find the base binary name from argv0 (without a full path). The empty string
/// is never returned; instead a fallback string is used. If the input is not valid
/// UTF-8, a default is used.
fn callname_from_argv0(argv0: &OsStr) -> &str {
let default = "bootc";
std::path::Path::new(argv0)
.file_name()
.and_then(|s| s.to_str())
.filter(|s| !s.is_empty())
.unwrap_or(default)
}
impl Opt {
/// In some cases (e.g. systemd generator) we dispatch specifically on argv0. This
/// requires some special handling in clap.
fn parse_including_static<I>(args: I) -> Self
where
I: IntoIterator,
I::Item: Into<OsString> + Clone,
{
let mut args = args.into_iter();
let first = if let Some(first) = args.next() {
let first: OsString = first.into();
let argv0 = callname_from_argv0(&first);
tracing::debug!("argv0={argv0:?}");
let mapped = match argv0 {
InternalsOpts::GENERATOR_BIN => {
Some(["bootc", "internals", "systemd-generator"].as_slice())
}
"ostree-container" | "ostree-ima-sign" | "ostree-provisional-repair" => {
Some(["bootc", "internals", "ostree-ext"].as_slice())
}
_ => None,
};
if let Some(base_args) = mapped {
let base_args = base_args.iter().map(OsString::from);
return Opt::parse_from(base_args.chain(args.map(|i| i.into())));
}
Some(first)
} else {
None
};
Opt::parse_from(first.into_iter().chain(args.map(|i| i.into())))
}
}
/// Internal (non-generic/monomorphized) primary CLI entrypoint
async fn run_from_opt(opt: Opt) -> Result<()> {
let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
match opt {
Opt::Upgrade(opts) => upgrade(opts).await,
Opt::Switch(opts) => switch(opts).await,
Opt::Rollback(opts) => rollback(opts).await,
Opt::Edit(opts) => edit(opts).await,
Opt::UsrOverlay => usroverlay().await,
Opt::Container(opts) => match opts {
ContainerOpts::Lint {
rootfs,
fatal_warnings,
list,
skip,
} => {
if list {
return lints::lint_list(std::io::stdout().lock());
}
let warnings = if fatal_warnings {
lints::WarningDisposition::FatalWarnings
} else {
lints::WarningDisposition::AllowWarnings
};
let root_type = if rootfs == "/" {
lints::RootType::Running
} else {
lints::RootType::Alternative
};
let root = &Dir::open_ambient_dir(rootfs, cap_std::ambient_authority())?;
let skip = skip.iter().map(|s| s.as_str());
lints::lint(root, warnings, root_type, skip, std::io::stdout().lock())?;
Ok(())
}
},
Opt::Image(opts) => match opts {
ImageOpts::List {
list_type,
list_format,
} => crate::image::list_entrypoint(list_type, list_format).await,
ImageOpts::CopyToStorage { source, target } => {
crate::image::push_entrypoint(source.as_deref(), target.as_deref()).await
}
ImageOpts::PullFromDefaultStorage { image } => {
let sysroot = get_storage().await?;
sysroot
.get_ensure_imgstore()?
.pull_from_host_storage(&image)
.await
}
ImageOpts::Cmd(opt) => {
let storage = get_storage().await?;
let imgstore = storage.get_ensure_imgstore()?;
match opt {
ImageCmdOpts::List { args } => {
crate::image::imgcmd_entrypoint(imgstore, "list", &args).await
}
ImageCmdOpts::Build { args } => {
crate::image::imgcmd_entrypoint(imgstore, "build", &args).await
}
ImageCmdOpts::Pull { args } => {
crate::image::imgcmd_entrypoint(imgstore, "pull", &args).await
}
ImageCmdOpts::Push { args } => {
crate::image::imgcmd_entrypoint(imgstore, "push", &args).await
}
}
}
},
Opt::Install(opts) => match opts {
#[cfg(feature = "install-to-disk")]
InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await,
InstallOpts::ToFilesystem(opts) => {
crate::install::install_to_filesystem(opts, false, crate::install::Cleanup::Skip)
.await
}
InstallOpts::ToExistingRoot(opts) => {
crate::install::install_to_existing_root(opts).await
}
InstallOpts::PrintConfiguration => crate::install::print_configuration(),
InstallOpts::EnsureCompletion {} => {
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
crate::install::completion::run_from_anaconda(rootfs).await
}
InstallOpts::Finalize { root_path } => {
crate::install::install_finalize(&root_path).await
}
},
Opt::ExecInHostMountNamespace { args } => {
crate::install::exec_in_host_mountns(args.as_slice())
}
Opt::Status(opts) => super::status::status(opts).await,
Opt::Internals(opts) => match opts {
InternalsOpts::SystemdGenerator {
normal_dir,
early_dir: _,
late_dir: _,
} => {
let unit_dir = &Dir::open_ambient_dir(normal_dir, cap_std::ambient_authority())?;
crate::generator::generator(root, unit_dir)
}
InternalsOpts::OstreeExt { args } => {
ostree_ext::cli::run_from_iter(["ostree-ext".into()].into_iter().chain(args)).await
}
InternalsOpts::OstreeContainer { args } => {
ostree_ext::cli::run_from_iter(
["ostree-ext".into(), "container".into()]
.into_iter()
.chain(args),
)
.await
}
// We don't depend on fsverity-utils today, so re-expose some helpful CLI tools.
InternalsOpts::Fsverity(args) => match args {
FsverityOpts::Measure { path } => {
let fd =
std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
let digest: fsverity::Sha256HashValue = fsverity::measure_verity(&fd)?;
let digest = hex::encode(digest);
println!("{digest}");
Ok(())
}
FsverityOpts::Enable { path } => {
let fd =
std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
fsverity::enable_verity::<fsverity::Sha256HashValue>(&fd)?;
Ok(())
}
},
InternalsOpts::Fsck => {
let sysroot = &get_storage().await?;
crate::fsck::fsck(&sysroot, std::io::stdout().lock()).await?;
Ok(())
}
InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root),
InternalsOpts::PrintJsonSchema { of } => {
let schema = match of {
SchemaType::Host => schema_for!(crate::spec::Host),
SchemaType::Progress => schema_for!(crate::progress_jsonl::Event),
};
let mut stdout = std::io::stdout().lock();
serde_json::to_writer_pretty(&mut stdout, &schema)?;
Ok(())
}
InternalsOpts::Cleanup => {
let sysroot = get_storage().await?;
crate::deploy::cleanup(&sysroot).await
}
InternalsOpts::Relabel { as_path, path } => {
let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
let path = path.strip_prefix("/")?;
let sepolicy =
&ostree::SePolicy::new(&gio::File::for_path("/"), gio::Cancellable::NONE)?;
crate::lsm::relabel_recurse(root, path, as_path.as_deref(), sepolicy)?;
Ok(())
}
InternalsOpts::BootcInstallCompletion { sysroot, stateroot } => {
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
crate::install::completion::run_from_ostree(rootfs, &sysroot, &stateroot).await
}
#[cfg(feature = "rhsm")]
InternalsOpts::PublishRhsmFacts => crate::rhsm::publish_facts(&root).await,
},
#[cfg(feature = "docgen")]
Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory),
Opt::State(opts) => match opts {
StateOpts::WipeOstree => {
let sysroot = ostree::Sysroot::new_default();
sysroot.load(gio::Cancellable::NONE)?;
crate::deploy::wipe_ostree(sysroot).await?;
Ok(())
}
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_callname() {
use std::os::unix::ffi::OsStrExt;
// Cases that change
let mapped_cases = [
("", "bootc"),
("/foo/bar", "bar"),
("/foo/bar/", "bar"),
("foo/bar", "bar"),
("../foo/bar", "bar"),
("usr/bin/ostree-container", "ostree-container"),
];
for (input, output) in mapped_cases {
assert_eq!(
output,
callname_from_argv0(OsStr::new(input)),
"Handling mapped case {input}"
);
}
// Invalid UTF-8
assert_eq!("bootc", callname_from_argv0(OsStr::from_bytes(b"foo\x80")));
// Cases that are identical
let ident_cases = ["foo", "bootc"];
for case in ident_cases {
assert_eq!(
case,
callname_from_argv0(OsStr::new(case)),
"Handling ident case {case}"
);
}
}
#[test]
fn test_parse_install_args() {
// Verify we still process the legacy --target-no-signature-verification
let o = Opt::try_parse_from([
"bootc",
"install",
"to-filesystem",
"--target-no-signature-verification",
"/target",
])
.unwrap();
let o = match o {
Opt::Install(InstallOpts::ToFilesystem(fsopts)) => fsopts,
o => panic!("Expected filesystem opts, not {o:?}"),
};
assert!(o.target_opts.target_no_signature_verification);
assert_eq!(o.filesystem_opts.root_path.as_str(), "/target");
// Ensure we default to old bound images behavior
assert_eq!(
o.config_opts.bound_images,
crate::install::BoundImagesOpt::Stored
);
}
#[test]
fn test_parse_opts() {
assert!(matches!(
Opt::parse_including_static(["bootc", "status"]),
Opt::Status(StatusOpts {
json: false,
format: None,
format_version: None,
booted: false
})
));
assert!(matches!(
Opt::parse_including_static(["bootc", "status", "--format-version=0"]),
Opt::Status(StatusOpts {
format_version: Some(0),
..
})
));
}
#[test]
fn test_parse_generator() {
assert!(matches!(
Opt::parse_including_static([
"/usr/lib/systemd/system/bootc-systemd-generator",
"/run/systemd/system"
]),
Opt::Internals(InternalsOpts::SystemdGenerator { normal_dir, .. }) if normal_dir == "/run/systemd/system"
));
}
#[test]
fn test_parse_ostree_ext() {
assert!(matches!(
Opt::parse_including_static(["bootc", "internals", "ostree-container"]),
Opt::Internals(InternalsOpts::OstreeContainer { .. })
));
fn peel(o: Opt) -> Vec<OsString> {
match o {
Opt::Internals(InternalsOpts::OstreeExt { args }) => args,
o => panic!("unexpected {o:?}"),
}
}
let args = peel(Opt::parse_including_static([
"/usr/libexec/libostree/ext/ostree-ima-sign",
"ima-sign",
"--repo=foo",
"foo",
"bar",
"baz",
]));
assert_eq!(
args.as_slice(),
["ima-sign", "--repo=foo", "foo", "bar", "baz"]
);
let args = peel(Opt::parse_including_static([
"/usr/libexec/libostree/ext/ostree-container",
"container",
"image",
"pull",
]));
assert_eq!(args.as_slice(), ["container", "image", "pull"]);
}
}