Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

install: Rename install -> install to-disk, peer with to-filesystem #226

Merged
merged 3 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ jobs:
run: |
set -xeuo pipefail
sudo podman run --rm -ti --privileged -v /:/target -v ./usr/bin/bootc:/usr/bin/bootc --pid=host --security-opt label=disable \
quay.io/centos-bootc/fedora-bootc-dev:eln bootc install-to-filesystem --target-no-signature-verification \
quay.io/centos-bootc/fedora-bootc-dev:eln bootc install to-filesystem --target-no-signature-verification \
--karg=foo=bar --disable-selinux --replace=alongside /target
ls -al /boot/loader/
sudo grep foo=bar /boot/loader/entries/*.conf
11 changes: 8 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,15 @@ First, build a derived container using any container build tooling.

#### Using `bootc install`

The `bootc install` command will write the current container to a disk, and set it up for booting.
The `bootc install` command has two high level sub-commands; `to-disk` and `to-filesystem`.

The `bootc install to-disk` handles basically everything in taking the current container
and writing it to a disk, and set it up for booting and future in-place upgrades.

In brief, the idea is that every container image shipping `bootc` also comes with a simple
installer that can set a system up to boot from it. Crucially, if you create a
*derivative* container image from a stock OS container image, it also automatically supports `bootc install`.
installer that can set a system up to boot from it. Crucially, if you create a
*derivative* container image from a stock OS container image, it also automatically
supports `bootc install`.

For more information, please see [install.md](install.md).

Expand Down
35 changes: 17 additions & 18 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ The Linux kernel (and optionally initramfs) is embedded in the container image;
is `/usr/lib/modules/$kver/vmlinuz`, and the initramfs should be in `initramfs.img`
in that directory.

The `bootc install` and `boot install-to-filesystem` commands bridge the two worlds
of a standard, runnable OCI image and a bootable system by running tooling logic embedded
The `bootc install` command bridges the two worlds of a standard, runnable OCI image
and a bootable system by running tooling logic embedded
in the container image to create the filesystem and bootloader setup dynamically.
This requires running the container via `--privileged`; it uses the running Linux kernel
on the host to write the file content from the running container image; not the kernel
inside the container.

There are two sub-commands: `bootc install to-disk` and `boot install to-filesystem`.

However, nothing *else* (external) is required to perform a basic installation
to disk. (The one exception to host requirements today is that the host must
have `skopeo` installed. This is a bug; more information in
Expand All @@ -44,8 +46,8 @@ image comes with a basic installer.
## Executing `bootc install`

The two installation commands allow you to install the container image
either directly to a block device (`bootc install`) or to an existing
filesystem (`bootc install-to-filesystem`).
either directly to a block device (`bootc install to-disk`) or to an existing
filesystem (`bootc install to-filesystem`).

The installation commands **MUST** be run **from** the container image
that will be installed, using `--privileged` and a few
Expand All @@ -56,7 +58,7 @@ to an existing system and install your container image. Failure to run
Here's an example of using `bootc install` (root/elevated permission required):

```bash
podman run --rm --privileged --pid=host --security-opt label=type:unconfined_t <image> bootc install --target-no-signature-verification /path/to/disk
podman run --rm --privileged --pid=host --security-opt label=type:unconfined_t <image> bootc install to-disk --target-no-signature-verification /path/to/disk
```

Note that while `--privileged` is used, this command will not perform any
Expand All @@ -68,7 +70,7 @@ The `--pid=host --security-opt label=type:unconfined_t` today
make it more convenient for bootc to perform some privileged
operations; in the future these requirement may be dropped.

Jump to the section for [`install-to-filesystem`](#more-advanced-installation) later
Jump to the section for [`install to-filesystem`](#more-advanced-installation) later
in this document for additional information about that method.

### "day 2" updates, security and fetch configuration
Expand Down Expand Up @@ -181,16 +183,16 @@ the files are underneath `/usr`. To rotate or change the set of keys,
one would build a new container image. Client systems using `bootc upgrade`
will transactionally update to this new system state.

## More advanced installation
## More advanced installation with `to-filesystem`

The basic `bootc install` logic is really a pretty small (but opinionated) wrapper
The basic `bootc install to-disk` logic is really a pretty small (but opinionated) wrapper
for a set of lower level tools that can also be invoked independently.

The `bootc install` command is effectively:
The `bootc install to-disk` command is effectively:

- `mkfs.$fs /dev/disk`
- `mount /dev/disk /mnt`
- `bootc install-to-filesystem --karg=root=UUID=<uuid of /mnt> --imgref $self /mnt`
- `bootc install to-filesystem --karg=root=UUID=<uuid of /mnt> --imgref $self /mnt`

There may be a bit more involved here; for example configuring
`--block-setup tpm2-luks` will configure the root filesystem
Expand All @@ -199,25 +201,22 @@ with LUKS bound to the TPM2 chip, currently via [systemd-cryptenroll](https://ww
Some OS/distributions may not want to enable it at all; it
can be configured off at build time via Cargo features.

### Using `bootc install-to-filesystem`

As noted above, there is also `bootc install-to-filesystem`, which allows
an arbitrary process to create the root filesystem.
### Using `bootc install to-filesystem`

The usual expected way for an external storage system to work
is to provide `root=<UUID>` type kernel arguments. At the current
time a separate `/boot` filesystem is also required (mainly to enable LUKS)
so you will also need to provide e.g. `--boot-mount-spec UUID=...`.

The `bootc install-to-filesystem` command allows an operating
The `bootc install to-filesystem` command allows an operating
system or distribution to ship a separate installer that creates more complex block
storage or filesystem setups, but reuses the "top half" of the logic.
For example, a goal is to change [Anaconda](https://github.com/rhinstaller/anaconda/)
to use this.

### Using `bootc install-to-filesystem --replace=alongside`
### Using `bootc install to-filesystem --replace=alongside`

This is a variant of `install-to-filesystem`, which maximizes convenience for using
This is a variant of `install to-filesystem`, which maximizes convenience for using
an existing Linux system, converting it into the target container image. Note that
the `/boot` (and `/boot/efi`) partitions *will be reinitialized* - so this is a
somewhat destructive operation for the existing Linux installation.
Expand All @@ -231,7 +230,7 @@ The core command should look like this (root/elevated permission required):
podman run --rm --privileged -v /:/target \
--pid=host --security-opt label=type:unconfined_t \
<image> \
bootc install-to-filesystem --replace=alongside /target
bootc install to-filesystem --replace=alongside /target
```

At the current time, leftover data in `/` is **NOT** automatically cleaned up. This can
Expand Down
28 changes: 19 additions & 9 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ pub(crate) struct StatusOpts {
pub(crate) booted: bool,
}

/// Options for internal testing
#[cfg(feature = "install")]
#[derive(Debug, clap::Subcommand)]
pub(crate) enum InstallOpts {
/// Install to the target block device
ToDisk(crate::install::InstallToDiskOpts),
/// Install to the target filesystem
ToFilesystem(crate::install::InstallToFilesystemOpts),
}

/// Options for man page generation
#[derive(Debug, Parser)]
pub(crate) struct ManOpts {
Expand All @@ -112,7 +122,7 @@ pub(crate) enum TestingOpts {
RunContainerIntegration {},
/// Block device setup for testing
PrepTestInstallFilesystem { blockdev: Utf8PathBuf },
/// e2e test of install-to-filesystem
/// e2e test of install to-filesystem
TestInstallFilesystem {
image: String,
blockdev: Utf8PathBuf,
Expand Down Expand Up @@ -150,17 +160,16 @@ pub(crate) enum Opt {
/// Add a transient writable overlayfs on `/usr` that will be discarded on reboot.
#[clap(alias = "usroverlay")]
UsrOverlay,
/// Install to the target block device
/// Install the running container to a target
#[clap(subcommand)]
#[cfg(feature = "install")]
Install(crate::install::InstallOpts),
Install(InstallOpts),
/// Execute the given command in the host mount namespace
#[cfg(feature = "install")]
#[clap(hide = true)]
#[command(external_subcommand)]
ExecInHostMountNamespace(Vec<OsString>),
/// Install to the target filesystem.
#[cfg(feature = "install")]
InstallToFilesystem(crate::install::InstallToFilesystemOpts),

/// Internal integration testing helpers.
#[clap(hide(true), subcommand)]
#[cfg(feature = "internal-testing-api")]
Expand Down Expand Up @@ -454,9 +463,10 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
Opt::Edit(opts) => edit(opts).await,
Opt::UsrOverlay => usroverlay().await,
#[cfg(feature = "install")]
Opt::Install(opts) => crate::install::install(opts).await,
#[cfg(feature = "install")]
Opt::InstallToFilesystem(opts) => crate::install::install_to_filesystem(opts).await,
Opt::Install(opts) => match opts {
InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await,
InstallOpts::ToFilesystem(opts) => crate::install::install_to_filesystem(opts).await,
},
#[cfg(feature = "install")]
Opt::ExecInHostMountNamespace(args) => {
crate::install::exec_in_host_mountns(args.as_slice())
Expand Down
35 changes: 26 additions & 9 deletions lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! This module supports installing a bootc-compatible image to
//! a block device directly via the `install` verb, or to an externally
//! set up filesystem via `install-to-filesystem`.
//! set up filesystem via `install to-filesystem`.

// This sub-module is the "basic" installer that handles creating basic block device
// and filesystem setup.
Expand All @@ -26,6 +26,7 @@ use cap_std_ext::prelude::CapStdExtDirExt;
use chrono::prelude::*;
use clap::ValueEnum;
use ostree_ext::oci_spec;
use rustix::fs::FileTypeExt;
use rustix::fs::MetadataExt;

use fn_error_context::context;
Expand Down Expand Up @@ -118,7 +119,7 @@ pub(crate) struct InstallConfigOpts {

/// Perform an installation to a block device.
#[derive(Debug, Clone, clap::Parser, Serialize, Deserialize)]
pub(crate) struct InstallOpts {
pub(crate) struct InstallToDiskOpts {
#[clap(flatten)]
#[serde(flatten)]
pub(crate) block_opts: InstallBlockDeviceOpts,
Expand Down Expand Up @@ -1021,9 +1022,16 @@ fn installation_complete() {
println!("Installation complete!");
}

/// Implementation of the `bootc install` CLI command.
pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
/// Implementation of the `bootc install to-disk` CLI command.
pub(crate) async fn install_to_disk(opts: InstallToDiskOpts) -> Result<()> {
let block_opts = opts.block_opts;
let target_blockdev_meta = block_opts
.device
.metadata()
.with_context(|| format!("Querying {}", &block_opts.device))?;
if !target_blockdev_meta.file_type().is_block_device() {
anyhow::bail!("Not a block device: {}", block_opts.device);
}
let state = prepare_install(opts.config_opts, opts.target_opts).await?;

// This is all blocking stuff
Expand Down Expand Up @@ -1113,15 +1121,24 @@ fn clean_boot_directories(rootfs: &Dir) -> Result<()> {
Ok(())
}

/// Implementation of the `bootc install-to-filsystem` CLI command.
/// Implementation of the `bootc install to-filsystem` CLI command.
pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Result<()> {
// Gather global state, destructuring the provided options
let state = prepare_install(opts.config_opts, opts.target_opts).await?;
let fsopts = opts.filesystem_opts;

let root_path = &fsopts.root_path;

let st = root_path.symlink_metadata()?;
if !st.is_dir() {
anyhow::bail!("Not a directory: {root_path}");
}
let rootfs_fd = Dir::open_ambient_dir(root_path, cap_std::ambient_authority())
.with_context(|| format!("Opening target root directory {root_path}"))?;
if let Some(false) = ostree_ext::mountutil::is_mountpoint(&rootfs_fd, ".")? {
anyhow::bail!("Not a root mountpoint: {root_path}");
}

// Gather global state, destructuring the provided options
let state = prepare_install(opts.config_opts, opts.target_opts).await?;

match fsopts.replace {
Some(ReplaceMode::Wipe) => {
let rootfs_fd = rootfs_fd.try_clone()?;
Expand Down Expand Up @@ -1252,7 +1269,7 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu

#[test]
fn install_opts_serializable() {
let c: InstallOpts = serde_json::from_value(serde_json::json!({
let c: InstallToDiskOpts = serde_json::from_value(serde_json::json!({
"device": "/dev/vda"
}))
.unwrap();
Expand Down
2 changes: 1 addition & 1 deletion lib/src/install/baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! This module handles creation of simple root filesystem setups. At the current time
//! it's very simple - just a direct filesystem (e.g. xfs, ext4, btrfs etc.). It is
//! intended to add opinionated handling of TPM2-bound LUKS too. But that's about it;
//! other more complex flows should set things up externally and use `bootc install-to-filesystem`.
//! other more complex flows should set things up externally and use `bootc install to-filesystem`.

use std::borrow::Cow;
use std::fmt::Display;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/privtests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ fn test_install_filesystem(image: &str, blockdev: &Utf8Path) -> Result<()> {
let mountpoint: &Utf8Path = mountpoint_dir.path().try_into().unwrap();

// And run the install
cmd!(sh, "podman run --rm --privileged --pid=host --env=RUST_LOG -v /usr/bin/bootc:/usr/bin/bootc -v {mountpoint}:/target-root {image} bootc install-to-filesystem --target-no-signature-verification /target-root").run()?;
cmd!(sh, "podman run --rm --privileged --pid=host --env=RUST_LOG -v /usr/bin/bootc:/usr/bin/bootc -v {mountpoint}:/target-root {image} bootc install to-filesystem --target-no-signature-verification /target-root").run()?;

cmd!(sh, "umount -R {mountpoint}").run()?;

Expand Down
4 changes: 2 additions & 2 deletions tests/kolainst/install
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ case "${AUTOPKGTEST_REBOOT_MARK:-}" in
EOF
podman build -t localhost/testimage .
podman run --rm -ti --privileged --pid=host --env RUST_LOG=error,bootc_lib::install=debug \
localhost/testimage bootc install --target-no-signature-verification --skip-fetch-check --karg=foo=bar ${DEV}
localhost/testimage bootc install to-disk --target-no-signature-verification --skip-fetch-check --karg=foo=bar ${DEV}
# In theory we could e.g. wipe the bootloader setup on the primary disk, then reboot;
# but for now let's just sanity test that the install command executes.
lsblk ${DEV}
Expand All @@ -41,7 +41,7 @@ EOF
umount /var/mnt
echo "ok install"

# Now test install-to-filesystem
# Now test install to-filesystem
# Wipe the device
ls ${DEV}* | tac | xargs wipefs -af
# This prepares the device and also runs podman directliy
Expand Down