Skip to content

Commit

Permalink
Merge pull request #226 from cgwalters/install-rework
Browse files Browse the repository at this point in the history
install: Rename `install` -> `install to-disk`, peer with `to-filesystem`
  • Loading branch information
cgwalters committed Dec 15, 2023
2 parents c9daff0 + 7997f57 commit 1a36314
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 44 deletions.
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 @@ -1022,9 +1023,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 @@ -1114,15 +1122,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 @@ -1253,7 +1270,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

0 comments on commit 1a36314

Please sign in to comment.