├── .gitignore
├── .goosehints
├── docs
├── .gitignore
├── src
│ ├── ..contributing.md
│ ├── HACKING.md
│ ├── contributing.md
│ ├── disk-images.md
│ ├── man
│ │ ├── bcvk-libvirt-base-disks.md
│ │ ├── bcvk-images.md
│ │ ├── bcvk-ephemeral-ps.md
│ │ ├── bcvk-libvirt-start.md
│ │ ├── bcvk-libvirt-stop.md
│ │ ├── bcvk-libvirt-rm.md
│ │ ├── bcvk-libvirt-status.md
│ │ ├── bcvk-libvirt-inspect.md
│ │ ├── bcvk-ephemeral.md
│ │ ├── bcvk-images-list.md
│ │ ├── bcvk-ephemeral-rm-all.md
│ │ ├── bcvk.md
│ │ ├── bcvk-libvirt.md
│ │ ├── bcvk-libvirt-list-volumes.md
│ │ ├── bcvk-libvirt-ssh.md
│ │ ├── bcvk-libvirt-rm-all.md
│ │ ├── bcvk-libvirt-upload.md
│ │ ├── bcvk-ephemeral-ssh.md
│ │ ├── bcvk-libvirt-list.md
│ │ ├── bcvk-ephemeral-run-ssh.md
│ │ ├── bcvk-to-disk.md
│ │ └── bcvk-libvirt-run.md
│ ├── libvirt-manage.md
│ ├── introduction.md
│ ├── installation.md
│ ├── image-management.md
│ ├── SUMMARY.md
│ ├── workflow-comparison.md
│ ├── ephemeral-ssh.md
│ ├── libvirt-advanced.md
│ ├── building.md
│ ├── quick-start.md
│ ├── ephemeral-run.md
│ ├── testing.md
│ ├── libvirt-run.md
│ ├── vs-podman-bootc.md
│ └── vs-bootc.md
├── footer.html
├── book.toml
├── mermaid-init.js
└── HACKING.md
├── .claude
└── CLAUDE.md
├── .cargo
└── config.toml
├── .bootc-dev-infra-commit.txt
├── crates
├── kit
│ ├── src
│ │ ├── lib.rs
│ │ ├── common_opts.rs
│ │ ├── podman.rs
│ │ ├── qemu_img.rs
│ │ ├── libvirt
│ │ │ ├── stop.rs
│ │ │ ├── start.rs
│ │ │ ├── inspect.rs
│ │ │ ├── print_firmware.rs
│ │ │ ├── base_disks_cli.rs
│ │ │ ├── list.rs
│ │ │ ├── rm_all.rs
│ │ │ └── rm.rs
│ │ ├── boot_progress.rs
│ │ ├── install_options.rs
│ │ ├── supervisor_status.rs
│ │ ├── systemd.rs
│ │ ├── container_entrypoint.rs
│ │ ├── status_monitor.rs
│ │ ├── cli_json.rs
│ │ ├── instancetypes.rs
│ │ └── arch.rs
│ ├── Cargo.toml
│ └── scripts
│ │ └── entrypoint.sh
├── integration-tests
│ ├── src
│ │ ├── tests
│ │ │ ├── libvirt_upload_disk.rs
│ │ │ └── mount_feature.rs
│ │ ├── bin
│ │ │ └── cleanup.rs
│ │ └── lib.rs
│ ├── fixtures
│ │ └── Dockerfile.no-kernel
│ └── Cargo.toml
└── xtask
│ ├── Cargo.toml
│ └── src
│ └── xtask.rs
├── .gemini
└── config.yaml
├── .devcontainer
└── devcontainer.json
├── .github
├── actions
│ ├── setup-rust
│ │ └── action.yml
│ └── bootc-ubuntu-setup
│ │ └── action.yml
└── workflows
│ ├── rebase.yml
│ ├── openssf-scorecard.yml
│ ├── docs.yml
│ ├── main.yml
│ ├── scheduled-release.yml
│ └── release.yml
├── AGENTS.md
├── LICENSE-MIT
├── Cargo.toml
├── .config
└── nextest.toml
├── Makefile
├── Justfile
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
--------------------------------------------------------------------------------
/.goosehints:
--------------------------------------------------------------------------------
1 | docs/HACKING.md
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | book/
2 |
--------------------------------------------------------------------------------
/.claude/CLAUDE.md:
--------------------------------------------------------------------------------
1 | ../AGENTS.md
--------------------------------------------------------------------------------
/docs/src/..contributing.md:
--------------------------------------------------------------------------------
1 | # HACKING
2 |
--------------------------------------------------------------------------------
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [alias]
2 | xtask = "run --package xtask --"
3 |
--------------------------------------------------------------------------------
/.bootc-dev-infra-commit.txt:
--------------------------------------------------------------------------------
1 | 2dd498656b9653c321e5d9a8600e6b506714acb3
2 |
--------------------------------------------------------------------------------
/docs/src/HACKING.md:
--------------------------------------------------------------------------------
1 | # HACKING
2 |
3 | See [../HACKING.md](../HACKING.md) for development instructions.
4 |
--------------------------------------------------------------------------------
/crates/kit/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! bcvk library - exposes internal modules for testing
2 |
3 | pub mod qemu_img;
4 | pub mod xml_utils;
5 |
--------------------------------------------------------------------------------
/docs/src/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | We welcome contributions to bcvk! Please see [../HACKING.md](../HACKING.md) for detailed development instructions.
--------------------------------------------------------------------------------
/docs/src/disk-images.md:
--------------------------------------------------------------------------------
1 | # Disk Images
2 |
3 | The `to-disk` command creates disk images from bootc containers.
4 | It is a wrapper for [bootc install to-disk](https://bootc-dev.github.io/bootc/man/bootc-install-to-disk.8.html).
5 |
--------------------------------------------------------------------------------
/docs/footer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | The Linux Foundation® is a registered trademark of The Linux Foundation. Linux is a registered trademark of Linus Torvalds.
4 |
5 |
--------------------------------------------------------------------------------
/crates/integration-tests/src/tests/libvirt_upload_disk.rs:
--------------------------------------------------------------------------------
1 | //! Integration tests for libvirt-upload-disk command
2 | //!
3 | //! These tests verify the libvirt disk upload functionality, including:
4 | //! - Disk image creation via to-disk
5 | //! - Upload to libvirt storage pools
6 | //! - Container image metadata annotation
7 | //! - Error handling and validation
8 |
9 | // use crate::get_bck_command;
10 |
--------------------------------------------------------------------------------
/docs/book.toml:
--------------------------------------------------------------------------------
1 | [book]
2 | title = "bcvk Documentation"
3 | authors = ["Colin Walters"]
4 | language = "en"
5 | multilingual = false
6 | src = "src"
7 |
8 | [build]
9 | build-dir = "book"
10 |
11 | [preprocessor.mermaid]
12 | command = "mdbook-mermaid"
13 |
14 | [output.html]
15 | additional-js = ["mermaid.min.js", "mermaid-init.js"]
16 | git-repository-url = "https://github.com/cgwalters/bootc-kit"
17 | git-repository-icon = "fa-github"
--------------------------------------------------------------------------------
/docs/src/man/bcvk-libvirt-base-disks.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-libvirt-base-disks - Manage base disk images used for VM cloning
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk libvirt base-disks** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | Manage base disk images used for VM cloning
12 |
13 |
14 |
15 |
16 | # SEE ALSO
17 |
18 | **bcvk**(8)
19 |
20 | # VERSION
21 |
22 |
23 |
--------------------------------------------------------------------------------
/crates/integration-tests/fixtures/Dockerfile.no-kernel:
--------------------------------------------------------------------------------
1 | # Test fixture: Bootc image with kernel removed
2 | # This simulates a broken/malformed bootc image that will fail during ephemeral VM startup
3 | # Used to test error handling and cleanup in ephemeral run-ssh command
4 |
5 | ARG BASE_IMAGE=quay.io/centos-bootc/centos-bootc:stream10
6 |
7 | FROM ${BASE_IMAGE}
8 |
9 | # Remove kernel and modules to simulate a broken bootc image
10 | RUN rm -rf /usr/lib/modules
11 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-images.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-images - Manage and inspect bootc container images
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk images** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | Manage and inspect bootc container images
12 |
13 |
14 |
15 |
16 | # SUBCOMMANDS
17 |
18 | bcvk-images-list(8)
19 |
20 | : List available bootc images
21 |
22 | # EXAMPLES
23 |
24 | TODO: Add practical examples showing how to use this command.
25 |
26 | # SEE ALSO
27 |
28 | **bcvk**(8)
29 |
30 | # VERSION
31 |
32 | v0.1.0
33 |
--------------------------------------------------------------------------------
/.gemini/config.yaml:
--------------------------------------------------------------------------------
1 | # NOTE: This file is canonically maintained in
2 | #
3 | # DO NOT EDIT
4 | #
5 | # This config mainly overrides `summary: false` by default
6 | # as it's really noisy.
7 | have_fun: true
8 | code_review:
9 | disable: false
10 | # Even medium level can be quite noisy, I don't think
11 | # we need LOW. Anyone who wants that type of stuff should
12 | # be able to get it locally or before review.
13 | comment_severity_threshold: MEDIUM
14 | max_review_comments: -1
15 | pull_request_opened:
16 | help: false
17 | summary: false # turned off by default
18 | code_review: true
19 | ignore_patterns: []
20 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-ephemeral-ps.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-ephemeral-ps - List ephemeral VM containers
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk ephemeral ps** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | List ephemeral VM containers
12 |
13 | # OPTIONS
14 |
15 |
16 | **--json**
17 |
18 | Output as structured JSON instead of table format
19 |
20 |
21 |
22 | # EXAMPLES
23 |
24 | List all running ephemeral VMs:
25 |
26 | bcvk ephemeral ps
27 |
28 | List ephemeral VMs with JSON output:
29 |
30 | bcvk ephemeral ps --json
31 |
32 | # SEE ALSO
33 |
34 | **bcvk**(8)
35 |
36 | # VERSION
37 |
38 |
39 |
--------------------------------------------------------------------------------
/crates/xtask/Cargo.toml:
--------------------------------------------------------------------------------
1 | # See https://github.com/matklad/cargo-xtask
2 | [package]
3 | name = "xtask"
4 | version = "0.1.0"
5 | license = "MIT OR Apache-2.0"
6 | edition = "2021"
7 | publish = false
8 |
9 | [[bin]]
10 | name = "xtask"
11 | path = "src/xtask.rs"
12 |
13 | [dependencies]
14 | color-eyre = { workspace = true }
15 | cfg-if = { workspace = true }
16 | tracing = { workspace = true }
17 | tracing-subscriber = { workspace = true }
18 | tracing-error = { workspace = true }
19 | xshell = { workspace = true }
20 | anyhow = "1.0"
21 | camino = "1.1"
22 | serde = { version = "1.0", features = ["derive"] }
23 | serde_json = "1.0"
24 | tempfile = "3.0"
25 | toml = "0.8"
26 |
27 | [lints]
28 | workspace = true
29 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-libvirt-start.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-libvirt-start - Start a stopped libvirt domain
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk libvirt start** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | Start a stopped libvirt domain
12 |
13 | # OPTIONS
14 |
15 |
16 | **NAME**
17 |
18 | Name of the domain to start
19 |
20 | This argument is required.
21 |
22 | **--ssh**
23 |
24 | Automatically SSH into the domain after starting
25 |
26 |
27 |
28 | # EXAMPLES
29 |
30 | TODO: Add practical examples showing how to use this command.
31 |
32 | # SEE ALSO
33 |
34 | **bcvk**(8)
35 |
36 | # VERSION
37 |
38 |
39 |
--------------------------------------------------------------------------------
/crates/kit/src/common_opts.rs:
--------------------------------------------------------------------------------
1 | //! Common CLI options shared across commands
2 |
3 | use clap::Parser;
4 | use serde::{Deserialize, Serialize};
5 | use std::fmt;
6 |
7 | pub const DEFAULT_MEMORY_USER_STR: &str = "4G";
8 |
9 | /// Memory size options
10 | #[derive(Parser, Debug, Clone, Default, Serialize, Deserialize)]
11 | pub struct MemoryOpts {
12 | #[clap(
13 | long,
14 | default_value = DEFAULT_MEMORY_USER_STR,
15 | help = "Memory size (e.g. 4G, 2048M, or plain number for MB)"
16 | )]
17 | pub memory: String,
18 | }
19 |
20 | impl fmt::Display for MemoryOpts {
21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 | write!(f, "{}", self.memory)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/docs/src/libvirt-manage.md:
--------------------------------------------------------------------------------
1 | # VM Lifecycle Management
2 |
3 | ## Basic Operations
4 |
5 | ```bash
6 | # Create and start VM
7 | bcvk libvirt run --name myvm quay.io/fedora/fedora-bootc:42
8 |
9 | # Manage state
10 | bcvk libvirt start myvm
11 | bcvk libvirt stop myvm
12 |
13 | # Remove VM
14 | bcvk libvirt rm myvm
15 |
16 | # List VMs
17 | bcvk libvirt list
18 | ```
19 |
20 | ## Resource Configuration
21 |
22 | ```bash
23 | # Configure memory, CPU, and disk
24 | bcvk libvirt run --name myvm \
25 | --memory 8192 \
26 | --cpus 4 \
27 | --disk-size 50G \
28 | quay.io/fedora/fedora-bootc:42
29 | ```
30 |
31 | ## SSH Access
32 |
33 | ```bash
34 | bcvk libvirt ssh myvm
35 | ```
36 |
37 | See the [libvirt run guide](./libvirt-run.md) for more details.
--------------------------------------------------------------------------------
/docs/src/introduction.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | bcvk (bootc virtualization kit) runs bootc containers as virtual machines and creates bootable disk images.
4 |
5 | ## Features
6 |
7 | - **Ephemeral VMs**: Launch temporary VMs from bootc containers without root
8 | - **Disk Images**: Create bootable disk images in various formats
9 | - **libvirt Integration**: Manage persistent VMs with full lifecycle control
10 | - **Image Management**: Discover and list bootc container images
11 |
12 | ## Use Cases
13 |
14 | - Testing bootc images quickly in VM environments
15 | - Creating disk images for cloud or bare-metal deployment
16 | - CI/CD integration for automated testing
17 | - Development with isolated VM environments
18 |
19 | See the [Quick Start Guide](./quick-start.md) to get started.
--------------------------------------------------------------------------------
/docs/src/man/bcvk-libvirt-stop.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-libvirt-stop - Stop a running libvirt domain
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk libvirt stop** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | Stop a running libvirt domain
12 |
13 | # OPTIONS
14 |
15 |
16 | **NAME**
17 |
18 | Name of the domain to stop
19 |
20 | This argument is required.
21 |
22 | **-f**, **--force**
23 |
24 | Force stop the domain
25 |
26 | **--timeout**=*TIMEOUT*
27 |
28 | Timeout in seconds for graceful shutdown
29 |
30 | Default: 60
31 |
32 |
33 |
34 | # EXAMPLES
35 |
36 | TODO: Add practical examples showing how to use this command.
37 |
38 | # SEE ALSO
39 |
40 | **bcvk**(8)
41 |
42 | # VERSION
43 |
44 |
45 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bootc-devenv-debian",
3 | // TODO override this back to prod image
4 | "image": "ghcr.io/bootc-dev/devenv-debian",
5 | "customizations": {
6 | "vscode": {
7 | // Abitrary, but most of our code is in one of these two
8 | "extensions": [
9 | "rust-lang.rust-analyzer",
10 | "golang.Go"
11 | ]
12 | }
13 | },
14 | "features": {},
15 | "runArgs": [
16 | // Because we want to be able to run podman and also use e.g. /dev/kvm
17 | // among other things
18 | "--privileged"
19 | ],
20 | "postCreateCommand": {
21 | // Our init script
22 | "devenv-init": "sudo /usr/local/bin/devenv-init.sh"
23 | },
24 | "remoteEnv": {
25 | "PATH": "${containerEnv:PATH}:/usr/local/cargo/bin"
26 | }
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/.github/actions/setup-rust/action.yml:
--------------------------------------------------------------------------------
1 | name: 'Setup Rust'
2 | description: 'Install Rust toolchain with caching and nextest'
3 | runs:
4 | using: 'composite'
5 | steps:
6 | - name: Install Rust toolchain
7 | uses: dtolnay/rust-toolchain@stable
8 | - name: Install nextest
9 | uses: taiki-e/install-action@v2
10 | with:
11 | tool: nextest
12 | - name: Setup Rust cache
13 | uses: Swatinem/rust-cache@v2
14 | with:
15 | cache-all-crates: true
16 | # Only generate caches on push to git main
17 | save-if: ${{ github.ref == 'refs/heads/main' }}
18 | # Suppress actually using the cache for builds running from
19 | # git main so that we avoid incremental compilation bugs
20 | lookup-only: ${{ github.ref == 'refs/heads/main' }}
21 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-libvirt-rm.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-libvirt-rm - Remove a libvirt domain and its resources
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk libvirt rm** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | Remove a libvirt domain and its resources
12 |
13 | # OPTIONS
14 |
15 |
16 | **NAME**
17 |
18 | Name of the domain to remove
19 |
20 | This argument is required.
21 |
22 | **-f**, **--force**
23 |
24 | Force removal without confirmation (also stops running VMs)
25 |
26 | **--stop**
27 |
28 | Stop domain if it's running (implied by --force)
29 |
30 |
31 |
32 | # EXAMPLES
33 |
34 | TODO: Add practical examples showing how to use this command.
35 |
36 | # SEE ALSO
37 |
38 | **bcvk**(8)
39 |
40 | # VERSION
41 |
42 |
43 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-libvirt-status.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-libvirt-status - Show libvirt environment status and capabilities
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk libvirt status** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | Show libvirt environment status and capabilities
12 |
13 | # OPTIONS
14 |
15 |
16 | **--format**=*FORMAT*
17 |
18 | Output format (yaml or json)
19 |
20 | Possible values:
21 | - yaml
22 | - json
23 |
24 | Default: yaml
25 |
26 |
27 |
28 | # EXAMPLES
29 |
30 | Show libvirt environment status (default YAML format):
31 |
32 | bcvk libvirt status
33 |
34 | Show status in JSON format:
35 |
36 | bcvk libvirt status --format json
37 |
38 | # SEE ALSO
39 |
40 | **bcvk**(8)
41 |
42 | # VERSION
43 |
44 |
45 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-libvirt-inspect.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-libvirt-inspect - Show detailed information about a libvirt domain
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk libvirt inspect** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | Show detailed information about a libvirt domain
12 |
13 | # OPTIONS
14 |
15 |
16 | **NAME**
17 |
18 | Name of the domain to inspect
19 |
20 | This argument is required.
21 |
22 | **--format**=*FORMAT*
23 |
24 | Output format
25 |
26 | Possible values:
27 | - table
28 | - json
29 | - yaml
30 | - xml
31 |
32 | Default: yaml
33 |
34 |
35 |
36 | # EXAMPLES
37 |
38 | TODO: Add practical examples showing how to use this command.
39 |
40 | # SEE ALSO
41 |
42 | **bcvk**(8)
43 |
44 | # VERSION
45 |
46 |
47 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-ephemeral.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-ephemeral - Manage ephemeral VMs for bootc containers
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk ephemeral** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | Manage ephemeral VMs for bootc containers
12 |
13 |
14 |
15 |
16 | # EXAMPLES
17 |
18 | Run an ephemeral VM in the background:
19 |
20 | bcvk ephemeral run -d --rm --name mytestvm quay.io/fedora/fedora-bootc:42
21 |
22 | Run an ephemeral VM with custom resources:
23 |
24 | bcvk ephemeral run --memory 4096 --cpus 4 --name bigvm quay.io/fedora/fedora-bootc:42
25 |
26 | Run an ephemeral VM with automatic SSH key generation:
27 |
28 | bcvk ephemeral run -d --rm -K --name testvm quay.io/fedora/fedora-bootc:42
29 |
30 | # SEE ALSO
31 |
32 | **bcvk**(8)
33 |
34 | # VERSION
35 |
36 |
37 |
--------------------------------------------------------------------------------
/docs/src/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | ## Prerequisites
4 |
5 | Required:
6 | - [Rust](https://www.rust-lang.org/)
7 | - Git
8 | - QEMU/KVM
9 | - virtiofsd
10 | - Podman
11 |
12 | Optional:
13 | - libvirt (for persistent VM features)
14 | ```bash
15 | sudo systemctl enable --now libvirtd
16 | sudo usermod -a -G libvirt $USER
17 | ```
18 |
19 | ## Building from Source
20 |
21 | Without cloning the repo:
22 |
23 | ```bash
24 | cargo install --locked --git https://github.com/bootc-dev/bcvk bcvk
25 | ```
26 |
27 | Inside a clone of the repo:
28 |
29 | ```bash
30 | cargo install --locked --path crates/kit
31 | ```
32 |
33 | ## Platform Support
34 |
35 | - Linux: Supported
36 | - macOS: Not supported, use [podman-bootc](https://github.com/containers/podman-bootc/)
37 | - Windows: Not supported
38 |
39 | See the [Quick Start Guide](./quick-start.md) to begin using bcvk.
40 |
--------------------------------------------------------------------------------
/docs/src/image-management.md:
--------------------------------------------------------------------------------
1 | # Image Management
2 |
3 | ## Listing Images
4 |
5 | bcvk identifies bootc images by the `containers.bootc=1` label.
6 |
7 | ```bash
8 | # List all bootc images
9 | bcvk images list
10 | ```
11 |
12 | ## Finding bootc Images
13 |
14 | Common bootc images:
15 |
16 | - `quay.io/fedora/fedora-bootc:42`
17 | - `quay.io/centos-bootc/centos-bootc:stream10`
18 | - `registry.redhat.io/rhel9/rhel-bootc:latest`
19 |
20 | ## Pulling Images
21 |
22 | ```bash
23 | podman pull quay.io/fedora/fedora-bootc:42
24 | ```
25 |
26 | ## Building Custom Images
27 |
28 | ```dockerfile
29 | FROM quay.io/fedora/fedora-bootc:42
30 | LABEL containers.bootc=1
31 |
32 | RUN dnf install -y httpd && dnf clean all
33 | RUN systemctl enable httpd
34 | ```
35 |
36 | Build and test:
37 | ```bash
38 | podman build -t localhost/my-bootc:latest .
39 | bcvk ephemeral run-ssh localhost/my-bootc:latest
40 | ```
--------------------------------------------------------------------------------
/docs/src/man/bcvk-images-list.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-images-list - List all available bootc container images on the system
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk images list** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | List all available bootc container images on the system
12 |
13 | # OPTIONS
14 |
15 |
16 | **--json**
17 |
18 | Output as structured JSON instead of table format
19 |
20 |
21 |
22 | # EXAMPLES
23 |
24 | List all bootc images (those with containers.bootc=1 label):
25 |
26 | bcvk images list
27 |
28 | Get structured JSON output for scripting:
29 |
30 | bcvk images list --json
31 |
32 | Find available bootc containers on your system:
33 |
34 | # This helps you find available bootc containers before running VMs
35 | bcvk images list
36 |
37 | # SEE ALSO
38 |
39 | **bcvk**(8)
40 |
41 | # VERSION
42 |
43 | v0.1.0
44 |
--------------------------------------------------------------------------------
/crates/integration-tests/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "integration-tests"
3 | version = "0.1.0"
4 | edition = "2021"
5 | publish = false
6 |
7 | # Disable automatic binary discovery to avoid conflicts with test target
8 | autobins = false
9 |
10 | [[test]]
11 | name = "integration-tests"
12 | path = "src/main.rs"
13 | harness = false
14 |
15 | [[bin]]
16 | name = "test-cleanup"
17 | path = "src/bin/cleanup.rs"
18 |
19 | [dependencies]
20 | bcvk = { path = "../kit" }
21 | color-eyre = { workspace = true }
22 | dirs = "5.0"
23 | tracing = { workspace = true }
24 | tracing-subscriber = { workspace = true }
25 | tracing-error = { workspace = true }
26 | xshell = { workspace = true }
27 | cfg-if = { workspace = true }
28 | serde = "1.0.199"
29 | serde_json = "1.0.116"
30 | libtest-mimic = "0.7.3"
31 | tempfile = "3"
32 | uuid = { version = "1.18.1", features = ["v4"] }
33 | camino = "1.1.12"
34 | regex = "1"
35 | linkme = "0.3.30"
36 | paste = "1.0"
37 | rand = { workspace = true }
38 |
39 | [lints]
40 | workspace = true
41 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-ephemeral-rm-all.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-ephemeral-rm-all - Remove all ephemeral VM containers
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk ephemeral rm-all** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | Remove all ephemeral VM containers
12 |
13 | # OPTIONS
14 |
15 |
16 | **-f**, **--force**
17 |
18 | Force removal without confirmation
19 |
20 |
21 |
22 | # EXAMPLES
23 |
24 | Remove all ephemeral VMs (will prompt for confirmation):
25 |
26 | bcvk ephemeral rm-all
27 |
28 | Remove all ephemeral VMs without confirmation:
29 |
30 | bcvk ephemeral rm-all --force
31 |
32 | Clean up after testing workflow:
33 |
34 | # Run some ephemeral test VMs
35 | bcvk ephemeral run -d --rm --name test1 quay.io/fedora/fedora-bootc:42
36 | bcvk ephemeral run -d --rm --name test2 quay.io/fedora/fedora-bootc:42
37 |
38 | # Clean up all at once
39 | bcvk ephemeral rm-all -f
40 |
41 | # SEE ALSO
42 |
43 | **bcvk**(8)
44 |
45 | # VERSION
46 |
47 |
48 |
--------------------------------------------------------------------------------
/AGENTS.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Instructions for AI agents
4 |
5 | ## CRITICAL instructions for generating commits
6 |
7 | ### Signed-off-by
8 |
9 | Human review is required for all code that is generated
10 | or assisted by a large language model. If you
11 | are a LLM, you MUST NOT include a `Signed-off-by`
12 | on any automatically generated git commits. Only explicit
13 | human action or request should include a Signed-off-by.
14 | If for example you automatically create a pull request
15 | and the DCO check fails, tell the human to review
16 | the code and give them instructions on how to add
17 | a signoff.
18 |
19 | ### Attribution
20 |
21 | When generating substantial amounts of code, you SHOULD
22 | include an `Assisted-by: TOOLNAME (MODELNAME)`. For example,
23 | `Assisted-by: Goose (Sonnet 4.5)`.
24 |
25 | ## Follow other guidelines
26 |
27 | Look at the project README.md and look for guidelines
28 | related to contribution, such as a CONTRIBUTING.md
29 | and follow those.
30 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Permission is hereby granted, free of charge, to any
2 | person obtaining a copy of this software and associated
3 | documentation files (the "Software"), to deal in the
4 | Software without restriction, including without
5 | limitation the rights to use, copy, modify, merge,
6 | publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software
8 | is furnished to do so, subject to the following
9 | conditions:
10 |
11 | The above copyright notice and this permission notice
12 | shall be included in all copies or substantial portions
13 | of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23 | DEALINGS IN THE SOFTWARE.
24 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [ "crates/*" ]
3 | default-members = [ "crates/kit", "crates/xtask" ]
4 | resolver = "2"
5 |
6 | [workspace.dependencies]
7 | color-eyre = "0.6"
8 | const_format = "0.2"
9 | cfg-if = "1.0.0"
10 | rand = "0.9"
11 | tracing = "0.1"
12 | tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
13 | tracing-error = { version = "0.2.0" }
14 | xshell = "0.2.7"
15 |
16 | [workspace.lints.rust]
17 | # Require an extra opt-in for unsafe
18 | unsafe_code = "deny"
19 | # Absolutely must handle errors
20 | unused_must_use = "forbid"
21 | missing_docs = "deny"
22 | missing_debug_implementations = "deny"
23 | # Feel free to comment this one out locally during development of a patch.
24 | unused_imports = "deny"
25 | dead_code = "deny"
26 |
27 | [workspace.lints.clippy]
28 | disallowed_methods = "deny"
29 | # These should only be in local code
30 | dbg_macro = "deny"
31 | todo = "deny"
32 | # These two are in my experience the lints which are most likely
33 | # to trigger, and among the least valuable to fix.
34 | needless_borrow = "allow"
35 | needless_borrows_for_generic_args = "allow"
36 |
37 | [profile.dev.package.backtrace]
38 | opt-level = 3
39 |
--------------------------------------------------------------------------------
/docs/src/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 |
3 | [Introduction](./introduction.md)
4 |
5 | - [Installation](./installation.md)
6 | - [Quick Start](./quick-start.md)
7 | - [Workflow Comparison](./workflow-comparison.md)
8 |
9 | # Reference
10 |
11 | - [Command Reference](./man/bcvk.md)
12 | - [ephemeral](./man/bcvk-ephemeral.md)
13 | - [ephemeral run](./man/bcvk-ephemeral-run.md)
14 | - [ephemeral ssh](./man/bcvk-ephemeral-ssh.md)
15 | - [ephemeral run-ssh](./man/bcvk-ephemeral-run-ssh.md)
16 | - [to-disk](./man/bcvk-to-disk.md)
17 | - [images](./man/bcvk-images.md)
18 | - [images list](./man/bcvk-images-list.md)
19 | - [libvirt](./man/bcvk-libvirt.md)
20 | - [libvirt run](./man/bcvk-libvirt-run.md)
21 | - [libvirt list](./man/bcvk-libvirt-list.md)
22 | - [libvirt ssh](./man/bcvk-libvirt-ssh.md)
23 | - [libvirt stop](./man/bcvk-libvirt-stop.md)
24 | - [libvirt start](./man/bcvk-libvirt-start.md)
25 | - [libvirt inspect](./man/bcvk-libvirt-inspect.md)
26 | - [libvirt rm](./man/bcvk-libvirt-rm.md)
27 | - [libvirt upload](./man/bcvk-libvirt-upload.md)
28 | - [libvirt create](./man/bcvk-libvirt-create.md)
29 |
30 | # Development
31 |
32 | - [HACKING](HACKING.md)
--------------------------------------------------------------------------------
/docs/src/workflow-comparison.md:
--------------------------------------------------------------------------------
1 | # Workflow Comparison
2 |
3 | The bootable container ecosystem includes several tools that serve different use cases. Understanding when to use each tool helps you choose the right approach.
4 |
5 | ## Tool Overview
6 |
7 | - **bootc** - Core tool for building and managing bootable container images
8 | - **bcvk** - Virtualization toolkit for development and testing
9 | - **podman-bootc** - Podman-integrated solution for cross-platform development
10 |
11 | ## Quick Comparison
12 |
13 | | Tool | Best For | Key Strength |
14 | |------|----------|-------------|
15 | | **bootc** | Production deployment | Direct hardware installation |
16 | | **bcvk** | Development/testing | Fast VM workflows |
17 | | **podman-bootc** | Cross-platform dev | Consistent experience |
18 |
19 | ## When to Use bcvk
20 |
21 | - **Development iteration**: Quick testing of container changes
22 | - **Linux-focused workflows**: Leveraging native virtualization
23 | - **Integration testing**: Automated VM testing in CI/CD
24 | - **Performance testing**: Native KVM performance
25 |
26 | ## When to Use Alternatives
27 |
28 | - **podman-bootc**: Cross-platform teams, Podman workflows
29 | - **bootc**: Production deployment, bare metal installation
30 |
31 | Most teams use multiple tools: bcvk for development, bootc for production.
--------------------------------------------------------------------------------
/docs/src/ephemeral-ssh.md:
--------------------------------------------------------------------------------
1 | # SSH Access
2 |
3 | ## Overview
4 |
5 | bcvk provides SSH access to ephemeral VMs with automatic key management. SSH sessions can be direct (VM created on-demand) or connect to existing named VMs.
6 |
7 | ## Key Management
8 |
9 | Keys are generated automatically per VM and injected during creation. You can also provide your own public keys if needed.
10 |
11 | ## Usage Examples
12 |
13 | ### Quick debugging
14 |
15 | ```bash
16 | # Create VM, SSH directly, cleanup on exit
17 | bcvk ephemeral ssh quay.io/myapp/debug:latest
18 | ```
19 |
20 | ### Development environment
21 |
22 | ```bash
23 | # Create development VM
24 | bcvk ephemeral run -d --name dev-env \
25 | --bind ~/code:/workspace \
26 | quay.io/myapp/dev:latest
27 |
28 | # Connect when needed
29 | bcvk ephemeral ssh dev-env
30 | ```
31 |
32 | ### Running commands
33 |
34 | ```bash
35 | # Execute commands in VM
36 | bcvk ephemeral ssh test-vm "systemctl status myapp"
37 | ```
38 |
39 | ## Integration
40 |
41 | Standard SSH tools work with bcvk VMs:
42 |
43 | - **scp/sftp**: File transfer
44 | - **ssh -L/-R**: Port forwarding
45 | - **IDE remote development**: VS Code, JetBrains, etc.
46 |
47 | ## See Also
48 |
49 | - [bcvk-ephemeral-ssh(8)](./man/bcvk-ephemeral-ssh.md) - Command reference
50 | - [Ephemeral VM Concepts](./ephemeral-run.md) - VM lifecycle
--------------------------------------------------------------------------------
/.github/workflows/rebase.yml:
--------------------------------------------------------------------------------
1 | name: Automatic Rebase
2 | on:
3 | pull_request:
4 | types: [labeled]
5 |
6 | permissions:
7 | contents: read
8 |
9 | jobs:
10 | rebase:
11 | name: Rebase
12 | if: github.event.label.name == 'needs-rebase'
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Generate Actions Token
16 | id: token
17 | uses: actions/create-github-app-token@v2
18 | with:
19 | app-id: ${{ secrets.APP_ID }}
20 | private-key: ${{ secrets.APP_PRIVATE_KEY }}
21 | owner: ${{ github.repository_owner }}
22 |
23 | - name: Checkout
24 | uses: actions/checkout@v6
25 | with:
26 | token: ${{ steps.token.outputs.token }}
27 | fetch-depth: 0
28 |
29 | - name: Automatic Rebase
30 | uses: peter-evans/rebase@v4
31 | with:
32 | token: ${{ steps.token.outputs.token }}
33 |
34 | - name: Remove needs-rebase label
35 | if: always()
36 | uses: actions/github-script@v8
37 | with:
38 | github-token: ${{ steps.token.outputs.token }}
39 | script: |
40 | await github.rest.issues.removeLabel({
41 | owner: context.repo.owner,
42 | repo: context.repo.repo,
43 | issue_number: context.issue.number,
44 | name: 'needs-rebase'
45 | });
46 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk - A toolkit for bootable containers and (local) virtualization.
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk** \[**-h**\|**\--help**\] \<*subcommands*\>
8 |
9 | # DESCRIPTION
10 |
11 | bcvk helps launch bootc containers using local virtualization.
12 | Build containers using your tool of choice (podman, docker, etc),
13 | then use `bcvk libvirt run` to quickly and conveniently create
14 | a libvirt virtual machine, and connect with `ssh`.
15 |
16 | The toolkit includes commands for:
17 |
18 | - Running ephemeral VMs for testing container images
19 | - Installing bootc containers to persistent disk images
20 | - Managing libvirt integration and VM lifecycle
21 | - SSH access to running VMs
22 |
23 |
24 |
25 |
26 | # SUBCOMMANDS
27 |
28 | bcvk-images(8)
29 |
30 | : Manage and inspect bootc container images
31 |
32 | bcvk-run-ephemeral(8)
33 |
34 | : Run bootc containers as temporary VMs for testing and development
35 |
36 | bcvk-to-disk(8)
37 |
38 | : Install bootc images to persistent disk images
39 |
40 | bcvk-libvirt(8)
41 |
42 | : Manage libvirt integration for bootc containers
43 |
44 | bcvk-ssh(8)
45 |
46 | : Connect to running VMs via SSH
47 |
48 | bcvk-help(8)
49 |
50 | : Print this message or the help of the given subcommand(s)
51 |
52 | # VERSION
53 |
54 | v0.1.0
55 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-libvirt.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-libvirt - Manage libvirt integration for bootc containers
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk libvirt** \[**-h**\|**\--help**\] \<*subcommands*\>
8 |
9 | # DESCRIPTION
10 |
11 | Comprehensive libvirt integration with subcommands for uploading disk images,
12 | creating domains, and managing bootc containers as libvirt VMs.
13 |
14 | This command provides seamless integration between bcvk disk images and
15 | libvirt virtualization infrastructure, enabling:
16 |
17 | - Upload of disk images to libvirt storage pools
18 | - Creation of libvirt domains with appropriate bootc annotations
19 | - Management of VM lifecycle through libvirt
20 | - Integration with existing libvirt-based infrastructure
21 |
22 | # OPTIONS
23 |
24 |
25 | **-c**, **--connect**=*CONNECT*
26 |
27 | Hypervisor connection URI (e.g., qemu:///system, qemu+ssh://host/system)
28 |
29 |
30 |
31 | # SUBCOMMANDS
32 |
33 | bcvk-libvirt-upload(8)
34 |
35 | : Upload bootc disk images to libvirt storage pools
36 |
37 | bcvk-libvirt-create(8)
38 |
39 | : Create libvirt domains from bootc disk images
40 |
41 | bcvk-libvirt-list(8)
42 |
43 | : List bootc-related libvirt domains and storage
44 |
45 | bcvk-libvirt-help(8)
46 |
47 | : Print this message or the help of the given subcommand(s)
48 |
49 | # VERSION
50 |
51 | v0.1.0
--------------------------------------------------------------------------------
/docs/src/man/bcvk-libvirt-list-volumes.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-libvirt-list-volumes - List available bootc volumes with metadata
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk libvirt list-volumes** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | List available bootc volumes with metadata
12 |
13 | # OPTIONS
14 |
15 |
16 | **--pool**=*POOL*
17 |
18 | Libvirt storage pool name to search
19 |
20 | Default: default
21 |
22 | **--json**
23 |
24 | Output format (human-readable or JSON)
25 |
26 | **--detailed**
27 |
28 | Show detailed volume information
29 |
30 | **--source-image**=*SOURCE_IMAGE*
31 |
32 | Filter by source container image
33 |
34 | **--all**
35 |
36 | Show all volumes (not just bootc volumes)
37 |
38 |
39 |
40 | # EXAMPLES
41 |
42 | List all bootc volumes in the default pool:
43 |
44 | bcvk libvirt list-volumes
45 |
46 | Show detailed volume information:
47 |
48 | bcvk libvirt list-volumes --detailed
49 |
50 | Filter volumes by source container image:
51 |
52 | bcvk libvirt list-volumes --source-image quay.io/fedora/fedora-bootc:42
53 |
54 | List all volumes including non-bootc volumes:
55 |
56 | bcvk libvirt list-volumes --all
57 |
58 | Output as JSON for scripting:
59 |
60 | bcvk libvirt list-volumes --json
61 |
62 | # SEE ALSO
63 |
64 | **bcvk**(8)
65 |
66 | # VERSION
67 |
68 |
69 |
--------------------------------------------------------------------------------
/docs/src/libvirt-advanced.md:
--------------------------------------------------------------------------------
1 | # Advanced libvirt Usage
2 |
3 | ## Multi-VM Deployments
4 |
5 | You can run multiple VMs simultaneously, each with different container images or configurations.
6 |
7 | Create isolated VMs for different application tiers (web servers, databases, etc.) using separate libvirt networks for network segmentation.
8 |
9 | ## Storage Configuration
10 |
11 | Use libvirt storage pools to manage VM disk images. By default, bcvk creates VM disks in the default libvirt storage pool.
12 |
13 | For shared storage across VMs, configure libvirt storage pools backed by NFS, iSCSI, or other network storage.
14 |
15 | ## Network Configuration
16 |
17 | Create custom libvirt networks for VM isolation:
18 | - Use libvirt's network XML definitions
19 | - Configure NAT, routed, or isolated networks
20 | - Set up DHCP ranges and static IP assignments
21 |
22 | For direct host network access, use bridged networking or macvtap interfaces.
23 |
24 | ## Automation with Scripts
25 |
26 | Use shell scripts or configuration management tools to automate VM provisioning and management with bcvk libvirt commands.
27 |
28 | VM definitions can be templated and version-controlled alongside your container images.
29 |
30 | ## See Also
31 |
32 | - [bcvk-libvirt(8)](./man/bcvk-libvirt.md) - Command reference
33 | - [Libvirt Integration](./libvirt-run.md) - Basic usage
34 | - Libvirt documentation at libvirt.org for network and storage pool configuration
--------------------------------------------------------------------------------
/docs/mermaid-init.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | const initializeMermaid = () => {
3 | const theme = document.documentElement.classList.contains('light') ? 'neutral' : 'dark';
4 | const mermaidConfig = {
5 | startOnLoad: true,
6 | theme: theme,
7 | themeVariables: {
8 | darkMode: theme === 'dark'
9 | },
10 | flowchart: {
11 | useMaxWidth: true,
12 | htmlLabels: true,
13 | curve: 'basis'
14 | },
15 | securityLevel: 'loose'
16 | };
17 |
18 | if (window.mermaid) {
19 | window.mermaid.initialize(mermaidConfig);
20 | window.mermaid.init();
21 | }
22 | };
23 |
24 | // Initialize on load
25 | if (document.readyState === 'loading') {
26 | document.addEventListener('DOMContentLoaded', initializeMermaid);
27 | } else {
28 | initializeMermaid();
29 | }
30 |
31 | // Re-initialize when theme changes
32 | const observer = new MutationObserver((mutations) => {
33 | mutations.forEach((mutation) => {
34 | if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
35 | initializeMermaid();
36 | }
37 | });
38 | });
39 |
40 | observer.observe(document.documentElement, {
41 | attributes: true,
42 | attributeFilter: ['class']
43 | });
44 | })();
--------------------------------------------------------------------------------
/.config/nextest.toml:
--------------------------------------------------------------------------------
1 | # cargo-nextest configuration for bcvk
2 | # https://nexte.st/book/configuration
3 | #
4 | # Integration tests use libtest-mimic which is fully compatible with nextest
5 |
6 | [store]
7 | # Max test runtime data to collect
8 | # This helps with analyzing test performance
9 | dir = "target/nextest"
10 |
11 | [profile.default]
12 | # Standard settings for unit tests
13 | test-threads = "num-cpus"
14 | slow-timeout = { period = "30s", terminate-after = 2 }
15 | fail-fast = false
16 | failure-output = "immediate"
17 | success-output = "never"
18 | status-level = "pass"
19 |
20 | # Profile for integration tests - run with limited parallelism due to QEMU/KVM resources
21 | [profile.integration]
22 | test-threads = 2
23 | # Full 20 minutes for a timeout by default, since GHA runners can be really slow
24 | slow-timeout = { period = "1200s", terminate-after = 60 }
25 | fail-fast = false
26 | failure-output = "immediate"
27 | success-output = "never"
28 | status-level = "pass"
29 | # Only run integration tests with this profile
30 | default-filter = "package(integration-tests)"
31 |
32 | [profile.integration.junit]
33 | path = "junit.xml"
34 | store-success-output = true
35 | store-failure-output = true
36 |
37 | # Per-test overrides for libvirt tests that create VMs
38 | # Tests that run `bootc install` can be heavy, so require 4 threads to limit parallelism
39 | [[profile.integration.overrides]]
40 | filter = 'test(~^libvirt_run) | test(~^to_disk)'
41 | threads-required = 4
42 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-libvirt-ssh.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-libvirt-ssh - SSH to libvirt domain with embedded SSH key
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk libvirt ssh** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | SSH to libvirt domain with embedded SSH key
12 |
13 | # OPTIONS
14 |
15 |
16 | **DOMAIN_NAME**
17 |
18 | Name of the libvirt domain to connect to
19 |
20 | This argument is required.
21 |
22 | **COMMAND**
23 |
24 | Command to execute on remote host
25 |
26 | **--user**=*USER*
27 |
28 | SSH username to use for connection (defaults to 'root')
29 |
30 | Default: root
31 |
32 | **--strict-host-keys**
33 |
34 | Use strict host key checking
35 |
36 | **--timeout**=*TIMEOUT*
37 |
38 | SSH connection timeout in seconds
39 |
40 | Default: 30
41 |
42 | **--log-level**=*LOG_LEVEL*
43 |
44 | SSH log level
45 |
46 | Default: ERROR
47 |
48 | **--extra-options**=*EXTRA_OPTIONS*
49 |
50 | Extra SSH options in key=value format
51 |
52 |
53 |
54 | # EXAMPLES
55 |
56 | SSH into a running libvirt VM:
57 |
58 | bcvk libvirt ssh my-server
59 |
60 | Execute a command on the VM:
61 |
62 | bcvk libvirt ssh my-server 'systemctl status'
63 |
64 | SSH with a specific user:
65 |
66 | bcvk libvirt ssh --user admin my-server
67 |
68 | Connect to a VM with extended timeout:
69 |
70 | bcvk libvirt ssh --timeout 60 my-server
71 |
72 | # SEE ALSO
73 |
74 | **bcvk**(8)
75 |
76 | # VERSION
77 |
78 | v0.1.0
79 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-libvirt-rm-all.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-libvirt-rm-all - Remove multiple libvirt domains and their resources
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk libvirt rm-all** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | Remove multiple libvirt domains and their resources
12 |
13 | # OPTIONS
14 |
15 |
16 | **-f**, **--force**
17 |
18 | Force removal without confirmation
19 |
20 | **--stop**
21 |
22 | Remove domains even if they're running
23 |
24 | **--label**=*LABEL*
25 |
26 | Filter domains by label (only remove domains with this label)
27 |
28 |
29 |
30 | # EXAMPLES
31 |
32 | Remove all stopped libvirt VMs (will prompt for confirmation):
33 |
34 | bcvk libvirt rm-all
35 |
36 | Remove all VMs without confirmation:
37 |
38 | bcvk libvirt rm-all --force
39 |
40 | Remove all VMs including running ones:
41 |
42 | bcvk libvirt rm-all --stop --force
43 |
44 | Remove all VMs with a specific label:
45 |
46 | bcvk libvirt rm-all --label environment=test --force
47 |
48 | Clean up test environment workflow:
49 |
50 | # Create some test VMs
51 | bcvk libvirt run --name test1 --label purpose=testing quay.io/fedora/fedora-bootc:42
52 | bcvk libvirt run --name test2 --label purpose=testing quay.io/fedora/fedora-bootc:42
53 |
54 | # Clean up only the test VMs
55 | bcvk libvirt rm-all --label purpose=testing -f
56 |
57 | # SEE ALSO
58 |
59 | **bcvk**(8)
60 |
61 | # VERSION
62 |
63 |
64 |
--------------------------------------------------------------------------------
/crates/kit/src/podman.rs:
--------------------------------------------------------------------------------
1 | use std::process::Command;
2 |
3 | use bootc_utils::CommandRunExt;
4 | use color_eyre::{eyre::eyre, Result};
5 | use serde::Deserialize;
6 |
7 | #[derive(Debug, Deserialize)]
8 | #[serde(rename_all = "camelCase")]
9 | pub struct Store {
10 | #[allow(dead_code)]
11 | pub graph_driver_name: String,
12 | pub graph_root: String,
13 | }
14 |
15 | #[derive(Debug, Deserialize)]
16 | #[serde(rename_all = "camelCase")]
17 | pub struct PodmanSystemInfo {
18 | pub store: Store,
19 | }
20 |
21 | #[derive(Debug, Deserialize)]
22 | #[serde(rename_all = "PascalCase")]
23 | pub struct ImageInspect {
24 | pub size: u64,
25 | }
26 |
27 | pub fn get_system_info() -> Result {
28 | Command::new("podman")
29 | .arg("system")
30 | .arg("info")
31 | .arg("--format=json")
32 | .run_and_parse_json()
33 | .map_err(|e| eyre!("podman system info failed: {}", e))
34 | }
35 |
36 | /// Get the size of a container image in bytes
37 | pub fn get_image_size(image: &str) -> Result {
38 | let inspect_result: Vec = Command::new("podman")
39 | .arg("inspect")
40 | .arg("--format=json")
41 | .arg("--type=image")
42 | .arg(image)
43 | .run_and_parse_json()
44 | .map_err(|e| eyre!("podman inspect failed for image {}: {}", image, e))?;
45 |
46 | if inspect_result.is_empty() {
47 | return Err(eyre!("No image found for: {}", image));
48 | }
49 |
50 | Ok(inspect_result[0].size)
51 | }
52 |
--------------------------------------------------------------------------------
/docs/src/building.md:
--------------------------------------------------------------------------------
1 | # Building from Source
2 |
3 | ## Prerequisites
4 |
5 | Rust toolchain (install via rustup):
6 | ```bash
7 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
8 | ```
9 |
10 | Build dependencies:
11 | ```bash
12 | # Fedora/RHEL/CentOS
13 | sudo dnf install gcc pkg-config openssl-devel
14 |
15 | # Ubuntu/Debian
16 | sudo apt install build-essential pkg-config libssl-dev
17 | ```
18 |
19 | Runtime dependencies:
20 | ```bash
21 | # Fedora/RHEL/CentOS
22 | sudo dnf install qemu-kvm qemu-img podman virtiofsd
23 |
24 | # Ubuntu/Debian
25 | sudo apt install qemu-kvm qemu-utils podman virtiofsd
26 | ```
27 |
28 | Optional libvirt support:
29 | ```bash
30 | # Fedora/RHEL/CentOS
31 | sudo dnf install libvirt libvirt-daemon-kvm
32 | sudo systemctl enable --now libvirtd
33 | ```
34 |
35 | ## Clone and Build
36 |
37 | ```bash
38 | git clone https://github.com/bootc-dev/bcvk.git
39 | cd bcvk
40 | cargo build --release
41 | ```
42 |
43 | The binary will be at `target/release/bcvk`.
44 |
45 | ## Installation
46 |
47 | ```bash
48 | # Install to ~/.cargo/bin
49 | cargo install --path .
50 |
51 | # Or copy to system location
52 | sudo cp target/release/bcvk /usr/local/bin/
53 | ```
54 |
55 | ## Development
56 |
57 | ```bash
58 | cargo test # Run tests
59 | cargo fmt # Format code
60 | cargo clippy # Run linter
61 | ```
62 |
63 | Using `just` (if installed):
64 | ```bash
65 | just test
66 | just fmt
67 | just clippy
68 | ```
69 |
70 | See [testing.md](./testing.md) for details.
--------------------------------------------------------------------------------
/.github/workflows/openssf-scorecard.yml:
--------------------------------------------------------------------------------
1 | # Upstream https://github.com/ossf/scorecard/blob/main/.github/workflows/scorecard-analysis.yml
2 | # Tweaked to not pin actions by SHA digest as I think that's overkill noisy security theater.
3 | name: OpenSSF Scorecard analysis
4 | on:
5 | push:
6 | branches:
7 | - main
8 |
9 | permissions: read-all
10 |
11 | jobs:
12 | analysis:
13 | name: Scorecard analysis
14 | runs-on: ubuntu-24.04
15 | permissions:
16 | # Needed for Code scanning upload
17 | security-events: write
18 | # Needed for GitHub OIDC token if publish_results is true
19 | id-token: write
20 |
21 | steps:
22 | - name: "Checkout code"
23 | uses: actions/checkout@v6
24 | with:
25 | persist-credentials: false
26 |
27 | - name: "Run analysis"
28 | uses: ossf/scorecard-action@v2.4.3
29 | with:
30 | results_file: results.sarif
31 | results_format: sarif
32 | # Scorecard team runs a weekly scan of public GitHub repos,
33 | # see https://github.com/ossf/scorecard#public-data.
34 | # Setting `publish_results: true` helps us scale by leveraging your workflow to
35 | # extract the results instead of relying on our own infrastructure to run scans.
36 | # And it's free for you!
37 | publish_results: true
38 |
39 | - name: "Upload artifact"
40 | uses: actions/upload-artifact@v6
41 | with:
42 | name: SARIF file
43 | path: results.sarif
44 | retention-days: 5
45 |
46 | - name: "Upload to code-scanning"
47 | uses: github/codeql-action/upload-sarif@v4
48 | with:
49 | sarif_file: results.sarif
50 |
51 |
--------------------------------------------------------------------------------
/docs/src/quick-start.md:
--------------------------------------------------------------------------------
1 | # Quick Start
2 |
3 | ## Prerequisites
4 |
5 | - bcvk installed (see [Installation Guide](./installation.md))
6 | - podman
7 | - QEMU/KVM
8 | - A bootc container image
9 |
10 | ## Your First VM
11 |
12 | ```bash
13 | bcvk ephemeral run-ssh quay.io/fedora/fedora-bootc:42
14 | ```
15 |
16 | This starts a VM and automatically SSHs into it. The VM terminates when you exit the SSH session.
17 |
18 | ## Ephemeral VMs
19 |
20 | ```bash
21 | # Start a background VM with auto-cleanup
22 | bcvk ephemeral run -d --rm -K --name mytestvm quay.io/fedora/fedora-bootc:42
23 |
24 | # SSH into it
25 | bcvk ephemeral ssh mytestvm
26 | ```
27 |
28 | ## Creating Disk Images
29 |
30 | ```bash
31 | # Raw disk image
32 | bcvk to-disk quay.io/centos-bootc/centos-bootc:stream10 /path/to/disk.img
33 |
34 | # qcow2 format
35 | bcvk to-disk --format qcow2 quay.io/fedora/fedora-bootc:42 /path/to/fedora.qcow2
36 |
37 | # Custom size
38 | bcvk to-disk --size 20G quay.io/fedora/fedora-bootc:42 /path/to/large-disk.img
39 | ```
40 |
41 | ## Persistent VMs with libvirt
42 |
43 | ```bash
44 | # Create and start
45 | bcvk libvirt run --name my-server quay.io/fedora/fedora-bootc:42
46 |
47 | # Manage lifecycle
48 | bcvk libvirt ssh my-server
49 | bcvk libvirt stop my-server
50 | bcvk libvirt start my-server
51 | bcvk libvirt list
52 | bcvk libvirt rm my-server
53 | ```
54 |
55 | ## Image Management
56 |
57 | ```bash
58 | # List bootc images
59 | bcvk images list
60 | ```
61 |
62 | ## Resource Configuration
63 |
64 | ```bash
65 | # Ephemeral VM
66 | bcvk ephemeral run --memory 4096 --cpus 4 --name bigvm quay.io/fedora/fedora-bootc:42
67 |
68 | # libvirt VM
69 | bcvk libvirt run --name webserver --memory 8192 --cpus 8 --disk-size 50G quay.io/centos-bootc/centos-bootc:stream10
70 | ```
--------------------------------------------------------------------------------
/docs/src/man/bcvk-libvirt-upload.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-libvirt-upload - Upload bootc disk images to libvirt with metadata annotations
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk libvirt upload** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | Upload bootc disk images to libvirt with metadata annotations
12 |
13 | # OPTIONS
14 |
15 |
16 | **SOURCE_IMAGE**
17 |
18 | Container image to install and upload
19 |
20 | This argument is required.
21 |
22 | **--volume-name**=*VOLUME_NAME*
23 |
24 | Name for the libvirt volume (defaults to sanitized image name)
25 |
26 | **--pool**=*POOL*
27 |
28 | Libvirt storage pool name
29 |
30 | Default: default
31 |
32 | **--disk-size**=*DISK_SIZE*
33 |
34 | Size of the disk image (e.g., '20G', '10240M'). If not specified, uses the actual size of the created disk
35 |
36 | **--filesystem**=*FILESYSTEM*
37 |
38 | Root filesystem type (e.g. ext4, xfs, btrfs)
39 |
40 | **--root-size**=*ROOT_SIZE*
41 |
42 | Root filesystem size (e.g., '10G', '5120M')
43 |
44 | **--storage-path**=*STORAGE_PATH*
45 |
46 | Path to host container storage (auto-detected if not specified)
47 |
48 | **--target-transport**=*TARGET_TRANSPORT*
49 |
50 | The transport; e.g. oci, oci-archive, containers-storage. Defaults to `registry`
51 |
52 | **--karg**=*KARG*
53 |
54 | Set a kernel argument
55 |
56 | **--composefs-backend**
57 |
58 | Default to composefs-native storage
59 |
60 | **--memory**=*MEMORY*
61 |
62 | Memory size (e.g. 4G, 2048M, or plain number for MB)
63 |
64 | Default: 4G
65 |
66 | **--vcpus**=*VCPUS*
67 |
68 | Number of vCPUs for installation VM
69 |
70 |
71 |
72 | # EXAMPLES
73 |
74 | TODO: Add practical examples showing how to use this command.
75 |
76 | # SEE ALSO
77 |
78 | **bcvk**(8)
79 |
80 | # VERSION
81 |
82 | v0.1.0
83 |
--------------------------------------------------------------------------------
/crates/kit/src/qemu_img.rs:
--------------------------------------------------------------------------------
1 | //! Helper functions for interacting with qemu-img
2 |
3 | use camino::Utf8Path;
4 | use color_eyre::{eyre::Context, Result};
5 | use serde::Deserialize;
6 | use std::process::Command;
7 |
8 | /// Information returned by `qemu-img info --output=json`
9 | #[derive(Debug, Deserialize)]
10 | #[serde(rename_all = "kebab-case")]
11 | #[allow(dead_code)]
12 | pub struct QemuImgInfo {
13 | /// Virtual size of the disk image in bytes
14 | pub virtual_size: u64,
15 | /// Path to the disk image file
16 | pub filename: String,
17 | /// Image format (e.g., "qcow2", "raw")
18 | pub format: String,
19 | /// Actual size on disk in bytes (if available)
20 | pub actual_size: Option,
21 | /// Cluster size in bytes (for formats like qcow2)
22 | pub cluster_size: Option,
23 | /// Backing file name (if this is a snapshot)
24 | pub backing_filename: Option,
25 | /// Full path to backing file (if this is a snapshot)
26 | pub full_backing_filename: Option,
27 | /// Whether the image is marked as dirty
28 | pub dirty_flag: Option,
29 | }
30 |
31 | /// Run `qemu-img info --force-share --output=json` on a disk image
32 | ///
33 | /// The `--force-share` flag allows reading disk info even when the image
34 | /// is locked by a running VM.
35 | pub fn info(path: &Utf8Path) -> Result {
36 | let output = Command::new("qemu-img")
37 | .args(["info", "--force-share", "--output=json", path.as_str()])
38 | .output()
39 | .with_context(|| format!("Failed to run qemu-img info on {:?}", path))?;
40 |
41 | if !output.status.success() {
42 | let stderr = String::from_utf8_lossy(&output.stderr);
43 | return Err(color_eyre::eyre::eyre!(
44 | "qemu-img info failed for {:?}: {}",
45 | path,
46 | stderr
47 | ));
48 | }
49 |
50 | serde_json::from_slice(&output.stdout)
51 | .with_context(|| format!("Failed to parse qemu-img info JSON for {:?}", path))
52 | }
53 |
--------------------------------------------------------------------------------
/crates/kit/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "bcvk"
3 | version = "0.9.0"
4 | edition = "2021"
5 | publish = false
6 |
7 | [dependencies]
8 | base64 = "0.22"
9 | # For some recent APIs, TODO switch back to published version
10 | cap-std-ext = { git = "https://github.com/coreos/cap-std-ext", rev = "cfdb25d51ffc697e70aa0d8d3cefe9ec2133bd0a" }
11 | chrono = { version = "0.4", features = ["serde"] }
12 | const_format = { workspace = true }
13 | color-eyre = { workspace = true }
14 | clap = { version = "4.4", features = ["derive"] }
15 | clap_mangen = { version = "0.2.20", optional = true }
16 | data-encoding = { version = "2.9" }
17 | dirs = "5.0"
18 | fn-error-context = { version = "0.2" }
19 | bootc-mount = { git = "https://github.com/bootc-dev/bootc", rev = "93b22f4dbc2d54f7cca7c1df3ee59fcdec0b2cf1" }
20 | bootc-utils = { git = "https://github.com/bootc-dev/bootc", rev = "93b22f4dbc2d54f7cca7c1df3ee59fcdec0b2cf1" }
21 | indicatif = "0.17"
22 | notify = "6.1"
23 | thiserror = "1.0"
24 | rustix = { "version" = "1", features = ["thread", "net", "fs", "pipe", "system", "process", "mount"] }
25 | serde = { version = "1.0.199", features = ["derive"] }
26 | serde_json = "1.0.116"
27 | serde_yaml = "0.9"
28 | tokio = { version = "1", features = ["full"] }
29 | tracing = { workspace = true }
30 | tracing-subscriber = { workspace = true }
31 | tracing-error = { workspace = true }
32 | shlex = "1"
33 | reqwest = { version = "0.12", features = ["blocking"] }
34 | tempfile = "3"
35 | uuid = { version = "1.10", features = ["v4"] }
36 | xshell = { workspace = true }
37 | yaml-rust2 = "0.9"
38 | rand = { workspace = true }
39 | cfg-if = { workspace = true }
40 | indoc = "2.0.6"
41 | regex = "1.10"
42 | itertools = "0.14.0"
43 | vsock = "0.5"
44 | nix = { version = "0.29", features = ["socket"] }
45 | libc = "0.2"
46 | camino = "1.1.12"
47 | comfy-table = "7.1"
48 | strum = { version = "0.26", features = ["derive"] }
49 | quick-xml = "0.36"
50 | oci-spec = "0.8.2"
51 | sha2 = "0.10"
52 | which = "7.0"
53 |
54 | [dev-dependencies]
55 | similar-asserts = "1.5"
56 |
57 | [features]
58 | # Implementation detail of man page generation.
59 | docgen = ["clap_mangen"]
60 |
61 | [lints]
62 | workspace = true
63 |
--------------------------------------------------------------------------------
/docs/src/ephemeral-run.md:
--------------------------------------------------------------------------------
1 | # Ephemeral VMs
2 |
3 | Ephemeral VMs are temporary virtual machines that start quickly from bootc container images and automatically clean up when stopped.
4 |
5 | ## Basic Usage
6 |
7 | ```bash
8 | # Quick test with auto-cleanup
9 | bcvk ephemeral run --rm quay.io/fedora/fedora-bootc:42
10 |
11 | # Background VM
12 | bcvk ephemeral run -d --name myvm quay.io/fedora/fedora-bootc:42
13 |
14 | # With SSH
15 | bcvk ephemeral run-ssh quay.io/fedora/fedora-bootc:42
16 | ```
17 |
18 | ## Resource Configuration
19 |
20 | ```bash
21 | # Custom resources
22 | bcvk ephemeral run --memory 4096 --cpus 4 --name myvm quay.io/fedora/fedora-bootc:42
23 | ```
24 |
25 | ## Detecting an ephemeral environment
26 |
27 | Conceptually now with `bcvk ephemeral`, there's *four* different ways to run
28 | a bootc container:
29 |
30 | - `podman|docker run bash` - directly run a shell (or other process) the container without systemd. Uses the host kernel, not kernel in the container.
31 | - `podman|docker run ` - by default runs systemd. See also . Uses the host kernel, not kernel in the container.
32 | - `bootc install` - Run directly on metal or a virtualized environment. Uses the kernel in the container.
33 | - `bcvk ephemeral` - Run as a virtual machine, but *not* a true "bootc install". Uses the kernel in the container.
34 |
35 | Some systemd units may need adaption to work in all of these modes. For example, if you have a systemd generator
36 | which synthesizes mount units for expected partitions, it can use `ConditionVirtualization=!container` to skip
37 | those in the first two cases (ensuring it still runs after a `bootc install`), but that won't be skipped in `bcvk ephemeral`
38 | even though there won't be any block devices (by default).
39 |
40 | At the current time there is not a dedicated way to detect `bcvk ephemeral`, but `ConditionKernelCommandLine=!rootfstype=virtiofs`
41 | should work reliably in the future.
42 |
43 | ## Use Cases
44 |
45 | - Quick testing of bootc images
46 | - Development environments
47 | - CI/CD integration
48 | - Isolated experimentation
49 |
50 | See [ephemeral-ssh](./ephemeral-ssh.md) for SSH access details.
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Documentation
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | paths: [ 'docs/**' ]
7 | pull_request:
8 | branches: [ main ]
9 | paths: [ 'docs/**' ]
10 | workflow_dispatch:
11 |
12 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
13 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
14 | concurrency:
15 | group: "pages"
16 | cancel-in-progress: false
17 |
18 | jobs:
19 | build:
20 | runs-on: ubuntu-latest
21 | steps:
22 | - name: Checkout
23 | uses: actions/checkout@v4
24 |
25 | - name: Install mdBook
26 | run: |
27 | curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.37/mdbook-v0.4.37-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=.
28 | echo "$(pwd)" >> $GITHUB_PATH
29 |
30 | - name: Install mdbook-mermaid
31 | run: |
32 | curl -sSL https://github.com/badboy/mdbook-mermaid/releases/download/v0.13.0/mdbook-mermaid-v0.13.0-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=.
33 | echo "$(pwd)" >> $GITHUB_PATH
34 |
35 | - name: Install mdbook-linkcheck
36 | run: |
37 | curl -sSL https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/download/v0.7.7/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip -o linkcheck.zip
38 | unzip -o linkcheck.zip
39 | chmod +x mdbook-linkcheck
40 | echo "$(pwd)" >> $GITHUB_PATH
41 |
42 | - name: Build documentation
43 | run: |
44 | cd docs
45 | mdbook build
46 |
47 | - name: Upload artifact
48 | if: github.ref == 'refs/heads/main'
49 | uses: actions/upload-pages-artifact@v3
50 | with:
51 | path: docs/book
52 |
53 | deploy:
54 | if: github.ref == 'refs/heads/main'
55 | environment:
56 | name: github-pages
57 | url: ${{ steps.deployment.outputs.page_url }}
58 | runs-on: ubuntu-latest
59 | needs: build
60 | permissions:
61 | pages: write
62 | id-token: write
63 | steps:
64 | - name: Deploy to GitHub Pages
65 | id: deployment
66 | uses: actions/deploy-pages@v4
--------------------------------------------------------------------------------
/crates/kit/src/libvirt/stop.rs:
--------------------------------------------------------------------------------
1 | //! libvirt stop command - stop a running bootc domain
2 | //!
3 | //! This module provides functionality to stop running libvirt domains
4 | //! that were created from bootc container images.
5 |
6 | use clap::Parser;
7 | use color_eyre::Result;
8 |
9 | /// Options for stopping a libvirt domain
10 | #[derive(Debug, Parser)]
11 | pub struct LibvirtStopOpts {
12 | /// Name of the domain to stop
13 | pub name: String,
14 |
15 | /// Force stop the domain
16 | #[clap(long, short = 'f')]
17 | pub force: bool,
18 |
19 | /// Timeout in seconds for graceful shutdown
20 | #[clap(long, default_value = "60")]
21 | pub timeout: u32,
22 | }
23 |
24 | /// Execute the libvirt stop command
25 | pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtStopOpts) -> Result<()> {
26 | use crate::domain_list::DomainLister;
27 | use color_eyre::eyre::Context;
28 |
29 | let connect_uri = global_opts.connect.as_ref();
30 | let lister = match connect_uri {
31 | Some(uri) => DomainLister::with_connection(uri.clone()),
32 | None => DomainLister::new(),
33 | };
34 |
35 | // Check if domain exists and get its state
36 | let state = lister
37 | .get_domain_state(&opts.name)
38 | .map_err(|_| color_eyre::eyre::eyre!("VM '{}' not found", opts.name))?;
39 |
40 | if state != "running" {
41 | println!("VM '{}' is already stopped (state: {})", opts.name, state);
42 | return Ok(());
43 | }
44 |
45 | println!("🛑 Stopping VM '{}'...", opts.name);
46 |
47 | // Use virsh to stop the domain
48 | let mut cmd = global_opts.virsh_command();
49 | if opts.force {
50 | cmd.args(&["destroy", &opts.name]);
51 | } else {
52 | cmd.args(&["shutdown", &opts.name]);
53 | }
54 |
55 | let output = cmd
56 | .output()
57 | .with_context(|| "Failed to run virsh command")?;
58 |
59 | if !output.status.success() {
60 | let stderr = String::from_utf8_lossy(&output.stderr);
61 | return Err(color_eyre::eyre::eyre!(
62 | "Failed to stop VM '{}': {}",
63 | opts.name,
64 | stderr
65 | ));
66 | }
67 |
68 | println!("VM '{}' stopped successfully", opts.name);
69 | Ok(())
70 | }
71 |
--------------------------------------------------------------------------------
/docs/src/testing.md:
--------------------------------------------------------------------------------
1 | # Testing
2 |
3 | ## Prerequisites
4 |
5 | Integration tests require KVM/QEMU and podman. For libvirt tests, ensure libvirtd is running and your user has libvirt group membership.
6 |
7 | ## Running Tests
8 |
9 | ```bash
10 | # Unit tests
11 | just test
12 | cargo test --lib
13 |
14 | # Integration tests (requires KVM, podman)
15 | just test-integration
16 | cargo test --test integration
17 |
18 | # Specific test with output
19 | cargo test test_name -- --nocapture
20 | ```
21 |
22 | ## Unit Tests
23 |
24 | Place tests in `#[cfg(test)] mod tests` blocks within source files. Use nested modules to organize related tests.
25 |
26 | ## Integration Tests
27 |
28 | Integration tests in `tests/` require network access to pull bootc images, working virtualization, and disk space. VMs need time to boot - account for this in test timeouts.
29 |
30 | ```bash
31 | # Pull test images first
32 | podman pull quay.io/fedora/fedora-bootc:42
33 | ```
34 |
35 | ## Manual Testing
36 |
37 | ```bash
38 | # Ephemeral VM
39 | bcvk ephemeral run -d --rm --name test quay.io/fedora/fedora-bootc:42
40 | podman stop test
41 |
42 | # SSH access
43 | bcvk ephemeral run -d -K --name ssh-test quay.io/fedora/fedora-bootc:42
44 | bcvk ephemeral ssh ssh-test "hostname"
45 | podman stop ssh-test
46 |
47 | # Disk image
48 | bcvk to-disk quay.io/fedora/fedora-bootc:42 /tmp/test.img
49 |
50 | # libvirt
51 | bcvk libvirt run --name test quay.io/fedora/fedora-bootc:42
52 | bcvk libvirt ssh test "uptime"
53 | bcvk libvirt rm test
54 | ```
55 |
56 | ## Cleanup
57 |
58 | ```bash
59 | # Remove test containers
60 | podman ps -a | grep test | awk '{print $1}' | xargs -r podman rm -f
61 |
62 | # Remove test disk images
63 | rm -f /tmp/test-*.img
64 |
65 | # Remove libvirt test VMs
66 | virsh list --all | grep test | awk '{print $2}' | xargs -r virsh destroy
67 | virsh list --all | grep test | awk '{print $2}' | xargs -r virsh undefine
68 | ```
69 |
70 | ## Debugging
71 |
72 | ```bash
73 | # Run test with full output
74 | cargo test test_name -- --nocapture --test-threads=1
75 |
76 | # Debug logging
77 | RUST_LOG=debug cargo test test_name
78 |
79 | # Check KVM access
80 | ls -la /dev/kvm
81 |
82 | # Check VM is running
83 | podman ps | grep test-vm
84 | podman logs test-vm
85 | ```
86 |
87 | ## Coverage
88 |
89 | ```bash
90 | cargo install cargo-tarpaulin
91 | cargo tarpaulin --out Html
92 | ```
--------------------------------------------------------------------------------
/docs/src/man/bcvk-ephemeral-ssh.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-ephemeral-ssh - Connect to running VMs via SSH
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk ephemeral ssh** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | Connect to running ephemeral VMs via SSH. This command provides SSH access to VMs created by **bcvk-ephemeral-run**(8).
12 |
13 | ## VM Lifecycle Management
14 |
15 | When using SSH with ephemeral VMs, the VM lifecycle can be bound to the SSH connection depending on how the VM was started:
16 |
17 | - **Background VMs** (started with `-d`): The VM continues running independently after SSH disconnection
18 | - **Interactive VMs** (started without `-d`): The VM terminates when SSH disconnects
19 | - **Auto-cleanup VMs** (started with `--rm`): The VM and container are automatically removed when the VM stops
20 |
21 | For the **bcvk-ephemeral-run-ssh**(8) command, the VM lifecycle is tightly coupled to the SSH session - when the SSH client terminates (e.g., by running `exit`), the entire VM and its container are automatically cleaned up.
22 |
23 | # OPTIONS
24 |
25 |
26 | **CONTAINER_NAME**
27 |
28 | Name or ID of the container running the target VM
29 |
30 | This argument is required.
31 |
32 | **ARGS**
33 |
34 | SSH arguments like -v, -L, -o
35 |
36 |
37 |
38 | # EXAMPLES
39 |
40 | Connect to a running ephemeral VM:
41 |
42 | bcvk ephemeral ssh mytestvm
43 |
44 | Connect with SSH verbose mode:
45 |
46 | bcvk ephemeral ssh mytestvm -v
47 |
48 | Connect with port forwarding:
49 |
50 | bcvk ephemeral ssh mytestvm -L 8080:localhost:80
51 |
52 | VM lifecycle examples:
53 |
54 | # Start a background VM (continues after SSH disconnect)
55 | bcvk ephemeral run -d --name persistent-vm quay.io/fedora/fedora-bootc:42
56 | bcvk ephemeral ssh persistent-vm
57 | # VM keeps running after 'exit'
58 |
59 | # Start an auto-cleanup VM (removes when stopped)
60 | bcvk ephemeral run -d --rm --name temp-vm quay.io/fedora/fedora-bootc:42
61 | bcvk ephemeral ssh temp-vm
62 | # VM and container auto-removed when VM stops
63 |
64 | # For tightly-coupled lifecycle, use run-ssh instead
65 | bcvk ephemeral run-ssh quay.io/fedora/fedora-bootc:42
66 | # VM terminates automatically when SSH session ends
67 |
68 | # SEE ALSO
69 |
70 | **bcvk**(8), **bcvk-ephemeral**(8), **bcvk-ephemeral-run**(8), **bcvk-ephemeral-run-ssh**(8)
71 |
72 | # VERSION
73 |
74 | v0.1.0
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | prefix ?= /usr
2 |
3 | SOURCE_DATE_EPOCH ?= $(shell git log -1 --pretty=%ct)
4 | # https://reproducible-builds.org/docs/archives/
5 | TAR_REPRODUCIBLE = tar --mtime="@${SOURCE_DATE_EPOCH}" --sort=name --owner=0 --group=0 --numeric-owner --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime
6 |
7 | all: bin manpages
8 |
9 | .PHONY: bin
10 | bin:
11 | cargo check --workspace
12 | cargo build --release
13 |
14 | # Generate man pages from markdown sources
15 | MAN8_SOURCES := $(wildcard docs/src/man/*.md)
16 | TARGETMAN := target/man
17 | MAN8_TARGETS := $(patsubst docs/src/man/%.md,$(TARGETMAN)/%.8,$(MAN8_SOURCES))
18 |
19 | # Single rule for generating man pages
20 | $(TARGETMAN)/%.8: docs/src/man/%.md
21 | @mkdir -p $(TARGETMAN)
22 | @# Create temp file with synced content
23 | @cp $< $<.tmp
24 | @# Generate man page using go-md2man
25 | @go-md2man -in $<.tmp -out $@
26 | @# Fix apostrophe handling
27 | @sed -i -e "1i .ds Aq \\\\(aq" -e "/\\.g \\.ds Aq/d" -e "/.el .ds Aq \'/d" $@
28 | @rm -f $<.tmp
29 | @echo "Generated $@"
30 |
31 | # Sync CLI options before generating man pages
32 | .PHONY: manpages
33 | manpages: sync-cli-options $(MAN8_TARGETS)
34 |
35 | # Hidden target to sync CLI options once
36 | sync-cli-options:
37 | @cargo xtask sync-manpages >/dev/null 2>&1 || true
38 |
39 | # This gates CI by default. Note that for clippy, we gate on
40 | # only the clippy correctness and suspicious lints, plus a select
41 | # set of default rustc warnings.
42 | # We intentionally don't gate on this for local builds in cargo.toml
43 | # because it impedes iteration speed.
44 | CLIPPY_CONFIG = -A clippy::all -D clippy::correctness -D clippy::suspicious -D clippy::disallowed-methods -Dunused_imports -Ddead_code
45 | validate:
46 | cargo fmt -- --check -l
47 | cargo test --no-run --workspace
48 | cargo clippy -- $(CLIPPY_CONFIG)
49 | env RUSTDOCFLAGS='-D warnings' cargo doc --lib
50 | .PHONY: validate
51 |
52 | install:
53 | install -D -m 0755 -t $(DESTDIR)$(prefix)/bin target/release/bcvk
54 | if [ -n "$(MAN8_TARGETS)" ]; then \
55 | install -D -m 0644 -t $(DESTDIR)$(prefix)/share/man/man8 $(MAN8_TARGETS); \
56 | fi
57 |
58 | makesudoinstall:
59 | make
60 | sudo make install
61 |
62 | sync-manpages:
63 | cargo xtask sync-manpages
64 |
65 | update-manpages:
66 | cargo xtask update-manpages
67 |
68 | update-generated: sync-manpages manpages
69 |
70 | .PHONY: all bin install manpages update-generated makesudoinstall sync-manpages update-manpages sync-cli-options
71 |
--------------------------------------------------------------------------------
/crates/kit/src/boot_progress.rs:
--------------------------------------------------------------------------------
1 | use color_eyre::Result;
2 | use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
3 | use std::{fs::File, io::BufRead, time::Duration};
4 |
5 | use crate::supervisor_status::{StatusWriter, SupervisorState, SupervisorStatus};
6 |
7 | const SSH_ACCESS: &str = "ssh-access.target";
8 |
9 | /// Create a progress bar for boot status
10 | pub fn create_boot_progress_bar() -> ProgressBar {
11 | let pb = ProgressBar::new_spinner();
12 | pb.set_draw_target(ProgressDrawTarget::stderr());
13 | pb.set_style(
14 | ProgressStyle::default_bar()
15 | .template("{spinner:.green} {msg}")
16 | .unwrap(),
17 | );
18 | pb.enable_steady_tick(Duration::from_millis(100));
19 | pb.set_message("Starting VM...");
20 | pb
21 | }
22 |
23 | /// Monitor systemd boot progress and update progress bar
24 | pub async fn monitor_boot_progress(piper: File, status_writer: StatusWriter) -> Result<()> {
25 | // Update status to indicate we're waiting for systemd
26 | status_writer.update_state(SupervisorState::WaitingForSystemd)?;
27 |
28 | let bufr = std::io::BufReader::new(piper);
29 |
30 | let mut ssh_access = false;
31 | for line in bufr.lines() {
32 | let line = line?;
33 | let line = line.trim();
34 |
35 | let Some((k, v)) = line.split_once('=') else {
36 | tracing::trace!("Unhandled status line: {line}");
37 | continue;
38 | };
39 | tracing::debug!("Got systemd notification: {k}={v}");
40 | match k {
41 | "READY" => {
42 | let state = SupervisorState::ReachedTarget(v.to_owned());
43 | status_writer.update(SupervisorStatus {
44 | state: Some(state),
45 | ssh_access,
46 | running: true,
47 | })?;
48 | }
49 | "X_SYSTEMD_UNIT_ACTIVE" => {
50 | let state = SupervisorState::ReachedTarget(v.to_owned());
51 | if v == SSH_ACCESS {
52 | ssh_access = true;
53 | }
54 | status_writer.update(SupervisorStatus {
55 | state: Some(state),
56 | ssh_access,
57 | running: true,
58 | })?;
59 | }
60 | _ => {
61 | tracing::trace!("Unhandled status line: {line}")
62 | }
63 | }
64 | }
65 |
66 | Ok(())
67 | }
68 |
--------------------------------------------------------------------------------
/docs/HACKING.md:
--------------------------------------------------------------------------------
1 | # Working on this project
2 |
3 | Be sure you've read [README.md](`../README.md`) too of course.
4 |
5 | ## Building
6 |
7 | - There is a `Justfile` which supports commands, read it and use it.
8 | This wraps generic tools like `cargo check`, `cargo build`, and `cargo test` to verify
9 | the project compiles and unit tests work. This assumes these tools are in the current
10 | host environment.
11 | - Use `just test-integration` to run the integration tests.
12 |
13 | ## Testing
14 |
15 | ### Unit tests
16 | ```bash
17 | just test
18 | ```
19 |
20 | ### Integration tests
21 | ```bash
22 | # Run all integration tests
23 | just test-integration
24 |
25 | # Run a specific integration test
26 | just test-integration-single
27 |
28 | # Examples:
29 | just test-integration-single run_ephemeral_with_storage
30 | just test-integration-single to_disk
31 | ```
32 |
33 | Integration tests require QEMU/KVM to be fully working as they launch actual VMs.
34 |
35 | ## Running
36 |
37 | Ensure the entrypoint script is in `$PATH`, i.e. that `bcvk` works.
38 |
39 | Then you can invoke `bcvk`.
40 |
41 | ## Code formatting
42 |
43 | - Always run `cargo fmt` before making a git commit, and in
44 | general at the end of a series of code edits.
45 |
46 | ## Code style
47 |
48 | Some use of emoji is OK, but avoid using it gratuitously. Especially
49 | don't use bulleted lists where each entry has an emoji prefix.
50 |
51 | ## Commit messages
52 |
53 | The commit message should be structured as follows:
54 |
55 | [optional scope]:
56 |
57 | [optional body]
58 |
59 | [optional footer]
60 |
61 | The commit contains the following structural elements, to communicate intent to the consumers of your library:
62 |
63 | - fix: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in semantic versioning).
64 | - feat: a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in semantic versioning).
65 | - BREAKING CHANGE: a commit that has the text BREAKING CHANGE: at the beginning of its optional body or footer section introduces a breaking API change (correlating with MAJOR in semantic versioning). A breaking change can be part of commits of any type. e.g., a fix:, feat: & chore: types would all be valid, in addition to any other type.
66 |
67 | DO NOT include `Generated with Claude Code` or `Co-authored-by: Claude`.
68 | You should include `Assisted-by: Claude ` though
69 | especially for nontrivial changes that did not require substantial assistance from
70 | a human.
71 |
--------------------------------------------------------------------------------
/crates/kit/scripts/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 |
4 | SELFEXE=/run/selfexe
5 |
6 | # Check for required binaries early
7 | if ! command -v bwrap &>/dev/null; then
8 | echo "Error: bwrap (bubblewrap) is currently required in the target container image" >&2
9 | exit 1
10 | fi
11 |
12 | # Shell script library
13 | init_tmproot() {
14 | if test -d /run/inner-shared; then return 0; fi
15 | # Should have been created by podman when initializing
16 | # the bind mount
17 | cd /run/tmproot
18 |
19 | # Create essential symlinks
20 | ln -sf usr/bin bin
21 | ln -sf usr/lib lib
22 | ln -sf usr/lib64 lib64
23 | ln -sf usr/sbin sbin
24 | mkdir -p {etc,var,dev,proc,run,sys,tmp}
25 | # Ensure we have /etc/passwd as ssh-keygen wants it for bad reasons
26 | systemd-sysusers --root $(pwd) &>/dev/null
27 |
28 | # Copy DNS configuration from container's /etc/resolv.conf (configured by podman --dns)
29 | # into the bwrap namespace so QEMU's slirp can use it for DNS resolution
30 | if [ -f /etc/resolv.conf ]; then
31 | cp /etc/resolv.conf /run/tmproot/etc/resolv.conf
32 | fi
33 |
34 | # Shared directory between containers
35 | mkdir /run/inner-shared
36 | }
37 |
38 | BWRAP_ARGS=(
39 | --bind /run/tmproot /
40 | --proc /proc
41 | --dev-bind /dev /dev
42 | --bind /var/tmp /var/tmp
43 | --tmpfs /run
44 | --tmpfs /tmp
45 | --bind /run/inner-shared /run/inner-shared
46 | )
47 |
48 | # Pass ALL arguments to container-entrypoint
49 | # Default to "run-ephemeral" if no args
50 | if [[ $# -eq 0 ]]; then
51 | set -- "run-ephemeral"
52 | # Initialize environment
53 | init_tmproot
54 | else
55 | # Other commands should wait for the other process
56 | # to create the temp root
57 | while test '!' -d /run/inner-shared; do sleep 0.1; done
58 | fi
59 |
60 | # Check systemd version from the container image (not host)
61 | export SYSTEMD_VERSION=$(systemctl --version 2>/dev/null)
62 |
63 | # Execute with proper environment passing
64 | # Set up signal handlers that will cleanly exit on INT or TERM
65 | trap 'kill -TERM $BWRAP_PID 2>/dev/null; exit 0' INT TERM
66 |
67 | # Run bwrap in background so we can handle signals; xref
68 | # https://github.com/containers/bubblewrap/pull/586
69 | # But probably really we should switch to systemd
70 | bwrap --as-pid-1 --unshare-pid "${BWRAP_ARGS[@]}" --bind /run /run -- ${SELFEXE} container-entrypoint "$@" &
71 | BWRAP_PID=$!
72 |
73 | # Wait for bwrap to complete
74 | wait $BWRAP_PID
75 | EXIT_CODE=$?
76 |
77 | # Exit with the same code as bwrap
78 | exit $EXIT_CODE
79 |
--------------------------------------------------------------------------------
/crates/kit/src/install_options.rs:
--------------------------------------------------------------------------------
1 | //! Common installation options shared across bcvk commands
2 | //!
3 | //! This module provides shared configuration structures for disk installation
4 | //! operations, ensuring consistency across to-disk, libvirt-upload-disk,
5 | //! and other installation-related commands.
6 |
7 | use camino::Utf8PathBuf;
8 | use clap::Parser;
9 |
10 | /// Common installation options for bootc disk operations
11 | ///
12 | /// These options control filesystem configuration and storage paths
13 | /// for bootc installation commands. Use `#[clap(flatten)]` to include
14 | /// these in command-specific option structures.
15 | #[derive(Debug, Default, Parser, Clone)]
16 | pub struct InstallOptions {
17 | /// Root filesystem type (overrides bootc image default)
18 | #[clap(long, help = "Root filesystem type (e.g. ext4, xfs, btrfs)")]
19 | pub filesystem: Option,
20 |
21 | /// Custom root filesystem size (e.g., '10G', '5120M')
22 | #[clap(long, help = "Root filesystem size (e.g., '10G', '5120M')")]
23 | pub root_size: Option,
24 |
25 | /// Path to host container storage (auto-detected if not specified)
26 | #[clap(
27 | long,
28 | help = "Path to host container storage (auto-detected if not specified)"
29 | )]
30 | pub storage_path: Option,
31 |
32 | /// The transport; e.g. oci, oci-archive, containers-storage. Defaults to `registry`
33 | #[clap(long)]
34 | pub target_transport: Option,
35 |
36 | #[clap(long)]
37 | /// Set a kernel argument
38 | pub karg: Vec,
39 |
40 | /// Default to composefs-native storage
41 | #[clap(long)]
42 | pub composefs_backend: bool,
43 | }
44 |
45 | impl InstallOptions {
46 | /// Get the bootc install command arguments for these options
47 | pub fn to_bootc_args(&self) -> Vec {
48 | let mut args = vec![];
49 |
50 | if let Some(ref filesystem) = self.filesystem {
51 | args.push("--filesystem".to_string());
52 | args.push(filesystem.clone());
53 | }
54 |
55 | if let Some(ref root_size) = self.root_size {
56 | args.push("--root-size".to_string());
57 | args.push(root_size.clone());
58 | }
59 |
60 | for k in self.karg.iter() {
61 | args.push(format!("--karg={k}"));
62 | }
63 |
64 | if let Some(ref t) = self.target_transport {
65 | args.push("--target-transport".to_string());
66 | args.push(t.clone());
67 | }
68 |
69 | if self.composefs_backend {
70 | args.push("--composefs-backend".to_owned());
71 | }
72 |
73 | args
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-libvirt-list.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-libvirt-list - List available bootc volumes with metadata
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk libvirt list** [*DOMAIN_NAME*] [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | List available bootc domains with metadata. When a domain name is provided, returns information about that specific domain only.
12 |
13 | When using `--format=json` with a specific domain name, the output is a single JSON object (not an array), making it easy to extract SSH credentials and connection information using tools like `jq`.
14 |
15 | # OPTIONS
16 |
17 | **DOMAIN_NAME**
18 |
19 | Optional domain name to query. When specified, returns information about only this domain.
20 |
21 | # OPTIONS
22 |
23 |
24 | **DOMAIN_NAME**
25 |
26 | Domain name to query (returns only this domain)
27 |
28 | **--format**=*FORMAT*
29 |
30 | Output format
31 |
32 | Possible values:
33 | - table
34 | - json
35 | - yaml
36 | - xml
37 |
38 | Default: table
39 |
40 | **-a**, **--all**
41 |
42 | Show all domains including stopped ones
43 |
44 | **--label**=*LABEL*
45 |
46 | Filter domains by label
47 |
48 |
49 |
50 | # EXAMPLES
51 |
52 | List all running bootc VMs:
53 |
54 | bcvk libvirt list
55 |
56 | List all bootc VMs including stopped ones:
57 |
58 | bcvk libvirt list --all
59 |
60 | Show VM status in your workflow:
61 |
62 | # Check what VMs are running
63 | bcvk libvirt list
64 |
65 | # Start a specific VM if needed
66 | bcvk libvirt start my-server
67 |
68 | Query a specific domain:
69 |
70 | bcvk libvirt list my-domain
71 |
72 | ## Working with SSH credentials via JSON output
73 |
74 | Connect via SSH using extracted credentials:
75 |
76 | # Query once, save to file, then extract credentials
77 | DOMAIN_NAME="mydomain"
78 |
79 | # Query domain info once and save to file
80 | bcvk libvirt list $DOMAIN_NAME --format=json > /tmp/domain-info.json
81 |
82 | # Extract SSH private key
83 | jq -r '.ssh_private_key' /tmp/domain-info.json > /tmp/key.pem
84 | chmod 600 /tmp/key.pem
85 |
86 | # Extract SSH port
87 | SSH_PORT=$(jq -r '.ssh_port' /tmp/domain-info.json)
88 |
89 | # Connect via SSH
90 | ssh -o IdentitiesOnly=yes -i /tmp/key.pem -p $SSH_PORT -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@127.0.0.1
91 |
92 | # Cleanup
93 | rm /tmp/domain-info.json /tmp/key.pem
94 |
95 | This is useful for automation scripts or when you need direct SSH access without using `bcvk libvirt ssh`.
96 |
97 | # SEE ALSO
98 |
99 | **bcvk**(8)
100 |
101 | # VERSION
102 |
103 | v0.1.0
104 |
--------------------------------------------------------------------------------
/crates/kit/src/supervisor_status.rs:
--------------------------------------------------------------------------------
1 | use cap_std_ext::{cap_std, cap_std::fs::Dir, dirext::CapStdExtDirExt};
2 | use color_eyre::Result;
3 | use serde::{Deserialize, Serialize};
4 | use std::fs;
5 | use std::path::Path;
6 |
7 | /// Status of the supervisor process and VM
8 | #[derive(Debug, Default, Clone, Serialize, Deserialize)]
9 | #[serde(rename_all = "snake_case")]
10 | pub struct SupervisorStatus {
11 | /// Current state of the supervisor/VM
12 | pub state: Option,
13 | /// If we saw ssh-access.target
14 | pub ssh_access: bool,
15 | /// True if qemu is running
16 | pub running: bool,
17 | }
18 |
19 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20 | #[serde(rename_all = "snake_case")]
21 | pub enum SupervisorState {
22 | /// Waiting for systemd to become ready
23 | WaitingForSystemd,
24 | /// Systemd reached a specific target
25 | ReachedTarget(String),
26 | /// VM is ready and accepting connections
27 | Ready,
28 | }
29 |
30 | impl SupervisorStatus {
31 | /// Create a new status with the given state
32 | pub fn new(state: SupervisorState) -> Self {
33 | Self {
34 | state: Some(state),
35 | running: true,
36 | ..Default::default()
37 | }
38 | }
39 |
40 | /// Write status to a JSON file atomically
41 | pub fn write_to_file(&self, path: impl AsRef) -> color_eyre::Result<()> {
42 | let path = path.as_ref();
43 | let json = serde_json::to_string_pretty(self)?;
44 |
45 | // Get parent directory for atomic write
46 | let parent = path.parent().unwrap_or(Path::new("/"));
47 | let filename = path.file_name().unwrap_or_else(|| path.as_os_str());
48 |
49 | let dir = Dir::open_ambient_dir(parent, cap_std::ambient_authority())?;
50 | dir.atomic_write(filename, json)?;
51 |
52 | Ok(())
53 | }
54 |
55 | /// Read status from a JSON file
56 | pub fn read_from_file(path: impl AsRef) -> color_eyre::Result {
57 | let contents = fs::read_to_string(path)?;
58 | Ok(serde_json::from_str(&contents)?)
59 | }
60 | }
61 |
62 | /// Helper to write status updates from the supervisor
63 | pub struct StatusWriter {
64 | path: String,
65 | }
66 |
67 | impl StatusWriter {
68 | pub fn new(path: impl Into) -> Self {
69 | Self { path: path.into() }
70 | }
71 |
72 | pub fn update(&self, status: SupervisorStatus) -> color_eyre::Result<()> {
73 | status.write_to_file(&self.path)
74 | }
75 |
76 | pub fn update_state(&self, state: SupervisorState) -> color_eyre::Result<()> {
77 | self.update(SupervisorStatus::new(state))
78 | }
79 |
80 | pub fn finish(self) -> Result<()> {
81 | self.update(SupervisorStatus {
82 | running: false,
83 | ..Default::default()
84 | })
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/crates/kit/src/systemd.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Display;
2 | use std::process::Command;
3 |
4 | use color_eyre::eyre::{eyre, Context as _};
5 | use color_eyre::Result;
6 |
7 | /// A systemd version
8 | #[derive(Debug, Clone)]
9 | pub struct SystemdVersion(pub u32);
10 |
11 | impl SystemdVersion {
12 | pub fn from_version_output(o: &str) -> Result {
13 | let Some(num) = o
14 | .lines()
15 | .next()
16 | .and_then(|v| v.split_ascii_whitespace().nth(1))
17 | else {
18 | return Err(eyre!("Failed to find systemd version"));
19 | };
20 | let num: u32 = num
21 | .parse()
22 | .with_context(|| format!("Parsing systemd version: {num}"))?;
23 | Ok(Self(num))
24 | }
25 |
26 | #[allow(dead_code)]
27 | pub fn new_current() -> Result {
28 | let o = Command::new("systemctl").arg("--version").output()?;
29 | let o = String::from_utf8_lossy(&o.stdout);
30 | Self::from_version_output(&o)
31 | }
32 |
33 | /// https://www.freedesktop.org/software/systemd/man/latest/systemd.html#vmm.notify_socket
34 | pub fn has_vmm_notify(&self) -> bool {
35 | self.0 >= 254
36 | }
37 | }
38 |
39 | impl Display for SystemdVersion {
40 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 | write!(f, "systemd {}", self.0)
42 | }
43 | }
44 |
45 | #[cfg(test)]
46 | mod tests {
47 | use super::*;
48 |
49 | #[test]
50 | fn test_parse_systemd_version() {
51 | // Test typical systemd version output
52 | let output = "systemd 254 (254.5-1.fc39)\n+PAM +AUDIT +SELINUX";
53 | let version = SystemdVersion::from_version_output(output).unwrap();
54 | assert_eq!(version.0, 254);
55 | assert!(version.has_vmm_notify());
56 |
57 | // Test older version
58 | let output = "systemd 253 (253.1-1.fc38)\n+PAM +AUDIT +SELINUX";
59 | let version = SystemdVersion::from_version_output(output).unwrap();
60 | assert_eq!(version.0, 253);
61 | assert!(!version.has_vmm_notify());
62 |
63 | // Test different format (minimal)
64 | let output = "systemd 249";
65 | let version = SystemdVersion::from_version_output(output).unwrap();
66 | assert_eq!(version.0, 249);
67 | assert!(!version.has_vmm_notify());
68 |
69 | // Test newer version
70 | let output = "systemd 257 (257.7-1.fc42)";
71 | let version = SystemdVersion::from_version_output(output).unwrap();
72 | assert_eq!(version.0, 257);
73 | assert!(version.has_vmm_notify());
74 | }
75 |
76 | #[test]
77 | fn test_invalid_version_output() {
78 | let output = "";
79 | assert!(SystemdVersion::from_version_output(output).is_err());
80 |
81 | let output = "invalid output";
82 | assert!(SystemdVersion::from_version_output(output).is_err());
83 |
84 | let output = "systemd";
85 | assert!(SystemdVersion::from_version_output(output).is_err());
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/crates/kit/src/libvirt/start.rs:
--------------------------------------------------------------------------------
1 | //! libvirt start command - start a stopped bootc domain
2 | //!
3 | //! This module provides functionality to start stopped libvirt domains
4 | //! that were created from bootc container images.
5 |
6 | use clap::Parser;
7 | use color_eyre::Result;
8 |
9 | /// Options for starting a libvirt domain
10 | #[derive(Debug, Parser)]
11 | pub struct LibvirtStartOpts {
12 | /// Name of the domain to start
13 | pub name: String,
14 |
15 | /// Automatically SSH into the domain after starting
16 | #[clap(long)]
17 | pub ssh: bool,
18 | }
19 |
20 | /// Execute the libvirt start command
21 | pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtStartOpts) -> Result<()> {
22 | use crate::domain_list::DomainLister;
23 | use color_eyre::eyre::Context;
24 |
25 | let connect_uri = global_opts.connect.as_ref();
26 | let lister = match connect_uri {
27 | Some(uri) => DomainLister::with_connection(uri.clone()),
28 | None => DomainLister::new(),
29 | };
30 |
31 | // Check if domain exists and get its state
32 | let state = lister
33 | .get_domain_state(&opts.name)
34 | .map_err(|_| color_eyre::eyre::eyre!("VM '{}' not found", opts.name))?;
35 |
36 | if state == "running" {
37 | println!("VM '{}' is already running", opts.name);
38 | if opts.ssh {
39 | println!("🔗 Connecting to running VM...");
40 | let ssh_opts = crate::libvirt::ssh::LibvirtSshOpts {
41 | domain_name: opts.name,
42 | user: "root".to_string(),
43 | command: vec![],
44 | strict_host_keys: false,
45 | timeout: 30,
46 | log_level: "ERROR".to_string(),
47 | extra_options: vec![],
48 | suppress_output: false,
49 | };
50 | return crate::libvirt::ssh::run(global_opts, ssh_opts);
51 | }
52 | return Ok(());
53 | }
54 |
55 | println!("Starting VM '{}'...", opts.name);
56 |
57 | // Use virsh to start the domain
58 | let output = global_opts
59 | .virsh_command()
60 | .args(&["start", &opts.name])
61 | .output()
62 | .with_context(|| "Failed to run virsh start")?;
63 |
64 | if !output.status.success() {
65 | let stderr = String::from_utf8_lossy(&output.stderr);
66 | return Err(color_eyre::eyre::eyre!(
67 | "Failed to start VM '{}': {}",
68 | opts.name,
69 | stderr
70 | ));
71 | }
72 |
73 | println!("VM '{}' started successfully", opts.name);
74 |
75 | if opts.ssh {
76 | // Use the libvirt SSH functionality directly
77 | let ssh_opts = crate::libvirt::ssh::LibvirtSshOpts {
78 | domain_name: opts.name,
79 | user: "root".to_string(),
80 | command: vec![],
81 | strict_host_keys: false,
82 | timeout: 30,
83 | log_level: "ERROR".to_string(),
84 | extra_options: vec![],
85 | suppress_output: false,
86 | };
87 | crate::libvirt::ssh::run(global_opts, ssh_opts)
88 | } else {
89 | Ok(())
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/crates/kit/src/libvirt/inspect.rs:
--------------------------------------------------------------------------------
1 | //! libvirt inspect command - show detailed information about a bootc domain
2 | //!
3 | //! This module provides functionality to display detailed information about
4 | //! libvirt domains that were created from bootc container images.
5 |
6 | use clap::Parser;
7 | use color_eyre::Result;
8 |
9 | use super::OutputFormat;
10 |
11 | /// Options for inspecting a libvirt domain
12 | #[derive(Debug, Parser)]
13 | pub struct LibvirtInspectOpts {
14 | /// Name of the domain to inspect
15 | pub name: String,
16 |
17 | /// Output format
18 | #[clap(long, value_enum, default_value_t = OutputFormat::Yaml)]
19 | pub format: OutputFormat,
20 | }
21 |
22 | /// Execute the libvirt inspect command
23 | pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtInspectOpts) -> Result<()> {
24 | use crate::domain_list::DomainLister;
25 | use color_eyre::eyre::Context;
26 |
27 | let connect_uri = global_opts.connect.as_ref();
28 | let lister = match connect_uri {
29 | Some(uri) => DomainLister::with_connection(uri.clone()),
30 | None => DomainLister::new(),
31 | };
32 |
33 | // Get domain info
34 | let vm = lister
35 | .get_domain_info(&opts.name)
36 | .map_err(|_| color_eyre::eyre::eyre!("VM '{}' not found", opts.name))?;
37 |
38 | match opts.format {
39 | OutputFormat::Yaml => {
40 | println!("name: {}", vm.name);
41 | if let Some(ref image) = vm.image {
42 | println!("image: {}", image);
43 | }
44 | println!("status: {}", vm.status_string());
45 | if let Some(memory) = vm.memory_mb {
46 | println!("memory_mb: {}", memory);
47 | }
48 | if let Some(vcpus) = vm.vcpus {
49 | println!("vcpus: {}", vcpus);
50 | }
51 | if let Some(ref disk_path) = vm.disk_path {
52 | println!("disk_path: {}", disk_path);
53 | }
54 | }
55 | OutputFormat::Json => {
56 | println!(
57 | "{}",
58 | serde_json::to_string_pretty(&vm)
59 | .with_context(|| "Failed to serialize VM as JSON")?
60 | );
61 | }
62 | OutputFormat::Xml => {
63 | // Output raw domain XML using virsh dumpxml
64 | let mut cmd = global_opts.virsh_command();
65 | cmd.args(["dumpxml", &opts.name]);
66 | let output = cmd
67 | .output()
68 | .with_context(|| format!("Failed to run virsh dumpxml for {}", opts.name))?;
69 |
70 | if !output.status.success() {
71 | return Err(color_eyre::eyre::eyre!(
72 | "Failed to get domain XML: {}",
73 | String::from_utf8_lossy(&output.stderr)
74 | ));
75 | }
76 |
77 | print!("{}", String::from_utf8_lossy(&output.stdout));
78 | }
79 | OutputFormat::Table => {
80 | return Err(color_eyre::eyre::eyre!(
81 | "Table format is not supported for inspect command"
82 | ))
83 | }
84 | }
85 | Ok(())
86 | }
87 |
--------------------------------------------------------------------------------
/docs/src/libvirt-run.md:
--------------------------------------------------------------------------------
1 | # Libvirt Integration
2 |
3 | The `bcvk libvirt run` command creates persistent virtual machines from bootc containers. It generates a disk image using `bcvk to-disk` and provisions a VM managed through libvirt.
4 |
5 | ## Basic Usage
6 |
7 | ```bash
8 | # Create a VM
9 | bcvk libvirt run quay.io/myapp/server:latest
10 |
11 | # With specific resources
12 | bcvk libvirt run \
13 | --name production-api \
14 | --memory 8192 \
15 | --cpus 4 \
16 | quay.io/myapp/api:v1.0
17 |
18 | # Development setup with SSH
19 | bcvk libvirt run \
20 | --name dev-environment \
21 | --memory 4096 \
22 | --cpus 2 \
23 | --ssh \
24 | quay.io/myapp/dev:latest
25 | ```
26 |
27 | ## Host Directory Mounting
28 |
29 | Share host directories with the VM using virtiofs:
30 |
31 | ```bash
32 | bcvk libvirt run \
33 | --volume /home/chris/projects/foo:src \
34 | --volume /home/chris/data:data \
35 | --ssh \
36 | quay.io/myapp/dev:latest
37 | ```
38 |
39 | Format: `--volume HOST_PATH:TAG` where TAG is the virtiofs mount tag.
40 |
41 | Mount in the guest:
42 |
43 | ```bash
44 | mkdir -p /mnt/src
45 | mount -t virtiofs src /mnt/src
46 | ```
47 |
48 | ## Container Storage Integration
49 |
50 | Access host container storage for bootc upgrades:
51 |
52 | ```bash
53 | bcvk libvirt run \
54 | --bind-storage-ro \
55 | quay.io/fedora/fedora-bootc:42
56 | ```
57 |
58 | This provisions a virtiofs mount named `hoststorage`. Mount in the guest:
59 |
60 | ```bash
61 | mkdir -p /run/virtiofs-mnt-hoststorage
62 | mount -t virtiofs hoststorage /run/virtiofs-mnt-hoststorage
63 | ```
64 |
65 | Use with bootc:
66 |
67 | ```bash
68 | env STORAGE_OPTS=additionalimagestore=/run/virtiofs-mnt-hoststorage \
69 | bootc switch --transport containers-storage localhost/bootc
70 |
71 | env STORAGE_OPTS=additionalimagestore=/run/virtiofs-mnt-hoststorage \
72 | bootc upgrade
73 | ```
74 |
75 | ## Secure Boot
76 |
77 | Prerequisites:
78 | ```bash
79 | sudo dnf install -y edk2-ovmf python3-virt-firmware openssl
80 | ```
81 |
82 | Enable Secure Boot with existing keys:
83 |
84 | ```bash
85 | bcvk libvirt run --firmware uefi-secure --secure-boot-keys /path/to/keys quay.io/myimage:latest
86 | ```
87 |
88 | Generate keys:
89 |
90 | ```bash
91 | mkdir -p ./my-secure-boot-keys
92 | cd ./my-secure-boot-keys
93 |
94 | openssl req -newkey rsa:4096 -nodes -keyout PK.key -new -x509 -sha256 -days 3650 -subj '/CN=Platform Key/' -out PK.crt
95 | openssl req -newkey rsa:4096 -nodes -keyout KEK.key -new -x509 -sha256 -days 3650 -subj '/CN=Key Exchange Key/' -out KEK.crt
96 | openssl req -newkey rsa:4096 -nodes -keyout db.key -new -x509 -sha256 -days 3650 -subj '/CN=Signature Database key/' -out db.crt
97 | uuidgen > GUID.txt
98 | ```
99 |
100 | Required files: PK.crt, KEK.crt, db.crt, GUID.txt
101 |
102 | Firmware options:
103 | - `--firmware uefi-secure` (default)
104 | - `--firmware uefi-insecure`
105 | - `--firmware bios`
106 |
107 | Verify in the guest:
108 | ```bash
109 | mokutil --sb-state
110 | ```
111 |
112 | ## See Also
113 |
114 | - [bcvk-libvirt-run(8)](./man/bcvk-libvirt-run.md) - Command reference
115 | - [Advanced Workflows](./libvirt-advanced.md) - Complex deployment patterns
--------------------------------------------------------------------------------
/Justfile:
--------------------------------------------------------------------------------
1 | PRIMARY_IMAGE := "quay.io/centos-bootc/centos-bootc:stream10"
2 | # TODO: Readd quay.io/almalinuxorg/almalinux-bootc:9.6 here after debugging
3 | #
4 | ALL_BASE_IMAGES := "quay.io/fedora/fedora-bootc:42 quay.io/centos-bootc/centos-bootc:stream9 quay.io/centos-bootc/centos-bootc:stream10 quay.io/almalinuxorg/almalinux-bootc:10.0"
5 |
6 | # Build the native binary
7 | build:
8 | make
9 |
10 | # Static checks
11 | validate:
12 | make validate
13 |
14 | # Run unit tests (excludes integration tests)
15 | unit *ARGS:
16 | #!/usr/bin/env bash
17 | set -euo pipefail
18 | if command -v cargo-nextest &> /dev/null; then
19 | cargo nextest run {{ ARGS }}
20 | else
21 | cargo test {{ ARGS }}
22 | fi
23 |
24 | pull-test-images:
25 | podman pull -q {{ALL_BASE_IMAGES}} >/dev/null
26 |
27 | # Run integration tests (auto-detects nextest, with cleanup)
28 | test-integration *ARGS: build pull-test-images
29 | #!/usr/bin/env bash
30 | set -euo pipefail
31 | export BCVK_PATH=$(pwd)/target/release/bcvk
32 | export BCVK_PRIMARY_IMAGE={{ PRIMARY_IMAGE }}
33 | # Note: BCVK_ALL_IMAGES is quoted to preserve the space-separated list
34 | export BCVK_ALL_IMAGES="{{ ALL_BASE_IMAGES }}"
35 |
36 | # Clean up any leftover containers before starting
37 | cargo run --release --bin test-cleanup -p integration-tests 2>/dev/null || true
38 |
39 | # Run the tests
40 | if command -v cargo-nextest &> /dev/null; then
41 | cargo nextest run --release -P integration -p integration-tests {{ ARGS }}
42 | TEST_EXIT_CODE=$?
43 | else
44 | cargo test --release -p integration-tests -- {{ ARGS }}
45 | TEST_EXIT_CODE=$?
46 | fi
47 |
48 | # Clean up containers after tests complete
49 | cargo run --release --bin test-cleanup -p integration-tests 2>/dev/null || true
50 |
51 | exit $TEST_EXIT_CODE
52 |
53 | # Clean up integration test containers
54 | test-cleanup:
55 | cargo run --release --bin test-cleanup -p integration-tests
56 |
57 | # Install cargo-nextest if not already installed
58 | install-nextest:
59 | @which cargo-nextest > /dev/null 2>&1 || cargo install cargo-nextest --locked
60 |
61 | # Run this before committing
62 | fmt:
63 | cargo fmt
64 |
65 | # Run the binary directly
66 | run *ARGS:
67 | cargo run --release -- {{ ARGS }}
68 |
69 | # Create archive with binary, tarball, and checksums
70 | archive: build
71 | #!/usr/bin/env bash
72 | set -euo pipefail
73 | ARCH=$(arch)
74 | BINARY_PATH="target/release/bcvk"
75 | TARGET_NAME="bcvk-${ARCH}-unknown-linux-gnu"
76 | ARTIFACTS_DIR="target"
77 |
78 | # Strip the binary
79 | strip "${BINARY_PATH}" || true
80 |
81 | # Copy binary with target-specific name to artifacts directory
82 | cp "${BINARY_PATH}" "${ARTIFACTS_DIR}/${TARGET_NAME}"
83 |
84 | # Create tarball in artifacts directory
85 | cd "${ARTIFACTS_DIR}"
86 | tar -czf "${TARGET_NAME}.tar.gz" "${TARGET_NAME}"
87 |
88 | # Generate checksums
89 | sha256sum "${TARGET_NAME}.tar.gz" > "${TARGET_NAME}.tar.gz.sha256"
90 |
91 | # Clean up the temporary binary copy
92 | rm "${TARGET_NAME}"
93 |
94 | echo "Archive created: ${ARTIFACTS_DIR}/${TARGET_NAME}.tar.gz"
95 | echo "Checksum: ${ARTIFACTS_DIR}/${TARGET_NAME}.tar.gz.sha256"
96 |
97 | # Install the binary to ~/.local/bin
98 | install: build
99 | cp target/release/bcvk ~/.local/bin/
100 |
101 |
--------------------------------------------------------------------------------
/crates/kit/src/container_entrypoint.rs:
--------------------------------------------------------------------------------
1 | use clap::{Parser, Subcommand};
2 | use color_eyre::Result;
3 | use tokio::signal::unix::SignalKind;
4 | use tracing::debug;
5 |
6 | use crate::run_ephemeral::RunEphemeralOpts;
7 |
8 | #[derive(Parser)]
9 | pub struct ContainerEntrypointOpts {
10 | #[command(subcommand)]
11 | pub command: ContainerCommands,
12 | }
13 |
14 | #[derive(Subcommand)]
15 | pub enum ContainerCommands {
16 | /// Run ephemeral VM (what run-ephemeral-impl does today)
17 | RunEphemeral,
18 |
19 | /// SSH to VM from container
20 | Ssh(SshOpts),
21 |
22 | /// Monitor VM status file using inotify
23 | MonitorStatus(MonitorStatusOpts),
24 | }
25 |
26 | #[derive(Parser)]
27 | pub struct SshOpts {
28 | /// SSH arguments
29 | #[clap(allow_hyphen_values = true)]
30 | pub args: Vec,
31 | }
32 |
33 | #[derive(Parser)]
34 | pub struct MonitorStatusOpts {}
35 |
36 | pub async fn run_ephemeral_in_container() -> Result<()> {
37 | // Parse BCK_CONFIG from environment
38 | let config_json = std::env::var("BCK_CONFIG")?;
39 | let opts: RunEphemeralOpts = serde_json::from_str(&config_json)?;
40 |
41 | // Call existing run_impl
42 | crate::run_ephemeral::run_impl(opts).await
43 | }
44 |
45 | pub fn ssh_to_vm(opts: SshOpts) -> Result<()> {
46 | debug!("SSH to VM with args: {:?}", opts.args);
47 |
48 | // SSH implementation
49 | // Default to root@10.0.2.15 (QEMU user networking)
50 | let mut cmd = std::process::Command::new("ssh");
51 |
52 | // Check if SSH key exists
53 | if std::path::Path::new("/tmp/ssh").exists() {
54 | cmd.args(["-i", "/tmp/ssh"]);
55 | }
56 |
57 | cmd.args(["-o", "StrictHostKeyChecking=no"]);
58 | cmd.args(["-o", "UserKnownHostsFile=/dev/null"]);
59 | cmd.args(["-o", "LogLevel=ERROR"]); // Reduce SSH verbosity
60 |
61 | // If no host specified in args, use default
62 | if !opts.args.iter().any(|arg| arg.contains("@")) {
63 | cmd.arg("root@10.0.2.15");
64 | }
65 |
66 | // Add any additional arguments
67 | if !opts.args.is_empty() && !opts.args.iter().any(|arg| arg.contains("@")) {
68 | cmd.arg("--");
69 | }
70 | cmd.args(&opts.args);
71 |
72 | let status = cmd.status()?;
73 | std::process::exit(status.code().unwrap_or(1));
74 | }
75 |
76 | pub fn monitor_status(_opts: MonitorStatusOpts) -> Result<()> {
77 | crate::status_monitor::monitor_and_stream_status()
78 | }
79 |
80 | pub async fn run(opts: ContainerEntrypointOpts) -> Result<()> {
81 | let signals = [libc::SIGTERM, libc::SIGINT, libc::SIGRTMIN() + 3];
82 | let mut signal_joinset = tokio::task::JoinSet::new();
83 | for s in signals {
84 | signal_joinset.spawn(async move {
85 | let mut signal = tokio::signal::unix::signal(SignalKind::from_raw(s))?;
86 | signal.recv().await;
87 | Ok::<_, std::io::Error>(())
88 | });
89 | }
90 |
91 | tokio::select! {
92 | _ = signal_joinset.join_next() => {
93 | debug!("Caught termination signal");
94 | Ok(())
95 | }
96 | r = async {
97 | match opts.command {
98 | ContainerCommands::RunEphemeral => run_ephemeral_in_container().await,
99 | ContainerCommands::Ssh(ssh_opts) => {
100 | tokio::task::spawn_blocking(move || ssh_to_vm(ssh_opts)).await?
101 | }
102 | ContainerCommands::MonitorStatus(monitor_opts) => {
103 | tokio::task::spawn_blocking(move || monitor_status(monitor_opts)).await?
104 | }
105 | }
106 | } => r
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-ephemeral-run-ssh.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-ephemeral-run-ssh - Run ephemeral VM and SSH into it
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk ephemeral run-ssh** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | Run ephemeral VM and SSH into it
12 |
13 | # OPTIONS
14 |
15 |
16 | **IMAGE**
17 |
18 | Container image to run as ephemeral VM
19 |
20 | This argument is required.
21 |
22 | **SSH_ARGS**
23 |
24 | SSH command to execute (optional, defaults to interactive shell)
25 |
26 | **--itype**=*ITYPE*
27 |
28 | Instance type (e.g., u1.nano, u1.small, u1.medium). Overrides vcpus/memory if specified.
29 |
30 | **--memory**=*MEMORY*
31 |
32 | Memory size (e.g. 4G, 2048M, or plain number for MB)
33 |
34 | Default: 4G
35 |
36 | **--vcpus**=*VCPUS*
37 |
38 | Number of vCPUs (overridden by --itype if specified)
39 |
40 | **--console**
41 |
42 | Enable console output to terminal for debugging
43 |
44 | **--debug**
45 |
46 | Enable debug mode (drop to shell instead of running QEMU)
47 |
48 | **--virtio-serial-out**=*NAME:FILE*
49 |
50 | Add virtio-serial device with output to file (format: name:/path/to/file)
51 |
52 | **--execute**=*EXECUTE*
53 |
54 | Execute command inside VM via systemd and capture output
55 |
56 | **-K**, **--ssh-keygen**
57 |
58 | Generate SSH keypair and inject via systemd credentials
59 |
60 | **-t**, **--tty**
61 |
62 | Allocate a pseudo-TTY for container
63 |
64 | **-i**, **--interactive**
65 |
66 | Keep STDIN open for container
67 |
68 | **-d**, **--detach**
69 |
70 | Run container in background
71 |
72 | **--rm**
73 |
74 | Automatically remove container when it exits
75 |
76 | **--name**=*NAME*
77 |
78 | Assign a name to the container
79 |
80 | **--network**=*NETWORK*
81 |
82 | Configure the network for the container
83 |
84 | **--label**=*LABEL*
85 |
86 | Add metadata to the container in key=value form
87 |
88 | **-e**, **--env**=*ENV*
89 |
90 | Set environment variables in the container (key=value)
91 |
92 | **--debug-entrypoint**=*DEBUG_ENTRYPOINT*
93 |
94 | Do not run the default entrypoint directly, but instead invoke the provided command (e.g. `bash`)
95 |
96 | **--bind**=*HOST_PATH[:NAME]*
97 |
98 | Bind mount host directory (RW) at /run/virtiofs-mnt-
99 |
100 | **--ro-bind**=*HOST_PATH[:NAME]*
101 |
102 | Bind mount host directory (RO) at /run/virtiofs-mnt-
103 |
104 | **--systemd-units**=*SYSTEMD_UNITS_DIR*
105 |
106 | Directory with systemd units to inject (expects system/ subdirectory)
107 |
108 | **--bind-storage-ro**
109 |
110 | Mount host container storage (RO) at /run/virtiofs-mnt-hoststorage
111 |
112 | **--add-swap**=*ADD_SWAP*
113 |
114 | Allocate a swap device of the provided size
115 |
116 | **--mount-disk-file**=*FILE[:NAME]*
117 |
118 | Mount disk file as virtio-blk device at /dev/disk/by-id/virtio-
119 |
120 | **--karg**=*KERNEL_ARGS*
121 |
122 | Additional kernel command line arguments
123 |
124 |
125 |
126 | # EXAMPLES
127 |
128 | Run an ephemeral VM and automatically SSH into it (VM cleans up when SSH exits):
129 |
130 | bcvk ephemeral run-ssh quay.io/fedora/fedora-bootc:42
131 |
132 | Run a quick test with automatic SSH and cleanup:
133 |
134 | bcvk ephemeral run-ssh quay.io/fedora/fedora-bootc:42
135 |
136 | Execute a specific command via SSH:
137 |
138 | bcvk ephemeral run-ssh quay.io/fedora/fedora-bootc:42 'systemctl status'
139 |
140 | Run with custom memory and CPU allocation:
141 |
142 | bcvk ephemeral run-ssh --memory 8G --vcpus 4 quay.io/fedora/fedora-bootc:42
143 |
144 | # SEE ALSO
145 |
146 | **bcvk**(8)
147 |
148 | # VERSION
149 |
150 |
151 |
--------------------------------------------------------------------------------
/crates/integration-tests/src/bin/cleanup.rs:
--------------------------------------------------------------------------------
1 | //! Cleanup utility for integration test resources
2 | //!
3 | //! This binary removes integration test containers and libvirt VMs that were created during testing.
4 |
5 | use std::process::Command;
6 |
7 | // Import shared constants from the library
8 | use integration_tests::{INTEGRATION_TEST_LABEL, LIBVIRT_INTEGRATION_TEST_LABEL};
9 |
10 | fn cleanup_integration_test_containers() -> Result<(), Box> {
11 | println!("Cleaning up integration test containers...");
12 |
13 | // List all containers with our integration test label
14 | let list_output = Command::new("podman")
15 | .args([
16 | "ps",
17 | "-a",
18 | "--filter",
19 | &format!("label={}", INTEGRATION_TEST_LABEL),
20 | "-q",
21 | ])
22 | .output()?;
23 |
24 | if !list_output.status.success() {
25 | eprintln!("Warning: Failed to list containers");
26 | return Ok(());
27 | }
28 |
29 | let container_ids = String::from_utf8_lossy(&list_output.stdout);
30 | let containers: Vec<&str> = container_ids.lines().filter(|l| !l.is_empty()).collect();
31 |
32 | if containers.is_empty() {
33 | println!("No integration test containers found to clean up");
34 | return Ok(());
35 | }
36 |
37 | println!(
38 | "Found {} integration test container(s) to clean up",
39 | containers.len()
40 | );
41 |
42 | // Force remove each container
43 | let mut cleaned = 0;
44 | for container_id in containers {
45 | print!(
46 | " Removing container {}... ",
47 | &container_id[..12.min(container_id.len())]
48 | );
49 | let rm_output = Command::new("podman")
50 | .args(["rm", "-f", container_id])
51 | .output()?;
52 |
53 | if rm_output.status.success() {
54 | println!("✓");
55 | cleaned += 1;
56 | } else {
57 | println!("✗ (failed)");
58 | eprintln!(" Error: {}", String::from_utf8_lossy(&rm_output.stderr));
59 | }
60 | }
61 |
62 | println!("Cleanup completed: {} container(s) removed", cleaned);
63 | Ok(())
64 | }
65 |
66 | fn cleanup_libvirt_integration_test_vms() -> Result<(), Box> {
67 | println!("Cleaning up integration test libvirt VMs...");
68 |
69 | // Get path to bcvk binary (should be in the same directory as this cleanup binary)
70 | let current_exe = std::env::current_exe()?;
71 | let bcvk_path = current_exe
72 | .parent()
73 | .ok_or("Failed to get parent directory")?
74 | .join("bcvk");
75 |
76 | if !bcvk_path.exists() {
77 | println!(
78 | "bcvk binary not found at {:?}, skipping libvirt cleanup",
79 | bcvk_path
80 | );
81 | return Ok(());
82 | }
83 |
84 | // Use bcvk libvirt rm-all with label filter
85 | let rm_output = Command::new(&bcvk_path)
86 | .args([
87 | "libvirt",
88 | "rm-all",
89 | "--label",
90 | LIBVIRT_INTEGRATION_TEST_LABEL,
91 | "--force",
92 | "--stop",
93 | ])
94 | .output()?;
95 |
96 | if !rm_output.status.success() {
97 | let stderr = String::from_utf8_lossy(&rm_output.stderr);
98 | eprintln!("Warning: Failed to clean up libvirt VMs: {}", stderr);
99 | return Ok(());
100 | }
101 |
102 | let stdout = String::from_utf8_lossy(&rm_output.stdout);
103 | println!("{}", stdout);
104 |
105 | Ok(())
106 | }
107 |
108 | fn main() {
109 | let mut errors = Vec::new();
110 |
111 | if let Err(e) = cleanup_integration_test_containers() {
112 | eprintln!("Error during container cleanup: {}", e);
113 | errors.push(format!("containers: {}", e));
114 | }
115 |
116 | if let Err(e) = cleanup_libvirt_integration_test_vms() {
117 | eprintln!("Error during libvirt VM cleanup: {}", e);
118 | errors.push(format!("libvirt: {}", e));
119 | }
120 |
121 | if !errors.is_empty() {
122 | eprintln!("Cleanup completed with errors: {}", errors.join(", "));
123 | std::process::exit(1);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-to-disk.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-to-disk - Install bootc images to persistent disk images
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk to-disk** \[**-h**\|**\--help**\] \[*OPTIONS*\] *IMAGE*
8 |
9 | # DESCRIPTION
10 |
11 | Performs automated installation of bootc containers to disk images
12 | using ephemeral VMs as the installation environment. Supports multiple
13 | filesystems, custom sizing, and creates bootable disk images ready
14 | for production deployment.
15 |
16 | The installation process:
17 |
18 | 1. Creates a new disk image with the specified filesystem layout
19 | 2. Boots an ephemeral VM with the target container image
20 | 3. Runs \`bootc install to-disk\` within the VM to install to the disk
21 | 4. Produces a bootable disk image that can be deployed anywhere
22 |
23 | # OPTIONS
24 |
25 |
26 | **SOURCE_IMAGE**
27 |
28 | Container image to install
29 |
30 | This argument is required.
31 |
32 | **TARGET_DISK**
33 |
34 | Target disk/device path
35 |
36 | This argument is required.
37 |
38 | **--filesystem**=*FILESYSTEM*
39 |
40 | Root filesystem type (e.g. ext4, xfs, btrfs)
41 |
42 | **--root-size**=*ROOT_SIZE*
43 |
44 | Root filesystem size (e.g., '10G', '5120M')
45 |
46 | **--storage-path**=*STORAGE_PATH*
47 |
48 | Path to host container storage (auto-detected if not specified)
49 |
50 | **--target-transport**=*TARGET_TRANSPORT*
51 |
52 | The transport; e.g. oci, oci-archive, containers-storage. Defaults to `registry`
53 |
54 | **--karg**=*KARG*
55 |
56 | Set a kernel argument
57 |
58 | **--composefs-backend**
59 |
60 | Default to composefs-native storage
61 |
62 | **--disk-size**=*DISK_SIZE*
63 |
64 | Disk size to create (e.g. 10G, 5120M, or plain number for bytes)
65 |
66 | **--format**=*FORMAT*
67 |
68 | Output disk image format
69 |
70 | Possible values:
71 | - raw
72 | - qcow2
73 |
74 | Default: raw
75 |
76 | **--itype**=*ITYPE*
77 |
78 | Instance type (e.g., u1.nano, u1.small, u1.medium). Overrides vcpus/memory if specified.
79 |
80 | **--memory**=*MEMORY*
81 |
82 | Memory size (e.g. 4G, 2048M, or plain number for MB)
83 |
84 | Default: 4G
85 |
86 | **--vcpus**=*VCPUS*
87 |
88 | Number of vCPUs (overridden by --itype if specified)
89 |
90 | **--console**
91 |
92 | Enable console output to terminal for debugging
93 |
94 | **--debug**
95 |
96 | Enable debug mode (drop to shell instead of running QEMU)
97 |
98 | **--virtio-serial-out**=*NAME:FILE*
99 |
100 | Add virtio-serial device with output to file (format: name:/path/to/file)
101 |
102 | **--execute**=*EXECUTE*
103 |
104 | Execute command inside VM via systemd and capture output
105 |
106 | **-K**, **--ssh-keygen**
107 |
108 | Generate SSH keypair and inject via systemd credentials
109 |
110 | **--install-log**=*INSTALL_LOG*
111 |
112 | Configure logging for `bootc install` by setting the `RUST_LOG` environment variable
113 |
114 | **--label**=*LABEL*
115 |
116 | Add metadata to the container in key=value form
117 |
118 | **--dry-run**
119 |
120 | Check if the disk would be regenerated without actually creating it
121 |
122 |
123 |
124 | # ARGUMENTS
125 |
126 | *IMAGE*
127 |
128 | : Container image reference to install (e.g., \`registry.example.com/my-bootc:latest\`)
129 |
130 | # EXAMPLES
131 |
132 | Create a raw disk image:
133 |
134 | bcvk to-disk quay.io/centos-bootc/centos-bootc:stream10 /path/to/disk.img
135 |
136 | Create a qcow2 disk image (more compact):
137 |
138 | bcvk to-disk --format qcow2 quay.io/fedora/fedora-bootc:42 /path/to/fedora.qcow2
139 |
140 | Create with specific disk size:
141 |
142 | bcvk to-disk --disk-size 20G quay.io/fedora/fedora-bootc:42 /path/to/large-disk.img
143 |
144 | Create with custom filesystem and root size:
145 |
146 | bcvk to-disk --filesystem btrfs --root-size 15G quay.io/fedora/fedora-bootc:42 /path/to/btrfs-disk.img
147 |
148 | Development workflow - test then create deployment image:
149 |
150 | # Test the container as a VM first
151 | bcvk ephemeral run-ssh my-app
152 |
153 | # If good, create the deployment image
154 | bcvk to-disk my-app /tmp/my-app.img
155 |
156 | # VERSION
157 |
158 | v0.1.0
--------------------------------------------------------------------------------
/.github/actions/bootc-ubuntu-setup/action.yml:
--------------------------------------------------------------------------------
1 | name: 'Bootc Ubuntu Setup'
2 | description: 'Default host setup'
3 | inputs:
4 | libvirt:
5 | description: 'Install libvirt and virtualization stack'
6 | required: false
7 | default: 'false'
8 | runs:
9 | using: 'composite'
10 | steps:
11 | # The default runners have TONS of crud on them...
12 | - name: Free up disk space on runner
13 | shell: bash
14 | run: |
15 | set -xeuo pipefail
16 | sudo df -h
17 | unwanted_pkgs=('^aspnetcore-.*' '^dotnet-.*' '^llvm-.*' 'php.*' '^mongodb-.*' '^mysql-.*'
18 | azure-cli google-chrome-stable firefox mono-devel)
19 | unwanted_dirs=(/usr/share/dotnet /opt/ghc /usr/local/lib/android /opt/hostedtoolcache/CodeQL)
20 | # Start background removal operations as systemd units; if this causes
21 | # races in the future around disk space we can look at waiting for cleanup
22 | # before starting further jobs, but right now we spent a lot of time waiting
23 | # on the network and scripts and such below, giving these plenty of time to run.
24 | n=0
25 | runcleanup() {
26 | sudo systemd-run -r -u action-cleanup-${n} -- "$@"
27 | n=$(($n + 1))
28 | }
29 | runcleanup docker image prune --all --force
30 | for x in ${unwanted_dirs[@]}; do
31 | runcleanup rm -rf "$x"
32 | done
33 | # Apt removals in foreground, as we can't parallelize these
34 | for x in ${unwanted_pkgs[@]}; do
35 | /bin/time -f '%E %C' sudo apt-get remove -y $x
36 | done
37 | # We really want support for heredocs
38 | - name: Update podman and install just
39 | shell: bash
40 | run: |
41 | set -eux
42 | # Require the runner is ubuntu-24.04
43 | IDV=$(. /usr/lib/os-release && echo ${ID}-${VERSION_ID})
44 | test "${IDV}" = "ubuntu-24.04"
45 | # plucky is the next release
46 | echo 'deb http://azure.archive.ubuntu.com/ubuntu plucky universe main' | sudo tee /etc/apt/sources.list.d/plucky.list
47 | /bin/time -f '%E %C' sudo apt update
48 | # skopeo is currently older in plucky for some reason hence --allow-downgrades
49 | /bin/time -f '%E %C' sudo apt install -y --allow-downgrades crun/plucky podman/plucky skopeo/plucky just
50 | # This is the default on e.g. Fedora derivatives, but not Debian
51 | - name: Enable unprivileged /dev/kvm access
52 | shell: bash
53 | run: |
54 | set -xeuo pipefail
55 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
56 | sudo udevadm control --reload-rules
57 | sudo udevadm trigger --name-match=kvm
58 | ls -l /dev/kvm
59 | # Used by a few workflows, but generally useful
60 | - name: Set architecture variable
61 | id: set_arch
62 | shell: bash
63 | run: echo "ARCH=$(arch)" >> $GITHUB_ENV
64 | # Install libvirt stack if requested
65 | - name: Install libvirt and virtualization stack
66 | if: ${{ inputs.libvirt == 'true' }}
67 | shell: bash
68 | run: |
69 | set -xeuo pipefail
70 | export BCVK_VERSION=0.9.0
71 | # see https://github.com/bootc-dev/bcvk/issues/176
72 | /bin/time -f '%E %C' sudo apt install -y libkrb5-dev pkg-config libvirt-dev genisoimage qemu-utils qemu-kvm virtiofsd libvirt-daemon-system python3-virt-firmware
73 | # Something in the stack is overriding this, but we want session right now for bcvk
74 | echo LIBVIRT_DEFAULT_URI=qemu:///session >> $GITHUB_ENV
75 | td=$(mktemp -d)
76 | cd $td
77 | # Install bcvk
78 | target=bcvk-$(arch)-unknown-linux-gnu
79 | /bin/time -f '%E %C' curl -LO https://github.com/bootc-dev/bcvk/releases/download/v${BCVK_VERSION}/${target}.tar.gz
80 | tar xzf ${target}.tar.gz
81 | sudo install -T ${target} /usr/bin/bcvk
82 | cd -
83 | rm -rf "$td"
84 |
85 | # Also bump the default fd limit as a workaround for https://github.com/bootc-dev/bcvk/issues/65
86 | sudo sed -i -e 's,^\* hard nofile 65536,* hard nofile 524288,' /etc/security/limits.conf
87 | - name: Cleanup status
88 | shell: bash
89 | run: |
90 | set -xeuo pipefail
91 | systemctl list-units 'action-cleanup*'
92 | df -h
93 |
--------------------------------------------------------------------------------
/crates/kit/src/libvirt/print_firmware.rs:
--------------------------------------------------------------------------------
1 | //! Print firmware information command
2 | //!
3 | //! This module provides a command to display detected OVMF firmware paths
4 | //! and configuration for debugging firmware detection issues.
5 |
6 | use clap::Parser;
7 | use color_eyre::{eyre::Context, Result};
8 | use serde::{Deserialize, Serialize};
9 |
10 | use super::secureboot::{find_ovmf_vars, find_secure_boot_firmware};
11 |
12 | /// Options for the print-firmware command
13 | #[derive(Debug, Parser)]
14 | pub struct LibvirtPrintFirmwareOpts {
15 | /// Output format (yaml or json)
16 | #[clap(long, default_value = "yaml", value_enum)]
17 | pub format: OutputFormat,
18 | }
19 |
20 | /// Output format for print-firmware command
21 | #[derive(Debug, Clone, clap::ValueEnum)]
22 | pub enum OutputFormat {
23 | /// YAML format (default, human-readable)
24 | Yaml,
25 | /// JSON format (machine-readable)
26 | Json,
27 | }
28 |
29 | /// Firmware information for display
30 | #[derive(Debug, Serialize, Deserialize)]
31 | pub struct PrintFirmwareInfo {
32 | /// Path to OVMF_VARS.fd (or equivalent)
33 | pub vars_path: Option,
34 | /// Path to OVMF_CODE.secboot.fd (or equivalent)
35 | pub code_secboot_path: Option,
36 | /// Format of OVMF_CODE file (raw or qcow2)
37 | pub code_format: Option,
38 | /// Format of OVMF_VARS file (raw or qcow2)
39 | pub vars_format: Option,
40 | /// Current architecture
41 | pub architecture: String,
42 | }
43 |
44 | /// Execute the print-firmware command
45 | pub fn run(opts: LibvirtPrintFirmwareOpts) -> Result<()> {
46 | // Try to find OVMF_VARS (non-secboot variant)
47 | let vars_path = match find_ovmf_vars() {
48 | Ok(path) => Some(path.to_string()),
49 | Err(e) => {
50 | tracing::debug!("Failed to find OVMF_VARS: {}", e);
51 | None
52 | }
53 | };
54 |
55 | // Try to find secure boot firmware (CODE and VARS with formats)
56 | let (code_secboot_path, code_format, vars_format) = match find_secure_boot_firmware() {
57 | Ok(fw_info) => (
58 | Some(fw_info.code_path.to_string()),
59 | Some(fw_info.code_format),
60 | Some(fw_info.vars_format),
61 | ),
62 | Err(e) => {
63 | tracing::debug!("Failed to find secure boot firmware: {}", e);
64 | (None, None, None)
65 | }
66 | };
67 |
68 | let info = PrintFirmwareInfo {
69 | vars_path,
70 | code_secboot_path,
71 | code_format,
72 | vars_format,
73 | architecture: std::env::consts::ARCH.to_string(),
74 | };
75 |
76 | // Output in requested format
77 | match opts.format {
78 | OutputFormat::Yaml => {
79 | println!(
80 | "{}",
81 | serde_yaml::to_string(&info)
82 | .with_context(|| "Failed to serialize firmware info as YAML")?
83 | );
84 | }
85 | OutputFormat::Json => {
86 | println!(
87 | "{}",
88 | serde_json::to_string_pretty(&info)
89 | .with_context(|| "Failed to serialize firmware info as JSON")?
90 | );
91 | }
92 | }
93 |
94 | Ok(())
95 | }
96 |
97 | #[cfg(test)]
98 | mod tests {
99 | use super::*;
100 |
101 | #[test]
102 | fn test_firmware_info_serialization() {
103 | let info = PrintFirmwareInfo {
104 | vars_path: Some("/usr/share/edk2/ovmf/OVMF_VARS.fd".to_string()),
105 | code_secboot_path: Some("/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd".to_string()),
106 | code_format: Some("raw".to_string()),
107 | vars_format: Some("raw".to_string()),
108 | architecture: "x86_64".to_string(),
109 | };
110 |
111 | // Test YAML serialization
112 | let yaml = serde_yaml::to_string(&info).unwrap();
113 | assert!(yaml.contains("vars_path"));
114 | assert!(yaml.contains("OVMF_VARS.fd"));
115 | assert!(yaml.contains("code_format"));
116 |
117 | // Test JSON serialization
118 | let json = serde_json::to_string(&info).unwrap();
119 | assert!(json.contains("vars_path"));
120 | assert!(json.contains("OVMF_VARS.fd"));
121 | assert!(json.contains("code_format"));
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # bcvk - bootc virtualization kit
2 |
3 | This project helps launch ephemeral VMs from bootc containers, and also create
4 | disk images that can be imported into other virtualization frameworks.
5 |
6 | ## Installation
7 |
8 | See [docs/src/installation.md](./docs/src/installation.md).
9 |
10 | ## Quick Start
11 |
12 | ### Running a bootc container as ephemeral VM
13 |
14 | This doesn't require any privileges, it's just a wrapper
15 | for `podman`. It does require a virt stack (qemu, virtiofsd)
16 | in the host environment.
17 |
18 | ```bash
19 | bcvk ephemeral run -d --rm -K --name mytestvm quay.io/fedora/fedora-bootc:42
20 | bcvk ephemeral ssh mytestvm
21 | ```
22 |
23 | Or to fully streamline the above and have the VM automatically terminate when you exit
24 | the SSH client:
25 |
26 | ```bash
27 | bcvk ephemeral run-ssh quay.io/fedora/fedora-bootc:42
28 | ```
29 |
30 | Everything with `bcvk ephemeral` creates a podman container that reuses the
31 | host virtualization stack, making it simple to test bootc containers without
32 | requiring root privileges or dedicated VM infrastructure.
33 |
34 | ### Creating a persistent bootable disk image from a container image
35 | ```bash
36 | # Install bootc image to disk
37 | bcvk to-disk quay.io/centos-bootc/centos-bootc:stream10 /path/to/disk.img
38 | ```
39 |
40 | ### Image management
41 |
42 | There's a convenient helper function which filters by all container images
43 | with the `containers.bootc=1` label: `bcvk images list`
44 |
45 | ### libvirt integration
46 |
47 | The libvirt commands provide comprehensive integration with libvirt infrastructure for managing bootc containers as persistent VMs.
48 |
49 | #### Starting a bootc container as a libvirt VM
50 |
51 | ```bash
52 | # Basic libvirt VM creation with default settings (2GB RAM, 2 CPUs, 20GB disk)
53 | bcvk libvirt run quay.io/centos-bootc/centos-bootc:stream10
54 |
55 | # Note requirement for --filesystem with the generic Fedora bootc base images
56 | bcvk libvirt run --filesystem btrfs quay.io/fedora/fedora-bootc:43
57 |
58 | # Custom VM with specific resources and name
59 | bcvk libvirt run --name example-vm --memory 4096 --cpus 4 --disk-size 50G quay.io/centos-bootc/centos-bootc:stream10
60 |
61 | # This example forwards a port and bind mounts content from the host
62 | bcvk libvirt run --name web-server --port 8080:80 --volume /host/data:/mnt/data localhost/myimage
63 |
64 | # Bind mount the host container storage for faster updates
65 | bcvk libvirt run --update-from-host --name devvm localhost/myimage
66 | ```
67 |
68 | #### Using and managing libvirt VMs
69 |
70 | After initializing a VM a common next step is `bcvk lbivirt ssh `.
71 | bcvk defaults to injecting SSH keys via [systemd credentials](https://systemd.io/CREDENTIALS/).
72 | The private key is specific to the VM and is stored in the domain metadata.
73 |
74 | Other operations:
75 |
76 | ```bash
77 | # List all bootc-related libvirt domains
78 | bcvk libvirt list
79 |
80 | # Stop a running VM
81 | bcvk libvirt stop my-fedora-vm
82 |
83 | # Start a stopped VM
84 | bcvk libvirt start my-fedora-vm
85 |
86 | # Get detailed information about a VM
87 | bcvk libvirt inspect my-fedora-vm
88 |
89 | # Remove a VM and its resources
90 | bcvk libvirt rm -f my-fedora-vm
91 | ```
92 |
93 | ## Other operations
94 |
95 | The `bcvk libvirt run` command wraps `bcvk to-disk` which in
96 | turns wraps `bootc install to-disk` in an ephemeral VM. In
97 | some cases, you may want to create a disk image directly.
98 |
99 | ```bash
100 | # Generate a disk image in qcow2 format.
101 | bcvk to-disk --format=qcow2 localhost/my-container-image output-disk.qcow2
102 | ```
103 |
104 | Note that at the current time, this project is not scoped to
105 | output other virtualization formats. The [bootc image builder](https://github.com/osbuild/bootc-image-builder)
106 | is one project that offers those.
107 |
108 | ## Goals
109 |
110 | This project aims to implement part of
111 | .
112 |
113 | Basically it will be "bootc virtualization kit", and help users
114 | run bootable containers as virtual machines.
115 |
116 | Related projects and content:
117 |
118 | - https://github.com/coreos/coreos-assembler/
119 | - https://github.com/ublue-os/bluefin-lts/blob/main/Justfile
120 |
121 | ## Development
122 |
123 | See [docs/HACKING.md](docs/HACKING.md).
124 |
125 |
126 |
--------------------------------------------------------------------------------
/docs/src/vs-podman-bootc.md:
--------------------------------------------------------------------------------
1 | # bcvk vs podman-bootc
2 |
3 | Both bcvk (bcvk) and podman-bootc solve similar problems but with different architectural approaches and design philosophies. Understanding these differences helps you choose the right tool for your workflow.
4 |
5 | ## Architectural Differences
6 |
7 | ### Container Runtime Integration
8 | - **bcvk**: Works with any container runtime and doesn't require specific Podman configurations
9 | - **podman-bootc**: Tightly integrated with Podman and requires rootful Podman Machine setup
10 |
11 | ### Platform Support Strategy
12 | - **bcvk**: Native virtualization on each platform (KVM on Linux, direct QEMU integration)
13 | - **podman-bootc**: Unified approach through Podman Machine abstraction layer
14 |
15 | ### VM Management Philosophy
16 | - **bcvk**: Flexible VM management with both ephemeral and persistent options
17 | - **podman-bootc**: Focused on development workflows with automatic lifecycle management
18 |
19 | ## Workflow Differences
20 |
21 | ### Development Iteration
22 | ```mermaid
23 | flowchart TD
24 | A[Container Image] --> B{Tool Choice}
25 | B -->|bcvk| C[Multiple VM Options]
26 | C --> D[Ephemeral Testing]
27 | C --> E[Persistent Development]
28 | C --> F[libvirt Production]
29 |
30 | B -->|podman-bootc| G[Standardized VM]
31 | G --> H[Development Session]
32 | H --> I[SSH Workflow]
33 | ```
34 |
35 | ### Cross-Platform Development
36 | - **bcvk**: Platform-native virtualization for optimal performance
37 | - **podman-bootc**: Consistent experience across Linux and macOS through Podman Machine
38 |
39 | ### Resource Management
40 | - **bcvk**: Fine-grained control over VM resources and configuration
41 | - **podman-bootc**: Simplified resource management with sensible defaults
42 |
43 | ## Use Case Strengths
44 |
45 | ### bcvk Excels At:
46 | - **Production-like testing** with full libvirt integration
47 | - **Performance-sensitive workloads** with native virtualization
48 | - **Complex networking scenarios** requiring advanced configuration
49 | - **Mixed virtualization environments** where you need multiple VM types
50 | - **CI/CD integration** where you need precise control over VM lifecycle
51 |
52 | ### podman-bootc Excels At:
53 | - **Cross-platform development teams** with mixed Linux/macOS environments
54 | - **Rapid prototyping** with minimal setup requirements
55 | - **Podman-centric workflows** where container and VM management are unified
56 | - **Simplified onboarding** for developers new to bootable containers
57 | - **Consistent development environments** across different host platforms
58 |
59 | ## Integration Patterns
60 |
61 | ### Container Registry Workflow
62 | Both tools integrate with container registries but with different approaches:
63 |
64 | - **bcvk**: Direct integration with any OCI-compatible registry
65 | - **podman-bootc**: Leverages Podman's registry integration and caching
66 |
67 | ### CI/CD Integration
68 | - **bcvk**: Designed for flexible CI/CD integration with customizable VM configurations
69 | - **podman-bootc**: Optimized for Podman-based CI systems and GitHub Actions workflows
70 |
71 | ### Team Collaboration
72 | - **bcvk**: Supports diverse team setups with flexible VM sharing options
73 | - **podman-bootc**: Standardized development environments reduce "works on my machine" issues
74 |
75 | ## Migration Considerations
76 |
77 | ### From podman-bootc to bcvk
78 | - More configuration flexibility but requires understanding of virtualization concepts
79 | - Better performance for compute-intensive testing
80 | - Access to advanced libvirt features
81 |
82 | ### From bcvk to podman-bootc
83 | - Simplified setup and management
84 | - Consistent cross-platform experience
85 | - Tighter integration with Podman workflows
86 |
87 | ## Complementary Usage
88 |
89 | Many teams use both tools for different purposes:
90 |
91 | - **bcvk** for production testing and performance validation
92 | - **podman-bootc** for daily development and quick testing
93 |
94 | This hybrid approach leverages the strengths of each tool while minimizing their individual limitations.
95 |
96 | ## Decision Framework
97 |
98 | Choose **bcvk** when:
99 | - You need production-grade VM testing capabilities
100 | - Performance and resource control are critical
101 | - You're working in Linux-centric environments
102 | - You need advanced networking or storage configurations
103 |
104 | Choose **podman-bootc** when:
105 | - You're working in mixed OS environments
106 | - You want minimal setup complexity
107 | - Your team is already using Podman extensively
108 | - You prioritize consistent development experiences
109 |
110 | Both tools continue to evolve, and the choice often comes down to your specific infrastructure, team preferences, and workflow requirements.
--------------------------------------------------------------------------------
/crates/kit/src/status_monitor.rs:
--------------------------------------------------------------------------------
1 | use color_eyre::Result;
2 | use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
3 | use std::io::Write;
4 | use std::path::Path;
5 | use std::sync::mpsc::{self, Receiver};
6 | use tracing::{debug, warn};
7 |
8 | use crate::supervisor_status::SupervisorStatus;
9 |
10 | /// Monitor a status file for changes using inotify
11 | pub fn monitor_status_file>(
12 | path: P,
13 | ) -> Result>> {
14 | let path = path.as_ref();
15 | let parent_dir = path.parent().unwrap_or(Path::new("/"));
16 |
17 | debug!("Setting up file watcher for: {}", path.display());
18 |
19 | let (tx, rx) = mpsc::channel();
20 |
21 | let mut watcher = RecommendedWatcher::new(
22 | move |res| {
23 | let _ = tx.send(res);
24 | },
25 | Config::default(),
26 | )?;
27 |
28 | // Watch the parent directory since the file might not exist yet
29 | watcher.watch(parent_dir, RecursiveMode::NonRecursive)?;
30 |
31 | Ok(StatusFileIterator {
32 | path: path.to_path_buf(),
33 | receiver: rx,
34 | _watcher: watcher,
35 | last_mtime: None,
36 | })
37 | }
38 |
39 | struct StatusFileIterator {
40 | path: std::path::PathBuf,
41 | receiver: Receiver>,
42 | _watcher: RecommendedWatcher,
43 | last_mtime: Option,
44 | }
45 |
46 | impl Iterator for StatusFileIterator {
47 | type Item = Result;
48 |
49 | fn next(&mut self) -> Option {
50 | loop {
51 | // First, try to read the file if it exists and has changed
52 | if let Some(status) = self.try_read_status_if_changed() {
53 | return Some(status);
54 | }
55 |
56 | // Wait for file system events with timeout
57 | let event = self.receiver.recv().ok()?.ok()?;
58 | // Check if this event is for our target file
59 | if self.is_relevant_event(&event) {
60 | if let Some(status) = self.try_read_status_if_changed() {
61 | return Some(status);
62 | }
63 | }
64 | }
65 | }
66 | }
67 |
68 | impl StatusFileIterator {
69 | fn try_read_status_if_changed(&mut self) -> Option> {
70 | // Check if file exists and get its mtime
71 | let metadata = match std::fs::metadata(&self.path) {
72 | Ok(meta) => meta,
73 | Err(_) => return None, // File doesn't exist yet
74 | };
75 |
76 | let current_mtime = metadata.modified().ok()?;
77 |
78 | // Check if mtime has changed
79 | let mtime_changed = match self.last_mtime {
80 | None => true, // First time reading
81 | Some(last) => current_mtime != last,
82 | };
83 |
84 | if !mtime_changed {
85 | return None; // No change, don't emit
86 | }
87 |
88 | // Update our tracked mtime
89 | self.last_mtime = Some(current_mtime);
90 |
91 | // Read and return the status
92 | Some(SupervisorStatus::read_from_file(&self.path))
93 | }
94 |
95 | fn is_relevant_event(&self, event: ¬ify::Event) -> bool {
96 | match event.kind {
97 | EventKind::Create(_) | EventKind::Modify(_) => {
98 | event.paths.iter().any(|p| p == &self.path)
99 | }
100 | _ => false,
101 | }
102 | }
103 | }
104 |
105 | /// Monitor status and stream updates to stdout as JSON lines
106 | pub fn monitor_and_stream_status() -> Result<()> {
107 | let path = "/run/supervisor-status.json";
108 |
109 | let monitor = monitor_status_file(path)?;
110 |
111 | for status_result in monitor {
112 | match status_result {
113 | Ok(status) => {
114 | // Output as JSON line - just stream every update. We don't panic
115 | // or error on failure to write, just silently exit as we assume
116 | // the caller intentionally dropped.
117 | let mut stdout = std::io::stdout().lock();
118 | if let Err(_) = serde_json::to_writer(&mut stdout, &status) {
119 | return Ok(());
120 | }
121 | let _ = stdout.write(b"\n");
122 | let _ = stdout.flush()?;
123 | // Terminate stream when qemu exits
124 | if !status.running {
125 | return Ok(());
126 | }
127 | }
128 | Err(e) => {
129 | warn!("Error reading status: {}", e);
130 | }
131 | }
132 | }
133 |
134 | Ok(())
135 | }
136 |
--------------------------------------------------------------------------------
/crates/kit/src/cli_json.rs:
--------------------------------------------------------------------------------
1 | //! Export CLI structure as JSON for documentation generation
2 | #![cfg(feature = "docgen")]
3 |
4 | use clap::Command;
5 | use serde::{Deserialize, Serialize};
6 |
7 | /// Representation of a CLI option for JSON export
8 | #[derive(Debug, Serialize, Deserialize)]
9 | pub struct CliOption {
10 | pub long: String,
11 | pub short: Option,
12 | pub value_name: Option,
13 | pub default: Option,
14 | pub help: String,
15 | pub possible_values: Vec,
16 | pub required: bool,
17 | }
18 |
19 | /// Representation of a CLI command for JSON export
20 | #[derive(Debug, Serialize, Deserialize)]
21 | pub struct CliCommand {
22 | pub name: String,
23 | pub about: Option,
24 | pub options: Vec,
25 | pub positionals: Vec,
26 | pub subcommands: Vec,
27 | }
28 |
29 | /// Representation of a positional argument
30 | #[derive(Debug, Serialize, Deserialize)]
31 | pub struct CliPositional {
32 | pub name: String,
33 | pub help: Option,
34 | pub required: bool,
35 | pub multiple: bool,
36 | }
37 |
38 | /// Convert a clap Command to our JSON representation
39 | pub fn command_to_json(cmd: &Command) -> CliCommand {
40 | let mut options = Vec::new();
41 | let mut positionals = Vec::new();
42 |
43 | // Extract arguments
44 | for arg in cmd.get_arguments() {
45 | let id = arg.get_id().as_str();
46 |
47 | // Skip built-in help and version
48 | if id == "help" || id == "version" {
49 | continue;
50 | }
51 |
52 | if arg.is_positional() {
53 | positionals.push(CliPositional {
54 | name: id.to_string(),
55 | help: arg.get_help().map(|h| h.to_string()),
56 | required: arg.is_required_set(),
57 | multiple: arg.get_action().takes_values(),
58 | });
59 | } else {
60 | let long = arg.get_long().unwrap_or(id).to_string();
61 |
62 | let short = arg.get_short().map(|c| c.to_string());
63 |
64 | // Don't set value_name for boolean flags
65 | // Check if this is a boolean flag by looking at the action type
66 | let value_name = match arg.get_action() {
67 | clap::ArgAction::SetTrue | clap::ArgAction::SetFalse => None,
68 | // If it takes no values, it's likely a boolean
69 | _ if !arg.get_action().takes_values() => None,
70 | _ => arg
71 | .get_value_names()
72 | .and_then(|names| names.first())
73 | .map(|name| name.as_str().to_string()),
74 | };
75 |
76 | let help = arg.get_help().map(|h| h.to_string()).unwrap_or_default();
77 |
78 | let all_possible_values: Vec = arg
79 | .get_possible_values()
80 | .iter()
81 | .map(|v| v.get_name().to_string())
82 | .collect();
83 |
84 | // Filter out "true/false" for boolean flags - they're obvious
85 | let possible_values = if all_possible_values.len() == 2
86 | && all_possible_values.contains(&"true".to_string())
87 | && all_possible_values.contains(&"false".to_string())
88 | {
89 | // This is a boolean flag, don't show possible values
90 | Vec::new()
91 | } else {
92 | all_possible_values
93 | };
94 |
95 | let default = arg
96 | .get_default_values()
97 | .first()
98 | .and_then(|v| v.to_str())
99 | .map(|s| s.to_string());
100 |
101 | options.push(CliOption {
102 | long,
103 | short,
104 | value_name,
105 | default,
106 | help,
107 | possible_values,
108 | required: arg.is_required_set(),
109 | });
110 | }
111 | }
112 |
113 | // Extract subcommands
114 | let subcommands = cmd
115 | .get_subcommands()
116 | .filter(|subcmd| !subcmd.is_hide_set())
117 | .map(command_to_json)
118 | .collect();
119 |
120 | CliCommand {
121 | name: cmd.get_name().to_string(),
122 | about: cmd.get_about().map(|s| s.to_string()),
123 | options,
124 | positionals,
125 | subcommands,
126 | }
127 | }
128 |
129 | /// Dump the complete CLI structure as JSON
130 | pub fn dump_cli_json() -> color_eyre::Result {
131 | use clap::CommandFactory;
132 |
133 | let cmd = crate::Cli::command();
134 | let json_structure = command_to_json(&cmd);
135 | let json = serde_json::to_string_pretty(&json_structure)?;
136 | Ok(json)
137 | }
138 |
--------------------------------------------------------------------------------
/docs/src/man/bcvk-libvirt-run.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | bcvk-libvirt-run - Run a bootable container as a persistent VM
4 |
5 | # SYNOPSIS
6 |
7 | **bcvk libvirt run** [*OPTIONS*]
8 |
9 | # DESCRIPTION
10 |
11 | Run a bootable container as a persistent VM
12 |
13 | # OPTIONS
14 |
15 |
16 | **IMAGE**
17 |
18 | Container image to run as a bootable VM
19 |
20 | This argument is required.
21 |
22 | **--name**=*NAME*
23 |
24 | Name for the VM (auto-generated if not specified)
25 |
26 | **-R**, **--replace**
27 |
28 | Replace existing VM with same name (stop and remove if exists)
29 |
30 | **--itype**=*ITYPE*
31 |
32 | Instance type (e.g., u1.nano, u1.small, u1.medium). Overrides cpus/memory if specified.
33 |
34 | **--memory**=*MEMORY*
35 |
36 | Memory size (e.g. 4G, 2048M, or plain number for MB)
37 |
38 | Default: 4G
39 |
40 | **--cpus**=*CPUS*
41 |
42 | Number of virtual CPUs for the VM (overridden by --itype if specified)
43 |
44 | Default: 2
45 |
46 | **--disk-size**=*DISK_SIZE*
47 |
48 | Disk size for the VM (e.g. 20G, 10240M, or plain number for bytes)
49 |
50 | Default: 20G
51 |
52 | **--filesystem**=*FILESYSTEM*
53 |
54 | Root filesystem type (e.g. ext4, xfs, btrfs)
55 |
56 | **--root-size**=*ROOT_SIZE*
57 |
58 | Root filesystem size (e.g., '10G', '5120M')
59 |
60 | **--storage-path**=*STORAGE_PATH*
61 |
62 | Path to host container storage (auto-detected if not specified)
63 |
64 | **--target-transport**=*TARGET_TRANSPORT*
65 |
66 | The transport; e.g. oci, oci-archive, containers-storage. Defaults to `registry`
67 |
68 | **--karg**=*KARG*
69 |
70 | Set a kernel argument
71 |
72 | **--composefs-backend**
73 |
74 | Default to composefs-native storage
75 |
76 | **-p**, **--port**=*PORT_MAPPINGS*
77 |
78 | Port mapping from host to VM (format: host_port:guest_port, e.g., 8080:80)
79 |
80 | **-v**, **--volume**=*RAW_VOLUMES*
81 |
82 | Volume mount from host to VM (raw virtiofs tag, for manual mounting)
83 |
84 | **--bind**=*BIND_MOUNTS*
85 |
86 | Bind mount from host to VM (format: host_path:guest_path)
87 |
88 | **--bind-ro**=*BIND_MOUNTS_RO*
89 |
90 | Bind mount from host to VM as read-only (format: host_path:guest_path)
91 |
92 | **--network**=*NETWORK*
93 |
94 | Network mode for the VM
95 |
96 | Default: user
97 |
98 | **--detach**
99 |
100 | Keep the VM running in background after creation
101 |
102 | **--ssh**
103 |
104 | Automatically SSH into the VM after creation
105 |
106 | **--ssh-wait**
107 |
108 | Wait for SSH to become available and verify connectivity (for testing)
109 |
110 | **--bind-storage-ro**
111 |
112 | Mount host container storage (RO) at /run/host-container-storage
113 |
114 | **--update-from-host**
115 |
116 | Implies --bind-storage-ro, but also configure to update from the host container storage by default
117 |
118 | **--firmware**=*FIRMWARE*
119 |
120 | Firmware type for the VM (defaults to uefi-secure)
121 |
122 | Possible values:
123 | - uefi-secure
124 | - uefi-insecure
125 | - bios
126 |
127 | Default: uefi-secure
128 |
129 | **--disable-tpm**
130 |
131 | Disable TPM 2.0 support (enabled by default)
132 |
133 | **--secure-boot-keys**=*SECURE_BOOT_KEYS*
134 |
135 | Directory containing secure boot keys (required for uefi-secure)
136 |
137 | **--label**=*LABEL*
138 |
139 | User-defined labels for organizing VMs (comma not allowed in labels)
140 |
141 | **--transient**
142 |
143 | Create a transient VM that disappears on shutdown/reboot
144 |
145 |
146 |
147 | # EXAMPLES
148 |
149 | Create and start a persistent VM:
150 |
151 | bcvk libvirt run --name my-server quay.io/fedora/fedora-bootc:42
152 |
153 | Create a VM with custom resources:
154 |
155 | bcvk libvirt run --name webserver --memory 8192 --cpus 8 --disk-size 50G quay.io/centos-bootc/centos-bootc:stream10
156 |
157 | Create a VM with port forwarding:
158 |
159 | bcvk libvirt run --name webserver --port 8080:80 quay.io/centos-bootc/centos-bootc:stream10
160 |
161 | Create a VM with volume mount:
162 |
163 | bcvk libvirt run --name devvm --volume /home/user/code:/workspace quay.io/fedora/fedora-bootc:42
164 |
165 | Create a VM and automatically SSH into it:
166 |
167 | bcvk libvirt run --name testvm --ssh quay.io/fedora/fedora-bootc:42
168 |
169 | Create a VM with access to host container storage for bootc upgrade:
170 |
171 | bcvk libvirt run --name upgrade-test --bind-storage-ro quay.io/fedora/fedora-bootc:42
172 |
173 | Server management workflow:
174 |
175 | # Create a persistent server VM
176 | bcvk libvirt run --name production-server --memory 8192 --cpus 4 --disk-size 100G my-server-image
177 |
178 | # Check status
179 | bcvk libvirt list
180 |
181 | # Access for maintenance
182 | bcvk libvirt ssh production-server
183 |
184 | # SEE ALSO
185 |
186 | **bcvk**(8)
187 |
188 | # VERSION
189 |
190 | v0.1.0
191 |
--------------------------------------------------------------------------------
/crates/xtask/src/xtask.rs:
--------------------------------------------------------------------------------
1 | //! See https://github.com/matklad/cargo-xtask
2 | //! This is kind of like "Justfile but in Rust".
3 |
4 | use std::process::Command;
5 |
6 | use color_eyre::eyre::{eyre, Context, Report};
7 | use color_eyre::Result;
8 | use xshell::Shell;
9 |
10 | mod man;
11 |
12 | #[allow(clippy::type_complexity)]
13 | const TASKS: &[(&str, fn(&Shell) -> Result<()>)] = &[
14 | ("manpages", manpages),
15 | ("update-manpages", update_manpages),
16 | ("sync-manpages", sync_manpages),
17 | ("package", package),
18 | ];
19 |
20 | fn install_tracing() {
21 | use tracing_error::ErrorLayer;
22 | use tracing_subscriber::fmt;
23 | use tracing_subscriber::prelude::*;
24 | use tracing_subscriber::EnvFilter;
25 |
26 | let fmt_layer = fmt::layer().with_target(false);
27 | let filter_layer = EnvFilter::try_from_default_env()
28 | .or_else(|_| EnvFilter::try_new("info"))
29 | .unwrap();
30 |
31 | tracing_subscriber::registry()
32 | .with(filter_layer)
33 | .with(fmt_layer)
34 | .with(ErrorLayer::default())
35 | .init();
36 | }
37 |
38 | fn main() -> Result<(), Report> {
39 | install_tracing();
40 | color_eyre::install()?;
41 | // Ensure our working directory is the toplevel
42 | {
43 | let toplevel_path = Command::new("git")
44 | .args(["rev-parse", "--show-toplevel"])
45 | .output()
46 | .context("Invoking git rev-parse")?;
47 | if !toplevel_path.status.success() {
48 | return Err(eyre!("Failed to invoke git rev-parse"));
49 | }
50 | let path = String::from_utf8(toplevel_path.stdout)?;
51 | std::env::set_current_dir(path.trim()).context("Changing to toplevel")?;
52 | }
53 |
54 | let task = std::env::args().nth(1);
55 |
56 | let sh = xshell::Shell::new()?;
57 | if let Some(cmd) = task.as_deref() {
58 | let f = TASKS
59 | .iter()
60 | .find_map(|(k, f)| (*k == cmd).then_some(*f))
61 | .unwrap_or(print_help);
62 | f(&sh)?;
63 | } else {
64 | print_help(&sh)?;
65 | }
66 | Ok(())
67 | }
68 |
69 | fn print_help(_sh: &Shell) -> Result<()> {
70 | println!("Tasks:");
71 | for (name, _) in TASKS {
72 | println!(" - {name}");
73 | }
74 | Ok(())
75 | }
76 |
77 | fn manpages(sh: &Shell) -> Result<()> {
78 | man::generate_man_pages(sh)
79 | }
80 |
81 | fn update_manpages(sh: &Shell) -> Result<()> {
82 | man::update_manpages(sh)
83 | }
84 |
85 | fn sync_manpages(sh: &Shell) -> Result<()> {
86 | man::sync_all_man_pages(sh)
87 | }
88 |
89 | fn package(sh: &Shell) -> Result<()> {
90 | use std::env;
91 | use xshell::cmd;
92 |
93 | // Get version from Cargo.toml
94 | let version = man::get_raw_package_version()?;
95 |
96 | println!("Creating release archives for version {}", version);
97 |
98 | // Get the git commit timestamp for reproducible builds
99 | let source_date_epoch = cmd!(sh, "git log -1 --format=%ct").read()?;
100 | env::set_var("SOURCE_DATE_EPOCH", source_date_epoch.trim());
101 |
102 | // Create target directory if it doesn't exist
103 | sh.create_dir("target")?;
104 |
105 | // Create temporary directory for intermediate files
106 | let tempdir = tempfile::tempdir()?;
107 | let temp_tar = tempdir.path().join(format!("bcvk-{}.tar", version));
108 |
109 | // Create source archive using git archive (uncompressed initially)
110 | let source_archive = format!("target/bcvk-{}.tar.zstd", version);
111 | cmd!(
112 | sh,
113 | "git archive --format=tar --prefix=bcvk-{version}/ HEAD -o {temp_tar}"
114 | )
115 | .run()?;
116 |
117 | // Create vendor archive
118 | let vendor_archive = format!("target/bcvk-{}-vendor.tar.zstd", version);
119 | cmd!(
120 | sh,
121 | "cargo vendor-filterer --format=tar.zstd {vendor_archive}"
122 | )
123 | .run()?;
124 |
125 | println!("Created vendor archive: {}", vendor_archive);
126 |
127 | // Create vendor config for the source archive
128 | let vendor_config_content = r#"[source.crates-io]
129 | replace-with = "vendored-sources"
130 |
131 | [source.vendored-sources]
132 | directory = "vendor"
133 | "#;
134 | let vendor_config_path = tempdir.path().join(".cargo-vendor-config.toml");
135 | std::fs::write(&vendor_config_path, vendor_config_content)?;
136 |
137 | // Add vendor config to source archive
138 | cmd!(sh, "tar --owner=0 --group=0 --numeric-owner --sort=name --mtime=@{source_date_epoch} -rf {temp_tar} --transform='s|.*/.cargo-vendor-config.toml|bcvk-{version}/.cargo/vendor-config.toml|' {vendor_config_path}").run()?;
139 |
140 | // Compress the final source archive
141 | cmd!(sh, "zstd {temp_tar} -f -o {source_archive}").run()?;
142 |
143 | println!("Created source archive: {}", source_archive);
144 |
145 | println!("Release archives created successfully:");
146 | println!(" Source: {}", source_archive);
147 | println!(" Vendor: {}", vendor_archive);
148 |
149 | Ok(())
150 | }
151 |
--------------------------------------------------------------------------------
/crates/kit/src/instancetypes.rs:
--------------------------------------------------------------------------------
1 | //! KubeVirt common-instancetypes support
2 | //!
3 | //! This module vendors the KubeVirt common-instancetypes definitions,
4 | //! specifically the U series (Universal/General Purpose) instance types.
5 | //! These provide standardized VM sizing with predefined vCPU and memory
6 | //! configurations.
7 | //!
8 | //! Instance types follow the format: u1.{size}
9 | //! Examples: u1.nano, u1.micro, u1.small, u1.medium, u1.large, etc.
10 | //!
11 | //! Source: https://github.com/kubevirt/common-instancetypes
12 |
13 | /// Instance type variants with associated vCPU and memory specifications
14 | ///
15 | /// Source: https://github.com/kubevirt/common-instancetypes/blob/main/instancetypes/u/1/sizes.yaml
16 | #[derive(
17 | Debug,
18 | Clone,
19 | Copy,
20 | PartialEq,
21 | Eq,
22 | serde::Serialize,
23 | serde::Deserialize,
24 | strum::Display,
25 | strum::EnumString,
26 | strum::EnumIter,
27 | )]
28 | #[non_exhaustive]
29 | pub enum InstanceType {
30 | /// u1.nano - 1 vCPU, 512 MiB memory
31 | #[strum(serialize = "u1.nano")]
32 | U1Nano,
33 | /// u1.micro - 1 vCPU, 1 GiB memory
34 | #[strum(serialize = "u1.micro")]
35 | U1Micro,
36 | /// u1.small - 1 vCPU, 2 GiB memory
37 | #[strum(serialize = "u1.small")]
38 | U1Small,
39 | /// u1.medium - 1 vCPU, 4 GiB memory
40 | #[strum(serialize = "u1.medium")]
41 | U1Medium,
42 | /// u1.2xmedium - 2 vCPU, 4 GiB memory
43 | #[strum(serialize = "u1.2xmedium")]
44 | U1TwoXMedium,
45 | /// u1.large - 2 vCPU, 8 GiB memory
46 | #[strum(serialize = "u1.large")]
47 | U1Large,
48 | /// u1.xlarge - 4 vCPU, 16 GiB memory
49 | #[strum(serialize = "u1.xlarge")]
50 | U1XLarge,
51 | /// u1.2xlarge - 8 vCPU, 32 GiB memory
52 | #[strum(serialize = "u1.2xlarge")]
53 | U1TwoXLarge,
54 | /// u1.4xlarge - 16 vCPU, 64 GiB memory
55 | #[strum(serialize = "u1.4xlarge")]
56 | U1FourXLarge,
57 | /// u1.8xlarge - 32 vCPU, 128 GiB memory
58 | #[strum(serialize = "u1.8xlarge")]
59 | U1EightXLarge,
60 | }
61 |
62 | impl InstanceType {
63 | /// Get the number of vCPUs for this instance type
64 | pub const fn vcpus(self) -> u32 {
65 | match self {
66 | Self::U1Nano => 1,
67 | Self::U1Micro => 1,
68 | Self::U1Small => 1,
69 | Self::U1Medium => 1,
70 | Self::U1TwoXMedium => 2,
71 | Self::U1Large => 2,
72 | Self::U1XLarge => 4,
73 | Self::U1TwoXLarge => 8,
74 | Self::U1FourXLarge => 16,
75 | Self::U1EightXLarge => 32,
76 | }
77 | }
78 |
79 | /// Get the memory in megabytes for this instance type
80 | pub const fn memory_mb(self) -> u32 {
81 | match self {
82 | Self::U1Nano => 512,
83 | Self::U1Micro => 1024,
84 | Self::U1Small => 2048,
85 | Self::U1Medium => 4096,
86 | Self::U1TwoXMedium => 4096,
87 | Self::U1Large => 8192,
88 | Self::U1XLarge => 16384,
89 | Self::U1TwoXLarge => 32768,
90 | Self::U1FourXLarge => 65536,
91 | Self::U1EightXLarge => 131072,
92 | }
93 | }
94 | }
95 |
96 | #[cfg(test)]
97 | mod tests {
98 | use super::*;
99 | use std::str::FromStr;
100 | use strum::IntoEnumIterator;
101 |
102 | #[test]
103 | fn test_properties() {
104 | for variant in InstanceType::iter() {
105 | let (expected_vcpus, expected_memory_mb) = match variant {
106 | InstanceType::U1Nano => (1, 512),
107 | InstanceType::U1Micro => (1, 1024),
108 | InstanceType::U1Small => (1, 2048),
109 | InstanceType::U1Medium => (1, 4096),
110 | InstanceType::U1TwoXMedium => (2, 4096),
111 | InstanceType::U1Large => (2, 8192),
112 | InstanceType::U1XLarge => (4, 16384),
113 | InstanceType::U1TwoXLarge => (8, 32768),
114 | InstanceType::U1FourXLarge => (16, 65536),
115 | InstanceType::U1EightXLarge => (32, 131072),
116 | };
117 | assert_eq!(
118 | variant.vcpus(),
119 | expected_vcpus,
120 | "Mismatch in vcpus for {:?}",
121 | variant
122 | );
123 | assert_eq!(
124 | variant.memory_mb(),
125 | expected_memory_mb,
126 | "Mismatch in memory_mb for {:?}",
127 | variant
128 | );
129 | }
130 | }
131 |
132 | #[test]
133 | fn test_parse_invalid_instancetype() {
134 | let result = InstanceType::from_str("invalid");
135 | assert!(result.is_err());
136 | }
137 |
138 | #[test]
139 | fn test_roundtrip() {
140 | for variant in InstanceType::iter() {
141 | let s = variant.to_string();
142 | let parsed = InstanceType::from_str(&s).unwrap();
143 | assert_eq!(parsed, variant);
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 | workflow_dispatch:
9 |
10 | env:
11 | # Something seems to be setting this in the default GHA runners, which breaks bcvk
12 | # as the default runner user doesn't have access
13 | LIBVIRT_DEFAULT_URI: "qemu:///session"
14 |
15 |
16 | jobs:
17 | build:
18 | runs-on: ubuntu-24.04
19 |
20 | steps:
21 | - uses: actions/checkout@v4
22 |
23 | - name: Setup bootc Ubuntu environment
24 | uses: ./.github/actions/bootc-ubuntu-setup
25 | with:
26 | libvirt: 'true'
27 |
28 | - name: Install additional dependencies
29 | run: sudo apt install -y go-md2man
30 |
31 | - name: Extract image lists from Justfile
32 | run: |
33 | echo "PRIMARY_IMAGE=$(just --evaluate PRIMARY_IMAGE)" >> $GITHUB_ENV
34 | echo "ALL_BASE_IMAGES=$(just --evaluate ALL_BASE_IMAGES)" >> $GITHUB_ENV
35 |
36 | - name: Setup Rust
37 | uses: ./.github/actions/setup-rust
38 |
39 | - name: Build
40 | run: just validate && just build
41 |
42 | - name: Run unit tests
43 | run: just unit
44 |
45 | - name: Pull test images
46 | run: just pull-test-images
47 |
48 | - name: Create nextest archive
49 | run: |
50 | cargo nextest archive --release -p integration-tests --archive-file nextest-archive.tar.zst
51 | env:
52 | BCVK_PATH: ${{ github.workspace }}/target/release/bcvk
53 | BCVK_PRIMARY_IMAGE: ${{ env.PRIMARY_IMAGE }}
54 | BCVK_ALL_IMAGES: ${{ env.ALL_BASE_IMAGES }}
55 |
56 | - name: Upload nextest archive
57 | uses: actions/upload-artifact@v4
58 | with:
59 | name: nextest-archive
60 | path: nextest-archive.tar.zst
61 | retention-days: 7
62 |
63 | - name: Upload bcvk binary for tests
64 | uses: actions/upload-artifact@v4
65 | with:
66 | name: bcvk-binary-tests
67 | path: target/release/bcvk
68 | retention-days: 7
69 |
70 | - name: Create bcvk archive
71 | run: just archive
72 |
73 | - name: Upload bcvk binary artifacts
74 | if: github.event_name == 'push' && github.ref == 'refs/heads/main'
75 | uses: actions/upload-artifact@v4
76 | with:
77 | name: bcvk-binary
78 | path: |
79 | target/bcvk-*.tar.gz
80 | target/bcvk-*.tar.gz.sha256
81 | retention-days: 7
82 |
83 | integration-tests:
84 | runs-on: ubuntu-24.04
85 | needs: build
86 | strategy:
87 | fail-fast: false
88 | matrix:
89 | partition: [1, 2, 3, 4]
90 |
91 | steps:
92 | - uses: actions/checkout@v4
93 |
94 | - uses: ./.github/actions/bootc-ubuntu-setup
95 | with:
96 | libvirt: 'true'
97 |
98 | - name: Extract image lists from Justfile
99 | run: |
100 | echo "PRIMARY_IMAGE=$(just --evaluate PRIMARY_IMAGE)" >> $GITHUB_ENV
101 | echo "ALL_BASE_IMAGES=$(just --evaluate ALL_BASE_IMAGES)" >> $GITHUB_ENV
102 |
103 | - name: Setup Rust
104 | uses: ./.github/actions/setup-rust
105 |
106 | - name: Pull test images
107 | run: just pull-test-images
108 |
109 | - name: Download nextest archive
110 | uses: actions/download-artifact@v4
111 | with:
112 | name: nextest-archive
113 |
114 | - name: Download bcvk binary
115 | uses: actions/download-artifact@v4
116 | with:
117 | name: bcvk-binary-tests
118 | path: target/release
119 |
120 | - name: Make bcvk executable
121 | run: chmod +x target/release/bcvk
122 |
123 | - name: Run integration tests (partition ${{ matrix.partition }}/4)
124 | run: |
125 | # Clean up any leftover containers before starting
126 | cargo run --release --bin test-cleanup -p integration-tests 2>/dev/null || true
127 |
128 | # Run the partitioned tests
129 | cargo nextest run --archive-file nextest-archive.tar.zst \
130 | --profile integration \
131 | --partition hash:${{ matrix.partition }}/4
132 |
133 | # Clean up containers after tests complete
134 | cargo run --release --bin test-cleanup -p integration-tests 2>/dev/null || true
135 | env:
136 | BCVK_PATH: ${{ github.workspace }}/target/release/bcvk
137 | BCVK_PRIMARY_IMAGE: ${{ env.PRIMARY_IMAGE }}
138 | BCVK_ALL_IMAGES: ${{ env.ALL_BASE_IMAGES }}
139 |
140 | - name: Upload junit XML
141 | if: always()
142 | uses: actions/upload-artifact@v4
143 | with:
144 | name: integration-junit-xml-${{ matrix.partition }}
145 | path: target/nextest/integration/junit.xml
146 | retention-days: 7
147 |
148 | # Sentinel job for required checks - configure this job name in repository settings
149 | required-checks:
150 | if: always()
151 | needs: [build, integration-tests]
152 | runs-on: ubuntu-latest
153 | steps:
154 | - run: exit 1
155 | if: >-
156 | needs.build.result != 'success' ||
157 | needs.integration-tests.result != 'success'
158 |
--------------------------------------------------------------------------------
/crates/kit/src/arch.rs:
--------------------------------------------------------------------------------
1 | //! Architecture detection and configuration utilities
2 | //!
3 | //! This module provides cross-architecture support for libvirt domain creation
4 | //! and QEMU emulator selection, avoiding hardcoded architecture assumptions.
5 |
6 | use crate::xml_utils::XmlWriter;
7 | use color_eyre::Result;
8 |
9 | /// Architecture configuration for libvirt domains and QEMU
10 | #[derive(Debug, Clone)]
11 | pub struct ArchConfig {
12 | /// Architecture string for libvirt (e.g., "x86_64", "aarch64")
13 | pub arch: &'static str,
14 | /// Machine type for libvirt (e.g., "q35", "virt")
15 | pub machine: &'static str,
16 | /// OS type for libvirt (usually "hvm")
17 | pub os_type: &'static str,
18 | }
19 |
20 | impl ArchConfig {
21 | /// Detect host architecture and return appropriate configuration
22 | pub fn detect() -> Result {
23 | let arch = std::env::consts::ARCH;
24 | match arch {
25 | "x86_64" => Ok(Self {
26 | arch: "x86_64",
27 | machine: "q35",
28 | os_type: "hvm",
29 | }),
30 | "aarch64" => Ok(Self {
31 | arch: "aarch64",
32 | machine: "virt",
33 | os_type: "hvm",
34 | }),
35 | // Add more architectures as needed
36 | // "riscv64" => Ok(Self {
37 | // arch: "riscv64",
38 | // machine: "virt",
39 | // os_type: "hvm",
40 | // }),
41 | unsupported => Err(color_eyre::eyre::eyre!(
42 | "Unsupported architecture: {}. Supported architectures: x86_64, aarch64",
43 | unsupported
44 | )),
45 | }
46 | }
47 |
48 | /// Generate architecture-specific timer configuration
49 | pub fn write_timers(&self, writer: &mut XmlWriter) -> Result<()> {
50 | // RTC timer is common to all architectures
51 | writer.write_empty_element("timer", &[("name", "rtc"), ("tickpolicy", "catchup")])?;
52 |
53 | // Add x86_64-specific timers
54 | if self.arch == "x86_64" {
55 | writer.write_empty_element("timer", &[("name", "pit"), ("tickpolicy", "delay")])?;
56 | writer.write_empty_element("timer", &[("name", "hpet"), ("present", "no")])?;
57 | }
58 |
59 | Ok(())
60 | }
61 |
62 | /// Check if this architecture supports VMport (x86_64 specific feature)
63 | #[allow(dead_code)]
64 | pub fn supports_vmport(&self) -> bool {
65 | self.arch == "x86_64"
66 | }
67 |
68 | /// Get recommended CPU mode for this architecture
69 | pub fn cpu_mode(&self) -> &'static str {
70 | match self.arch {
71 | "x86_64" => "host-passthrough",
72 | "aarch64" => "host-passthrough",
73 | _ => "host-model",
74 | }
75 | }
76 | }
77 |
78 | /// Detect host architecture string (shorthand for ArchConfig::detect().arch)
79 | #[allow(dead_code)]
80 | pub fn host_arch() -> Result<&'static str> {
81 | Ok(ArchConfig::detect()?.arch)
82 | }
83 |
84 | /// Check if running on x86_64 architecture
85 | #[allow(dead_code)]
86 | pub fn is_x86_64() -> bool {
87 | std::env::consts::ARCH == "x86_64"
88 | }
89 |
90 | /// Check if running on ARM64/AArch64 architecture
91 | #[allow(dead_code)]
92 | pub fn is_aarch64() -> bool {
93 | std::env::consts::ARCH == "aarch64"
94 | }
95 |
96 | #[cfg(test)]
97 | mod tests {
98 | use super::*;
99 |
100 | #[test]
101 | fn test_arch_detection() {
102 | let arch_config = ArchConfig::detect().unwrap();
103 |
104 | // Should detect the current architecture
105 | assert_eq!(arch_config.arch, std::env::consts::ARCH);
106 |
107 | // Should have valid configuration
108 | assert!(!arch_config.machine.is_empty());
109 | assert_eq!(arch_config.os_type, "hvm");
110 | }
111 |
112 | #[test]
113 | fn test_arch_specific_features() {
114 | let arch_config = ArchConfig::detect().unwrap();
115 |
116 | // Test that we can generate timers XML without errors
117 | let mut writer = XmlWriter::new();
118 | arch_config.write_timers(&mut writer).unwrap();
119 | let timers_xml = writer.into_string().unwrap();
120 | assert!(timers_xml.contains("timer"));
121 | assert!(timers_xml.contains("rtc"));
122 |
123 | // CPU mode should be valid
124 | assert!(!arch_config.cpu_mode().is_empty());
125 | }
126 |
127 | #[test]
128 | fn test_vmport_support() {
129 | let arch_config = ArchConfig::detect().unwrap();
130 |
131 | // VMport support should match architecture
132 | if arch_config.arch == "x86_64" {
133 | assert!(arch_config.supports_vmport());
134 | } else {
135 | assert!(!arch_config.supports_vmport());
136 | }
137 | }
138 |
139 | #[test]
140 | fn test_helper_functions() {
141 | let detected_arch = host_arch().unwrap();
142 | assert_eq!(detected_arch, std::env::consts::ARCH);
143 |
144 | // At least one should be true
145 | assert!(is_x86_64() || is_aarch64());
146 |
147 | // Should be mutually exclusive
148 | assert!(!(is_x86_64() && is_aarch64()));
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/crates/kit/src/libvirt/base_disks_cli.rs:
--------------------------------------------------------------------------------
1 | //! Base disk management CLI commands
2 | //!
3 | //! This module provides CLI commands for managing base disk images that serve
4 | //! as CoW sources for VM disks.
5 |
6 | use clap::{Parser, Subcommand};
7 | use color_eyre::Result;
8 | use comfy_table::{presets::UTF8_FULL, Table};
9 | use serde_json;
10 |
11 | use super::base_disks::{list_base_disks, prune_base_disks};
12 | use super::OutputFormat;
13 |
14 | /// Options for base-disks command
15 | #[derive(Debug, Parser)]
16 | pub struct LibvirtBaseDisksOpts {
17 | #[command(subcommand)]
18 | pub command: BaseDisksSubcommand,
19 | }
20 |
21 | /// Base disk subcommands
22 | #[derive(Debug, Subcommand)]
23 | pub enum BaseDisksSubcommand {
24 | /// List all base disk images
25 | List(ListOpts),
26 | /// Prune unreferenced base disk images
27 | Prune(PruneOpts),
28 | }
29 |
30 | /// Options for list command
31 | #[derive(Debug, Parser)]
32 | pub struct ListOpts {
33 | /// Output format
34 | #[clap(long, value_enum, default_value_t = OutputFormat::Table)]
35 | pub format: OutputFormat,
36 | }
37 |
38 | /// Options for prune command
39 | #[derive(Debug, Parser)]
40 | pub struct PruneOpts {
41 | /// Show what would be removed without actually removing
42 | #[clap(long)]
43 | pub dry_run: bool,
44 | }
45 |
46 | /// Execute the base-disks command
47 | pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtBaseDisksOpts) -> Result<()> {
48 | let connect_uri = global_opts.connect.as_deref();
49 |
50 | match opts.command {
51 | BaseDisksSubcommand::List(list_opts) => run_list(connect_uri, list_opts),
52 | BaseDisksSubcommand::Prune(prune_opts) => run_prune(connect_uri, prune_opts),
53 | }
54 | }
55 |
56 | /// Execute the list subcommand
57 | fn run_list(connect_uri: Option<&str>, opts: ListOpts) -> Result<()> {
58 | let base_disks = list_base_disks(connect_uri)?;
59 |
60 | match opts.format {
61 | OutputFormat::Table => {
62 | if base_disks.is_empty() {
63 | println!("No base disk images found");
64 | return Ok(());
65 | }
66 |
67 | let mut table = Table::new();
68 | table.load_preset(UTF8_FULL);
69 | table.set_header(vec!["NAME", "SIZE", "REFS", "CREATED", "IMAGE DIGEST"]);
70 |
71 | for disk in &base_disks {
72 | let name = disk.path.file_name().unwrap_or("unknown");
73 |
74 | let size = disk
75 | .size
76 | .map(|bytes| indicatif::BinaryBytes(bytes).to_string())
77 | .unwrap_or_else(|| "unknown".to_string());
78 |
79 | let refs = disk.ref_count.to_string();
80 |
81 | let created = disk
82 | .created
83 | .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
84 | .and_then(|d| chrono::DateTime::from_timestamp(d.as_secs() as i64, 0))
85 | .map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
86 | .unwrap_or_else(|| "unknown".to_string());
87 |
88 | let digest = disk
89 | .image_digest
90 | .as_ref()
91 | .map(|d| {
92 | // Truncate long digests for display
93 | if d.len() > 56 {
94 | format!("{}...", &d[..53])
95 | } else {
96 | d.clone()
97 | }
98 | })
99 | .unwrap_or_else(|| "".to_string());
100 |
101 | table.add_row(vec![name, &size, &refs, &created, &digest]);
102 | }
103 |
104 | println!("{}", table);
105 | println!(
106 | "\nFound {} base disk{}",
107 | base_disks.len(),
108 | if base_disks.len() == 1 { "" } else { "s" }
109 | );
110 | }
111 | OutputFormat::Json => {
112 | println!("{}", serde_json::to_string_pretty(&base_disks)?);
113 | }
114 | OutputFormat::Yaml => {
115 | return Err(color_eyre::eyre::eyre!(
116 | "YAML format is not supported for base-disks list command"
117 | ))
118 | }
119 | OutputFormat::Xml => {
120 | return Err(color_eyre::eyre::eyre!(
121 | "XML format is not supported for base-disks list command"
122 | ))
123 | }
124 | }
125 |
126 | Ok(())
127 | }
128 |
129 | /// Execute the prune subcommand
130 | fn run_prune(connect_uri: Option<&str>, opts: PruneOpts) -> Result<()> {
131 | if opts.dry_run {
132 | println!("Dry run: showing base disks that would be removed");
133 | }
134 |
135 | let pruned = prune_base_disks(connect_uri, opts.dry_run)?;
136 |
137 | if pruned.is_empty() {
138 | println!("No unreferenced base disks found to remove");
139 | } else {
140 | println!(
141 | "\n{} {} base disk{}",
142 | if opts.dry_run {
143 | "Would remove"
144 | } else {
145 | "Removed"
146 | },
147 | pruned.len(),
148 | if pruned.len() == 1 { "" } else { "s" }
149 | );
150 | }
151 |
152 | Ok(())
153 | }
154 |
--------------------------------------------------------------------------------
/.github/workflows/scheduled-release.yml:
--------------------------------------------------------------------------------
1 | # Keep this in sync with the code in bootc-dev/bootc
2 |
3 | name: Create Release PR
4 |
5 | on:
6 | schedule:
7 | # Run every 3 weeks on Monday at 8:00 AM UTC
8 | # Note: GitHub Actions doesn't support "every 3 weeks" directly,
9 | # so we use a workaround by running weekly and checking if it's been 3 weeks
10 | - cron: '0 8 * * 1'
11 | workflow_dispatch:
12 | inputs:
13 | version:
14 | description: 'Version to release (e.g., 0.2.0). Leave empty to auto-increment.'
15 | required: false
16 | type: string
17 |
18 | permissions:
19 | contents: write
20 | pull-requests: write
21 |
22 | jobs:
23 | create-release-pr:
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: actions/create-github-app-token@v2
27 | id: app-token
28 | with:
29 | app-id: ${{ secrets.APP_ID }}
30 | private-key: ${{ secrets.APP_PRIVATE_KEY }}
31 |
32 | - name: Checkout repository
33 | uses: actions/checkout@v4
34 | with:
35 | fetch-depth: 0
36 | token: ${{ steps.app-token.outputs.token }}
37 | persist-credentials: false
38 |
39 | - name: Configure git safety
40 | if: steps.check_schedule.outputs.should_release == 'true'
41 | run: |
42 | git config --global --add safe.directory "$GITHUB_WORKSPACE"
43 |
44 | - name: Check if it's time for a release
45 | id: check_schedule
46 | run: |
47 | # For manual workflow dispatch, always proceed
48 | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
49 | echo "should_release=true" >> $GITHUB_OUTPUT
50 | exit 0
51 | fi
52 |
53 | START_DATE="2025-09-22" # start of a 3 week sprint
54 | START_TIMESTAMP=$(date -d "$START_DATE" +%s)
55 | CURRENT_TIMESTAMP=$(date +%s)
56 | # Add 12 hour buffer (43200 seconds) to account for scheduling delays
57 | ADJUSTED_TIMESTAMP=$((CURRENT_TIMESTAMP + 43200))
58 | DAYS_SINCE_START=$(( (ADJUSTED_TIMESTAMP - START_TIMESTAMP) / 86400 ))
59 | WEEKS_SINCE_START=$(( DAYS_SINCE_START / 7 ))
60 |
61 | echo "Days since start date ($START_DATE): $DAYS_SINCE_START"
62 | echo "Weeks since start date: $WEEKS_SINCE_START"
63 |
64 | # Release every 3 weeks
65 | if [ $WEEKS_SINCE_START -gt 0 ] && [ $((WEEKS_SINCE_START % 3)) -eq 0 ]; then
66 | echo "should_release=true" >> $GITHUB_OUTPUT
67 | else
68 | echo "should_release=false" >> $GITHUB_OUTPUT
69 | fi
70 |
71 | - name: Import GPG key
72 | if: steps.check_schedule.outputs.should_release == 'true'
73 | uses: crazy-max/ghaction-import-gpg@v6
74 | with:
75 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
76 | passphrase: ${{ secrets.GPG_PASSPHRASE }}
77 | git_user_signingkey: true
78 | git_commit_gpgsign: true
79 | git_tag_gpgsign: true
80 |
81 | - name: Setup Rust
82 | if: steps.check_schedule.outputs.should_release == 'true'
83 | uses: dtolnay/rust-toolchain@stable
84 |
85 | - name: Install cargo-edit
86 | if: steps.check_schedule.outputs.should_release == 'true'
87 | run: cargo install cargo-edit
88 |
89 | - name: Generate release changes
90 | id: create_commit
91 | if: steps.check_schedule.outputs.should_release == 'true'
92 | env:
93 | INPUT_VERSION: ${{ github.event.inputs.version }}
94 | run: |
95 | # If version is provided via workflow dispatch, validate and use it
96 | if [ -n "$INPUT_VERSION" ]; then
97 | VERSION="$INPUT_VERSION"
98 | # Validate version format strictly
99 | if ! echo "$VERSION" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' >/dev/null; then
100 | echo "Error: Invalid version format. Expected X.Y.Z (e.g., 0.2.0)"
101 | exit 1
102 | fi
103 | cargo set-version --manifest-path crates/kit/Cargo.toml --package bcvk "$VERSION"
104 | else
105 | # default to bump the minor since that is most common
106 | cargo set-version --manifest-path crates/kit/Cargo.toml --package bcvk --bump minor
107 | VERSION=$(cargo read-manifest --manifest-path crates/kit/Cargo.toml | jq -r '.version')
108 | fi
109 |
110 | cargo update --workspace
111 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
112 |
113 | - name: Create Pull Request
114 | uses: peter-evans/create-pull-request@v7
115 | env:
116 | VERSION: ${{ steps.create_commit.outputs.VERSION }}
117 | with:
118 | token: ${{ steps.app-token.outputs.token }}
119 | title: "Release ${{ env.VERSION }}"
120 | commit-message: "Release ${{ env.VERSION }}"
121 | branch: "release-${{ env.VERSION }}"
122 | delete-branch: true
123 | labels: release
124 | signoff: true
125 | sign-commits: true
126 | body: |
127 | ## Release ${{ env.VERSION }}
128 |
129 | This is an automated release PR created by the scheduled release workflow.
130 |
131 | ### Release Process
132 |
133 | 1. Review the changes in this PR
134 | 2. Ensure all tests pass
135 | 3. Merge the PR
136 | 4. The release tag will be automatically created when this PR is merged
137 |
138 | The release workflow will automatically trigger when the tag is pushed.
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # Keep this in sync with the code in bootc-dev/bootc
2 | name: Release
3 |
4 | on:
5 | pull_request:
6 | types: [closed]
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | release:
13 | name: Create Release
14 | if: |
15 | (github.event_name == 'pull_request' &&
16 | github.event.pull_request.merged == true &&
17 | contains(github.event.pull_request.labels.*.name, 'release'))
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/create-github-app-token@v2
21 | id: app-token
22 | with:
23 | app-id: ${{ secrets.APP_ID }}
24 | private-key: ${{ secrets.APP_PRIVATE_KEY }}
25 |
26 | - name: Checkout repository
27 | uses: actions/checkout@v4
28 | with:
29 | fetch-depth: 0
30 | token: ${{ steps.app-token.outputs.token }}
31 |
32 | - name: Extract version
33 | id: extract_version
34 | run: |
35 | # Extract version from crates/kit/Cargo.toml
36 | VERSION=$(cargo read-manifest --manifest-path crates/kit/Cargo.toml | jq -r '.version')
37 |
38 | # Validate version format
39 | if ! echo "$VERSION" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' >/dev/null; then
40 | echo "Error: Invalid version format in Cargo.toml: $VERSION"
41 | exit 1
42 | fi
43 |
44 | echo "Extracted version: $VERSION"
45 | echo "version=$VERSION" >> $GITHUB_OUTPUT
46 | echo "TAG_NAME=v$VERSION" >> $GITHUB_OUTPUT
47 |
48 | - name: Import GPG key
49 | if: github.event_name != 'push'
50 | uses: crazy-max/ghaction-import-gpg@v6
51 | with:
52 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
53 | passphrase: ${{ secrets.GPG_PASSPHRASE }}
54 | git_user_signingkey: true
55 | git_commit_gpgsign: true
56 | git_tag_gpgsign: true
57 |
58 | - name: Create and push tag
59 | if: github.event_name != 'push'
60 | run: |
61 | VERSION="${{ steps.extract_version.outputs.version }}"
62 | TAG_NAME="v$VERSION"
63 |
64 | if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
65 | echo "Tag $TAG_NAME already exists"
66 | exit 0
67 | fi
68 |
69 | git tag -s -m "Release $VERSION" "$TAG_NAME"
70 | git push origin "$TAG_NAME"
71 |
72 | echo "Successfully created and pushed tag $TAG_NAME"
73 |
74 | git checkout "$TAG_NAME"
75 |
76 | - name: Install dependencies
77 | run: |
78 | sudo apt update
79 | sudo apt install -y just pkg-config go-md2man libssl-dev
80 |
81 | - name: Setup Rust
82 | uses: dtolnay/rust-toolchain@stable
83 |
84 | - name: Cache build artifacts
85 | uses: Swatinem/rust-cache@v2
86 | with:
87 | key: release-build
88 |
89 | - name: Install additional dependencies
90 | run: |
91 | # Install cargo-vendor-filterer for creating vendor archives
92 | cargo install cargo-vendor-filterer --locked
93 |
94 | - name: Build binaries and create archives
95 | run: |
96 | # Build release binaries
97 | just build
98 |
99 | # Create binary archives (existing functionality)
100 | just archive
101 |
102 | # Create source and vendor archives for distribution
103 | cargo xtask package
104 | env:
105 | CARGO_PROFILE_RELEASE_LTO: true
106 | CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 1
107 |
108 | - name: Create release
109 | env:
110 | GH_TOKEN: ${{ steps.app-token.outputs.token }}
111 | run: |
112 | VERSION="${{ steps.extract_version.outputs.version }}"
113 | TAG_NAME="${{ steps.extract_version.outputs.TAG_NAME }}"
114 | PRERELEASE=""
115 | if [[ "$VERSION" == *"-"* ]]; then
116 | PRERELEASE="--prerelease"
117 | fi
118 |
119 | gh release create "$TAG_NAME" \
120 | --draft \
121 | --title "Release $TAG_NAME" \
122 | --notes "Release $TAG_NAME
123 |
124 | ## Installation
125 |
126 | Download the appropriate binary for your platform from the assets below.
127 |
128 | ### Linux x86_64 (glibc)
129 | \`\`\`bash
130 | curl -LO https://github.com/${{ github.repository }}/releases/download/$TAG_NAME/bcvk-x86_64-unknown-linux-gnu.tar.gz
131 | tar xzf bcvk-x86_64-unknown-linux-gnu.tar.gz
132 | sudo mv bcvk-x86_64-unknown-linux-gnu /usr/local/bin/bcvk
133 | \`\`\`
134 |
135 |
136 | ## Checksums
137 |
138 | Verify the integrity of your download with the provided SHA256 checksums.
139 | " \
140 | $PRERELEASE
141 |
142 | - name: Upload to release
143 | env:
144 | GH_TOKEN: ${{ steps.app-token.outputs.token }}
145 | run: |
146 | cd target
147 | # Upload binary archives and checksums
148 | for file in bcvk-*.tar.gz bcvk-*.tar.gz.sha256; do
149 | echo "Uploading binary archive: $file"
150 | gh release upload "${{ steps.extract_version.outputs.TAG_NAME }}" "$file" --clobber
151 | done
152 |
153 | # Upload source and vendor archives
154 | for file in bcvk-*.tar.zstd; do
155 | echo "Uploading source archive: $file"
156 | gh release upload "${{ steps.extract_version.outputs.TAG_NAME }}" "$file" --clobber
157 | done
--------------------------------------------------------------------------------
/crates/kit/src/libvirt/list.rs:
--------------------------------------------------------------------------------
1 | //! libvirt list command - list bootc domains
2 | //!
3 | //! This module provides functionality to list libvirt domains that were
4 | //! created from bootc container images, showing their status and metadata.
5 |
6 | use clap::Parser;
7 | use color_eyre::Result;
8 | use comfy_table::{presets::UTF8_FULL, Table};
9 |
10 | use super::OutputFormat;
11 |
12 | /// Options for listing libvirt domains
13 | #[derive(Debug, Parser)]
14 | pub struct LibvirtListOpts {
15 | /// Domain name to query (returns only this domain)
16 | pub domain_name: Option,
17 |
18 | /// Output format
19 | #[clap(long, value_enum, default_value_t = OutputFormat::Table)]
20 | pub format: OutputFormat,
21 |
22 | /// Show all domains including stopped ones
23 | #[clap(long, short = 'a')]
24 | pub all: bool,
25 |
26 | /// Filter domains by label
27 | #[clap(long)]
28 | pub label: Option,
29 | }
30 |
31 | /// Execute the libvirt list command
32 | pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtListOpts) -> Result<()> {
33 | use crate::domain_list::DomainLister;
34 | use color_eyre::eyre::Context;
35 |
36 | // Use libvirt as the source of truth for domain listing
37 | let connect_uri = global_opts.connect.as_ref();
38 | let lister = match connect_uri {
39 | Some(uri) => DomainLister::with_connection(uri.clone()),
40 | None => DomainLister::new(),
41 | };
42 |
43 | let mut domains = if let Some(ref domain_name) = opts.domain_name {
44 | // Query specific domain by name
45 | match lister.get_domain_info(domain_name) {
46 | Ok(domain) => vec![domain],
47 | Err(e) => {
48 | return Err(color_eyre::eyre::eyre!(
49 | "Failed to get domain '{}': {}",
50 | domain_name,
51 | e
52 | ));
53 | }
54 | }
55 | } else if opts.all {
56 | lister
57 | .list_bootc_domains()
58 | .with_context(|| "Failed to list bootc domains from libvirt")?
59 | } else {
60 | lister
61 | .list_running_bootc_domains()
62 | .with_context(|| "Failed to list running bootc domains from libvirt")?
63 | };
64 |
65 | // Filter by label if specified
66 | if let Some(ref filter_label) = opts.label {
67 | domains.retain(|d| d.labels.contains(filter_label));
68 | }
69 |
70 | match opts.format {
71 | OutputFormat::Table => {
72 | if domains.is_empty() {
73 | if opts.all {
74 | println!("No VMs found");
75 | println!("Tip: Create VMs with 'bcvk libvirt run '");
76 | } else {
77 | println!("No running VMs found");
78 | println!(
79 | "Use --all to see stopped VMs or 'bcvk libvirt run ' to create one"
80 | );
81 | }
82 | return Ok(());
83 | }
84 |
85 | let mut table = Table::new();
86 | table.load_preset(UTF8_FULL);
87 | table.set_header(vec!["NAME", "IMAGE", "STATUS", "MEMORY", "SSH"]);
88 |
89 | for domain in &domains {
90 | let image = match &domain.image {
91 | Some(img) => {
92 | if img.len() > 38 {
93 | format!("{}...", &img[..35])
94 | } else {
95 | img.clone()
96 | }
97 | }
98 | None => "".to_string(),
99 | };
100 | let memory = match domain.memory_mb {
101 | Some(mem) => format!("{}MB", mem),
102 | None => "unknown".to_string(),
103 | };
104 | let ssh = match domain.ssh_port {
105 | Some(port) if domain.has_ssh_key => format!(":{}", port),
106 | Some(port) => format!(":{}*", port),
107 | None => "-".to_string(),
108 | };
109 | table.add_row(vec![
110 | &domain.name,
111 | &image,
112 | &domain.status_string(),
113 | &memory,
114 | &ssh,
115 | ]);
116 | }
117 |
118 | println!("{}", table);
119 | println!(
120 | "\nFound {} domain{} (source: libvirt)",
121 | domains.len(),
122 | if domains.len() == 1 { "" } else { "s" }
123 | );
124 | }
125 | OutputFormat::Json => {
126 | // If querying a specific domain, return object directly instead of array
127 | if opts.domain_name.is_some() && !domains.is_empty() {
128 | println!(
129 | "{}",
130 | serde_json::to_string_pretty(&domains[0])
131 | .with_context(|| "Failed to serialize domain as JSON")?
132 | );
133 | } else {
134 | println!(
135 | "{}",
136 | serde_json::to_string_pretty(&domains)
137 | .with_context(|| "Failed to serialize domains as JSON")?
138 | );
139 | }
140 | }
141 | OutputFormat::Yaml => {
142 | return Err(color_eyre::eyre::eyre!(
143 | "YAML format is not supported for list command"
144 | ))
145 | }
146 | OutputFormat::Xml => {
147 | return Err(color_eyre::eyre::eyre!(
148 | "XML format is not supported for list command"
149 | ))
150 | }
151 | }
152 | Ok(())
153 | }
154 |
--------------------------------------------------------------------------------
/crates/kit/src/libvirt/rm_all.rs:
--------------------------------------------------------------------------------
1 | //! libvirt rm-all command - remove multiple bootc domains and their resources
2 | //!
3 | //! This module provides functionality to remove multiple libvirt domains
4 | //! and their associated resources at once, with optional label filtering.
5 |
6 | use clap::Parser;
7 | use color_eyre::Result;
8 |
9 | /// Options for removing multiple libvirt domains
10 | #[derive(Debug, Parser)]
11 | pub struct LibvirtRmAllOpts {
12 | /// Force removal without confirmation
13 | #[clap(long, short = 'f')]
14 | pub force: bool,
15 |
16 | /// Remove domains even if they're running
17 | #[clap(long)]
18 | pub stop: bool,
19 |
20 | /// Filter domains by label (only remove domains with this label)
21 | #[clap(long)]
22 | pub label: Option,
23 | }
24 |
25 | /// Execute the libvirt rm-all command
26 | pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtRmAllOpts) -> Result<()> {
27 | use crate::domain_list::DomainLister;
28 | use color_eyre::eyre::Context;
29 |
30 | let connect_uri = global_opts.connect.as_ref();
31 | let lister = match connect_uri {
32 | Some(uri) => DomainLister::with_connection(uri.clone()),
33 | None => DomainLister::new(),
34 | };
35 |
36 | // Get all bootc domains
37 | let mut domains = lister
38 | .list_bootc_domains()
39 | .with_context(|| "Failed to list bootc domains from libvirt")?;
40 |
41 | // Filter by label if specified
42 | if let Some(ref filter_label) = opts.label {
43 | domains.retain(|d| d.labels.contains(filter_label));
44 | }
45 |
46 | if domains.is_empty() {
47 | if let Some(ref label) = opts.label {
48 | println!("No VMs found with label '{}'", label);
49 | } else {
50 | println!("No VMs found");
51 | }
52 | return Ok(());
53 | }
54 |
55 | // Confirmation prompt
56 | if !opts.force {
57 | println!(
58 | "This will permanently delete {} VM{} and their data:",
59 | domains.len(),
60 | if domains.len() == 1 { "" } else { "s" }
61 | );
62 | for domain in &domains {
63 | println!(" - {} ({})", domain.name, domain.status_string());
64 | if let Some(ref image) = domain.image {
65 | println!(" Image: {}", image);
66 | }
67 | if let Some(ref disk_path) = domain.disk_path {
68 | println!(" Disk: {}", disk_path);
69 | }
70 | if !domain.labels.is_empty() {
71 | println!(" Labels: {}", domain.labels.join(", "));
72 | }
73 | }
74 | println!();
75 | println!("Are you sure? This cannot be undone. Use --force to skip this prompt.");
76 | return Ok(());
77 | }
78 |
79 | let mut removed_count = 0;
80 | let mut error_count = 0;
81 |
82 | for domain in &domains {
83 | println!("Removing VM '{}'...", domain.name);
84 |
85 | // Stop if running
86 | if domain.is_running() {
87 | if opts.stop {
88 | println!(" Stopping running VM...");
89 | let output = global_opts
90 | .virsh_command()
91 | .args(&["destroy", &domain.name])
92 | .output()
93 | .with_context(|| format!("Failed to stop VM '{}'", domain.name))?;
94 |
95 | if !output.status.success() {
96 | let stderr = String::from_utf8_lossy(&output.stderr);
97 | eprintln!(" Failed to stop VM '{}': {}", domain.name, stderr);
98 | error_count += 1;
99 | continue;
100 | }
101 | } else {
102 | eprintln!(
103 | " Skipping '{}': VM is running. Use --stop to force removal.",
104 | domain.name
105 | );
106 | error_count += 1;
107 | continue;
108 | }
109 | }
110 |
111 | // Remove disk manually if it exists (unmanaged storage)
112 | if let Some(ref disk_path) = domain.disk_path {
113 | if std::path::Path::new(disk_path).exists() {
114 | println!(" Removing disk image...");
115 | if let Err(e) = std::fs::remove_file(disk_path) {
116 | eprintln!(
117 | " Warning: Failed to remove disk file '{}': {}",
118 | disk_path, e
119 | );
120 | // Continue anyway - libvirt may still have the domain
121 | }
122 | }
123 | }
124 |
125 | // Remove libvirt domain with nvram
126 | println!(" Removing libvirt domain...");
127 | let output = global_opts
128 | .virsh_command()
129 | .args(&["undefine", &domain.name, "--nvram"])
130 | .output()
131 | .with_context(|| format!("Failed to undefine domain '{}'", domain.name))?;
132 |
133 | if output.status.success() {
134 | println!(" VM '{}' removed successfully", domain.name);
135 | removed_count += 1;
136 | } else {
137 | let stderr = String::from_utf8_lossy(&output.stderr);
138 | eprintln!(
139 | " Failed to remove libvirt domain '{}': {}",
140 | domain.name, stderr
141 | );
142 | error_count += 1;
143 | }
144 | }
145 |
146 | println!();
147 | println!(
148 | "Summary: {} VM{} removed, {} error{}",
149 | removed_count,
150 | if removed_count == 1 { "" } else { "s" },
151 | error_count,
152 | if error_count == 1 { "" } else { "s" }
153 | );
154 |
155 | if error_count > 0 {
156 | Err(color_eyre::eyre::eyre!(
157 | "Failed to remove {} VM{}",
158 | error_count,
159 | if error_count == 1 { "" } else { "s" }
160 | ))
161 | } else {
162 | Ok(())
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/crates/integration-tests/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Shared library code for integration tests
2 | //!
3 | //! This module contains constants and utilities that are shared between
4 | //! the main test binary and helper binaries like cleanup.
5 |
6 | // Unfortunately needed here to work with linkme
7 | #![allow(unsafe_code)]
8 |
9 | /// Label used to identify containers created by integration tests
10 | pub const INTEGRATION_TEST_LABEL: &str = "bcvk.integration-test=1";
11 |
12 | /// Label used to identify libvirt VMs created by integration tests
13 | pub const LIBVIRT_INTEGRATION_TEST_LABEL: &str = "bcvk-integration";
14 |
15 | /// A test function that returns a Result
16 | pub type TestFn = fn() -> color_eyre::Result<()>;
17 |
18 | /// A parameterized test function that takes an image parameter
19 | pub type ParameterizedTestFn = fn(&str) -> color_eyre::Result<()>;
20 |
21 | /// Metadata for a registered integration test
22 | #[derive(Debug)]
23 | pub struct IntegrationTest {
24 | /// Name of the integration test
25 | pub name: &'static str,
26 | /// Test function to execute
27 | pub f: TestFn,
28 | }
29 |
30 | impl IntegrationTest {
31 | /// Create a new integration test with the given name and function
32 | pub const fn new(name: &'static str, f: TestFn) -> Self {
33 | Self { name, f }
34 | }
35 | }
36 |
37 | /// Metadata for a parameterized integration test that runs once per image
38 | #[derive(Debug)]
39 | pub struct ParameterizedIntegrationTest {
40 | /// Base name of the integration test (will be suffixed with image identifier)
41 | pub name: &'static str,
42 | /// Parameterized test function to execute
43 | pub f: ParameterizedTestFn,
44 | }
45 |
46 | impl ParameterizedIntegrationTest {
47 | /// Create a new parameterized integration test with the given name and function
48 | pub const fn new(name: &'static str, f: ParameterizedTestFn) -> Self {
49 | Self { name, f }
50 | }
51 | }
52 |
53 | /// Distributed slice holding all registered integration tests
54 | #[linkme::distributed_slice]
55 | pub static INTEGRATION_TESTS: [IntegrationTest];
56 |
57 | /// Distributed slice holding all registered parameterized integration tests
58 | #[linkme::distributed_slice]
59 | pub static PARAMETERIZED_INTEGRATION_TESTS: [ParameterizedIntegrationTest];
60 |
61 | /// Register an integration test with less boilerplate.
62 | ///
63 | /// This macro generates the static registration for an integration test function.
64 | ///
65 | /// # Examples
66 | ///
67 | /// ```ignore
68 | /// fn test_basic_functionality() -> Result<()> {
69 | /// let output = run_bcvk(&["some", "args"])?;
70 | /// output.assert_success("test");
71 | /// Ok(())
72 | /// }
73 | /// integration_test!(test_basic_functionality);
74 | /// ```
75 | #[macro_export]
76 | macro_rules! integration_test {
77 | ($fn_name:ident) => {
78 | ::paste::paste! {
79 | #[::linkme::distributed_slice($crate::INTEGRATION_TESTS)]
80 | static [<$fn_name:upper>]: $crate::IntegrationTest =
81 | $crate::IntegrationTest::new(stringify!($fn_name), $fn_name);
82 | }
83 | };
84 | }
85 |
86 | /// Register a parameterized integration test with less boilerplate.
87 | ///
88 | /// This macro generates the static registration for a parameterized integration test function.
89 | ///
90 | /// # Examples
91 | ///
92 | /// ```ignore
93 | /// fn test_with_image(image: &str) -> Result<()> {
94 | /// let output = run_bcvk(&["command", image])?;
95 | /// output.assert_success("test");
96 | /// Ok(())
97 | /// }
98 | /// parameterized_integration_test!(test_with_image);
99 | /// ```
100 | #[macro_export]
101 | macro_rules! parameterized_integration_test {
102 | ($fn_name:ident) => {
103 | ::paste::paste! {
104 | #[::linkme::distributed_slice($crate::PARAMETERIZED_INTEGRATION_TESTS)]
105 | static [<$fn_name:upper>]: $crate::ParameterizedIntegrationTest =
106 | $crate::ParameterizedIntegrationTest::new(stringify!($fn_name), $fn_name);
107 | }
108 | };
109 | }
110 |
111 | /// Create a test suffix from an image name by replacing invalid characters with underscores
112 | ///
113 | /// Replaces all non-alphanumeric characters with `_` to create a predictable, filesystem-safe
114 | /// test name suffix.
115 | ///
116 | /// Examples:
117 | /// - "quay.io/fedora/fedora-bootc:42" -> "quay_io_fedora_fedora_bootc_42"
118 | /// - "quay.io/centos-bootc/centos-bootc:stream10" -> "quay_io_centos_bootc_centos_bootc_stream10"
119 | /// - "quay.io/image@sha256:abc123" -> "quay_io_image_sha256_abc123"
120 | pub fn image_to_test_suffix(image: &str) -> String {
121 | image.replace(|c: char| !c.is_alphanumeric(), "_")
122 | }
123 |
124 | #[cfg(test)]
125 | mod tests {
126 | use super::*;
127 |
128 | #[test]
129 | fn test_image_to_test_suffix_basic() {
130 | assert_eq!(
131 | image_to_test_suffix("quay.io/fedora/fedora-bootc:42"),
132 | "quay_io_fedora_fedora_bootc_42"
133 | );
134 | }
135 |
136 | #[test]
137 | fn test_image_to_test_suffix_stream() {
138 | assert_eq!(
139 | image_to_test_suffix("quay.io/centos-bootc/centos-bootc:stream10"),
140 | "quay_io_centos_bootc_centos_bootc_stream10"
141 | );
142 | }
143 |
144 | #[test]
145 | fn test_image_to_test_suffix_digest() {
146 | assert_eq!(
147 | image_to_test_suffix("quay.io/image@sha256:abc123"),
148 | "quay_io_image_sha256_abc123"
149 | );
150 | }
151 |
152 | #[test]
153 | fn test_image_to_test_suffix_complex() {
154 | assert_eq!(
155 | image_to_test_suffix("registry.example.com:5000/my-org/my-image:v1.2.3"),
156 | "registry_example_com_5000_my_org_my_image_v1_2_3"
157 | );
158 | }
159 |
160 | #[test]
161 | fn test_image_to_test_suffix_only_alphanumeric() {
162 | assert_eq!(image_to_test_suffix("simpleimage"), "simpleimage");
163 | }
164 |
165 | #[test]
166 | fn test_image_to_test_suffix_special_chars() {
167 | assert_eq!(
168 | image_to_test_suffix("image/with@special:chars-here.now"),
169 | "image_with_special_chars_here_now"
170 | );
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/docs/src/vs-bootc.md:
--------------------------------------------------------------------------------
1 | # bcvk vs Raw bootc
2 |
3 | While bootc is the foundational tool for building and managing bootable containers, bcvk provides a higher-level abstraction focused on virtualization workflows. Understanding when to use each approach is essential for effective bootable container development.
4 |
5 | ## Fundamental Differences
6 |
7 | ### Scope and Purpose
8 | - **bootc**: Core functionality for building, installing, and managing bootable container systems
9 | - **bcvk**: Specialized toolkit that leverages bootc for virtualization-focused workflows
10 |
11 | ### Abstraction Level
12 | - **bootc**: Low-level control over bootable container lifecycle and installation
13 | - **bcvk**: High-level workflows that abstract away virtualization complexity
14 |
15 | ### Target Users
16 | - **bootc**: System administrators, platform engineers, and advanced users who need direct control
17 | - **bcvk**: Developers and teams who want streamlined virtualization workflows
18 |
19 | ## Workflow Comparison
20 |
21 | ### Image Creation and Testing
22 |
23 | ```mermaid
24 | flowchart TD
25 | A[Container Image] --> B{Approach}
26 | B -->|Raw bootc| C[Manual bootc install]
27 | C --> D[Manual VM Setup]
28 | D --> E[Manual Testing]
29 |
30 | B -->|bcvk| F[Automated VM Creation]
31 | F --> G[Integrated Testing]
32 | G --> H[Streamlined Workflow]
33 | ```
34 |
35 | ### Installation Workflows
36 | - **Raw bootc**: Direct control over installation targets, filesystem layouts, and boot configuration
37 | - **bcvk**: Automated installation into VM environments with sensible defaults
38 |
39 | ### Testing and Development
40 | - **Raw bootc**: Manual VM creation and management for testing changes
41 | - **bcvk**: Automated VM lifecycle management with integrated SSH access
42 |
43 | ## When to Use Raw bootc
44 |
45 | ### Production Deployments
46 | Raw bootc is essential for production deployments where you need:
47 | - **Direct hardware installation** onto bare metal servers
48 | - **Custom partition layouts** and filesystem configurations
49 | - **Integration with existing infrastructure** management tools
50 | - **Precise control** over boot processes and system configuration
51 |
52 | ### Platform Development
53 | When building bootable container platforms:
54 | - **Custom bootloader configurations** for specific hardware
55 | - **Specialized installation targets** beyond standard VM formats
56 | - **Integration with hardware provisioning** systems
57 | - **Development of bootc extensions** and plugins
58 |
59 | ### Edge Computing
60 | For edge deployments requiring:
61 | - **Minimal resource overhead** without virtualization layers
62 | - **Direct hardware access** for performance-critical applications
63 | - **Custom update mechanisms** integrated with edge management platforms
64 | - **Specialized boot configurations** for embedded systems
65 |
66 | ## When to Use bcvk
67 |
68 | ### Development Workflows
69 | bcvk excels in development scenarios:
70 | - **Rapid iteration** on bootable container changes
71 | - **Automated testing** of container modifications
72 | - **Consistent development environments** across team members
73 | - **Integration testing** without manual VM management
74 |
75 | ### CI/CD Pipelines
76 | For automated testing and validation:
77 | - **Automated VM provisioning** for test environments
78 | - **Parallel testing** across multiple configurations
79 | - **Integration with container registries** and build systems
80 | - **Standardized testing workflows** that can be shared across projects
81 |
82 | ### Prototyping and Experimentation
83 | When exploring bootable container concepts:
84 | - **Quick experimentation** with different container configurations
85 | - **Learning bootable containers** without complex virtualization setup
86 | - **Proof-of-concept development** before production implementation
87 | - **Educational environments** for teaching bootable container concepts
88 |
89 | ## Complementary Usage Patterns
90 |
91 | ### Development to Production Pipeline
92 | Many organizations use both tools in sequence:
93 |
94 | 1. **Development Phase**: Use bcvk for rapid iteration and testing
95 | 2. **Validation Phase**: Use bcvk for automated testing and CI/CD
96 | 3. **Production Phase**: Use raw bootc for deployment to target infrastructure
97 |
98 | ### Hybrid Workflows
99 | Teams often maintain parallel workflows:
100 | - **bcvk**: For development team velocity and testing automation
101 | - **Raw bootc**: For platform engineering and production operations
102 |
103 | ## Technical Capabilities
104 |
105 | ### Installation Targets
106 | - **Raw bootc**: Any supported target (disk images, physical devices, cloud instances)
107 | - **bcvk**: Focused on VM targets with optimized workflows
108 |
109 | ### Configuration Flexibility
110 | - **Raw bootc**: Complete control over all aspects of installation and configuration
111 | - **bcvk**: Streamlined configuration focused on development and testing needs
112 |
113 | ### Integration Points
114 | - **Raw bootc**: Direct integration with system management tools and infrastructure
115 | - **bcvk**: Integration with development tools, CI/CD systems, and virtualization platforms
116 |
117 | ## Migration and Transition
118 |
119 | ### From bcvk to Production
120 | Moving from development with bcvk to production with raw bootc:
121 | - **Configuration translation** from VM-optimized to production settings
122 | - **Testing validation** ensuring parity between environments
123 | - **Deployment automation** adapting bcvk workflows to production tooling
124 |
125 | ### Learning Path
126 | For teams new to bootable containers:
127 | 1. **Start with bcvk** for rapid learning and experimentation
128 | 2. **Understand underlying bootc** concepts through bcvk usage
129 | 3. **Transition to raw bootc** as production requirements become clear
130 |
131 | ## Decision Criteria
132 |
133 | Choose **raw bootc** when:
134 | - Deploying to production infrastructure
135 | - Need precise control over installation and configuration
136 | - Working with specialized hardware or edge devices
137 | - Building platform-level tooling
138 |
139 | Choose **bcvk** when:
140 | - Developing and testing bootable containers
141 | - Need rapid iteration and automation
142 | - Working in virtualized development environments
143 | - Building CI/CD workflows for bootable containers
144 |
145 | The two tools are designed to work together, with bcvk providing development velocity while raw bootc enables production deployment capabilities.
--------------------------------------------------------------------------------
/crates/integration-tests/src/tests/mount_feature.rs:
--------------------------------------------------------------------------------
1 | //! Integration tests for mount features
2 | //!
3 | //! ⚠️ **CRITICAL INTEGRATION TEST POLICY** ⚠️
4 | //!
5 | //! INTEGRATION TESTS MUST NEVER "warn and continue" ON FAILURES!
6 | //!
7 | //! If something is not working:
8 | //! - Use `todo!("reason why this doesn't work yet")`
9 | //! - Use `panic!("clear error message")`
10 | //! - Use `assert!()` and `unwrap()` to fail hard
11 | //!
12 | //! NEVER use patterns like:
13 | //! - "Note: test failed - likely due to..."
14 | //! - "This is acceptable in CI/testing environments"
15 | //! - Warning and continuing on failures
16 |
17 | use camino::Utf8Path;
18 | use color_eyre::Result;
19 | use integration_tests::integration_test;
20 |
21 | use std::fs;
22 | use tempfile::TempDir;
23 |
24 | use crate::{get_test_image, run_bcvk, INTEGRATION_TEST_LABEL};
25 |
26 | /// Create a systemd unit that verifies a mount exists and tests writability
27 | fn create_mount_verify_unit(
28 | unit_dir: &Utf8Path,
29 | mount_name: &str,
30 | expected_file: &str,
31 | expected_content: Option<&str>,
32 | readonly: bool,
33 | ) -> std::io::Result<()> {
34 | let (description, content_check, write_check, unit_prefix) = if readonly {
35 | (
36 | format!("Verify read-only mount {mount_name} and poweroff"),
37 | format!("ExecStart=test -f /run/virtiofs-mnt-{mount_name}/{expected_file}"),
38 | format!("ExecStart=/bin/sh -c '! echo test-write > /run/virtiofs-mnt-{mount_name}/write-test.txt 2>/dev/null'"),
39 | "verify-ro-mount",
40 | )
41 | } else {
42 | let content = expected_content.expect("expected_content required for writable mounts");
43 | (
44 | format!("Verify mount {mount_name} and poweroff"),
45 | format!("ExecStart=grep -qF \"{content}\" /run/virtiofs-mnt-{mount_name}/{expected_file}"),
46 | format!("ExecStart=/bin/sh -c 'echo test-write > /run/virtiofs-mnt-{mount_name}/write-test.txt'"),
47 | "verify-mount",
48 | )
49 | };
50 |
51 | let unit_content = format!(
52 | r#"[Unit]
53 | Description={description}
54 | RequiresMountsFor=/run/virtiofs-mnt-{mount_name}
55 |
56 | [Service]
57 | Type=oneshot
58 | {content_check}
59 | {write_check}
60 | ExecStart=echo ok mount verify {mount_name}
61 | ExecStart=systemctl poweroff
62 | StandardOutput=journal+console
63 | StandardError=journal+console
64 | "#
65 | );
66 |
67 | let unit_path = unit_dir.join(format!("{unit_prefix}-{mount_name}.service"));
68 | fs::write(&unit_path, unit_content)?;
69 | Ok(())
70 | }
71 |
72 | fn test_mount_feature_bind() -> Result<()> {
73 | // Create a temporary directory to test bind mounting
74 | let temp_dir = TempDir::new().expect("Failed to create temp directory");
75 | let temp_dir_path = Utf8Path::from_path(temp_dir.path()).expect("temp dir path is not utf8");
76 | let test_file_path = temp_dir_path.join("test.txt");
77 | let test_content = "Test content for bind mount";
78 | fs::write(&test_file_path, test_content).expect("Failed to write test file");
79 |
80 | // Create systemd units directory
81 | let units_dir = TempDir::new().expect("Failed to create units directory");
82 | let units_dir_path = Utf8Path::from_path(units_dir.path()).expect("units dir path is not utf8");
83 | let system_dir = units_dir_path.join("system");
84 | fs::create_dir(&system_dir).expect("Failed to create system directory");
85 |
86 | // Create verification unit
87 | create_mount_verify_unit(
88 | &system_dir,
89 | "testmount",
90 | "test.txt",
91 | Some(test_content),
92 | false,
93 | )
94 | .expect("Failed to create verify unit");
95 |
96 | println!("Testing bind mount with temp directory: {}", temp_dir_path);
97 |
98 | // Run with bind mount and verification unit
99 | let output = run_bcvk(&[
100 | "ephemeral",
101 | "run",
102 | "--rm",
103 | "--label",
104 | INTEGRATION_TEST_LABEL,
105 | "--console",
106 | "-K",
107 | "--bind",
108 | &format!("{}:testmount", temp_dir_path),
109 | "--systemd-units",
110 | units_dir_path.as_str(),
111 | "--karg",
112 | "systemd.unit=verify-mount-testmount.service",
113 | "--karg",
114 | "systemd.journald.forward_to_console=1",
115 | &get_test_image(),
116 | ])?;
117 |
118 | assert!(output.stdout.contains("ok mount verify"));
119 |
120 | println!("Successfully tested and verified bind mount feature");
121 | Ok(())
122 | }
123 | integration_test!(test_mount_feature_bind);
124 |
125 | fn test_mount_feature_ro_bind() -> Result<()> {
126 | // Create a temporary directory to test read-only bind mounting
127 | let temp_dir = TempDir::new().expect("Failed to create temp directory");
128 | let temp_dir_path = Utf8Path::from_path(temp_dir.path()).expect("temp dir path is not utf8");
129 | let test_file_path = temp_dir_path.join("readonly.txt");
130 | fs::write(&test_file_path, "Read-only content").expect("Failed to write test file");
131 |
132 | // Create systemd units directory
133 | let units_dir = TempDir::new().expect("Failed to create units directory");
134 | let units_dir_path = Utf8Path::from_path(units_dir.path()).expect("units dir path is not utf8");
135 | let system_dir = units_dir_path.join("system");
136 | fs::create_dir(&system_dir).expect("Failed to create system directory");
137 |
138 | // Create verification unit for read-only mount
139 | create_mount_verify_unit(&system_dir, "romount", "readonly.txt", None, true)
140 | .expect("Failed to create verify unit");
141 |
142 | println!(
143 | "Testing read-only bind mount with temp directory: {}",
144 | temp_dir_path
145 | );
146 |
147 | // Run with read-only bind mount and verification unit
148 | let output = run_bcvk(&[
149 | "ephemeral",
150 | "run",
151 | "--rm",
152 | "--label",
153 | INTEGRATION_TEST_LABEL,
154 | "--console",
155 | "-K",
156 | "--ro-bind",
157 | &format!("{}:romount", temp_dir_path),
158 | "--systemd-units",
159 | units_dir_path.as_str(),
160 | "--karg",
161 | "systemd.unit=verify-ro-mount-romount.service",
162 | "--karg",
163 | "systemd.journald.forward_to_console=1",
164 | &get_test_image(),
165 | ])?;
166 |
167 | assert!(output.stdout.contains("ok mount verify"));
168 | Ok(())
169 | }
170 | integration_test!(test_mount_feature_ro_bind);
171 |
--------------------------------------------------------------------------------
/crates/kit/src/libvirt/rm.rs:
--------------------------------------------------------------------------------
1 | //! libvirt rm command - remove a bootc domain and its resources
2 | //!
3 | //! This module provides functionality to permanently remove libvirt domains
4 | //! and their associated disk images that were created from bootc container images.
5 |
6 | use clap::Parser;
7 | use color_eyre::Result;
8 |
9 | /// Options for removing a libvirt domain
10 | #[derive(Debug, Parser)]
11 | pub struct LibvirtRmOpts {
12 | /// Name of the domain to remove
13 | pub name: String,
14 |
15 | /// Force removal without confirmation (also stops running VMs)
16 | #[clap(long, short = 'f')]
17 | pub force: bool,
18 |
19 | /// Stop domain if it's running (implied by --force)
20 | #[clap(long)]
21 | pub stop: bool,
22 | }
23 |
24 | /// Core removal implementation that accepts pre-fetched domain state and info
25 | ///
26 | /// This private function performs the actual removal logic without fetching
27 | /// domain information, allowing callers to optimize by reusing already-fetched data.
28 | fn remove_vm_impl(
29 | global_opts: &crate::libvirt::LibvirtOptions,
30 | vm_name: &str,
31 | state: &str,
32 | domain_info: &crate::domain_list::PodmanBootcDomain,
33 | stop_if_running: bool,
34 | ) -> Result<()> {
35 | use color_eyre::eyre::Context;
36 |
37 | // Check if VM is running
38 | if state == "running" {
39 | if stop_if_running {
40 | let output = global_opts
41 | .virsh_command()
42 | .args(&["destroy", vm_name])
43 | .output()
44 | .with_context(|| "Failed to stop VM before removal")?;
45 |
46 | if !output.status.success() {
47 | let stderr = String::from_utf8_lossy(&output.stderr);
48 | return Err(color_eyre::eyre::eyre!(
49 | "Failed to stop VM '{}' before removal: {}",
50 | vm_name,
51 | stderr
52 | ));
53 | }
54 | } else {
55 | return Err(color_eyre::eyre::eyre!(
56 | "VM '{}' is running. Cannot remove without stopping.",
57 | vm_name
58 | ));
59 | }
60 | }
61 |
62 | // Remove disk manually if it exists (unmanaged storage)
63 | if let Some(ref disk_path) = domain_info.disk_path {
64 | if std::path::Path::new(disk_path).exists() {
65 | std::fs::remove_file(disk_path)
66 | .with_context(|| format!("Failed to remove disk file: {}", disk_path))?;
67 | }
68 | }
69 |
70 | // Remove libvirt domain with nvram and storage
71 | let output = global_opts
72 | .virsh_command()
73 | .args(&["undefine", vm_name, "--nvram", "--remove-all-storage"])
74 | .output()
75 | .with_context(|| "Failed to undefine libvirt domain")?;
76 |
77 | if !output.status.success() {
78 | let stderr = String::from_utf8_lossy(&output.stderr);
79 | return Err(color_eyre::eyre::eyre!(
80 | "Failed to remove libvirt domain: {}",
81 | stderr
82 | ));
83 | }
84 |
85 | Ok(())
86 | }
87 |
88 | /// Remove a VM without confirmation
89 | ///
90 | /// This is the core removal logic that can be reused by other commands.
91 | /// It assumes the caller has already confirmed the operation.
92 | pub fn remove_vm_forced(
93 | global_opts: &crate::libvirt::LibvirtOptions,
94 | vm_name: &str,
95 | stop_if_running: bool,
96 | ) -> Result<()> {
97 | use crate::domain_list::DomainLister;
98 | use color_eyre::eyre::Context;
99 |
100 | let connect_uri = global_opts.connect.as_ref();
101 | let lister = match connect_uri {
102 | Some(uri) => DomainLister::with_connection(uri.clone()),
103 | None => DomainLister::new(),
104 | };
105 |
106 | // Check if domain exists and get its state
107 | let state = lister
108 | .get_domain_state(vm_name)
109 | .map_err(|_| color_eyre::eyre::eyre!("VM '{}' not found", vm_name))?;
110 |
111 | // Get domain info for disk cleanup
112 | let domain_info = lister
113 | .get_domain_info(vm_name)
114 | .with_context(|| format!("Failed to get info for VM '{}'", vm_name))?;
115 |
116 | remove_vm_impl(global_opts, vm_name, &state, &domain_info, stop_if_running)
117 | }
118 |
119 | /// Execute the libvirt rm command
120 | pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtRmOpts) -> Result<()> {
121 | use crate::domain_list::DomainLister;
122 | use color_eyre::eyre::Context;
123 |
124 | let connect_uri = global_opts.connect.as_ref();
125 | let lister = match connect_uri {
126 | Some(uri) => DomainLister::with_connection(uri.clone()),
127 | None => DomainLister::new(),
128 | };
129 |
130 | // Check if domain exists and get its state
131 | let state = lister
132 | .get_domain_state(&opts.name)
133 | .map_err(|_| color_eyre::eyre::eyre!("VM '{}' not found", opts.name))?;
134 |
135 | // Get domain info for display
136 | let domain_info = lister
137 | .get_domain_info(&opts.name)
138 | .with_context(|| format!("Failed to get info for VM '{}'", opts.name))?;
139 |
140 | // Check if VM is running
141 | if state == "running" {
142 | // --force implies --stop
143 | if opts.stop || opts.force {
144 | println!("Stopping running VM '{}'...", opts.name);
145 | } else {
146 | return Err(color_eyre::eyre::eyre!(
147 | "VM '{}' is running. Use --stop or --force to remove a running VM, or stop it first.",
148 | opts.name
149 | ));
150 | }
151 | }
152 |
153 | // Confirmation prompt
154 | if !opts.force {
155 | println!(
156 | "This will permanently delete VM '{}' and its data:",
157 | opts.name
158 | );
159 | if let Some(ref image) = domain_info.image {
160 | println!(" Image: {}", image);
161 | }
162 | if let Some(ref disk_path) = domain_info.disk_path {
163 | println!(" Disk: {}", disk_path);
164 | }
165 | println!(" Status: {}", domain_info.status_string());
166 | println!();
167 | println!("Are you sure? This cannot be undone. Use --force to skip this prompt.");
168 | return Ok(());
169 | }
170 |
171 | println!("Removing VM '{}'...", opts.name);
172 |
173 | // Use the optimized removal implementation with already-fetched info
174 | remove_vm_impl(
175 | global_opts,
176 | &opts.name,
177 | &state,
178 | &domain_info,
179 | opts.stop || opts.force,
180 | )?;
181 |
182 | println!("VM '{}' removed successfully", opts.name);
183 | Ok(())
184 | }
185 |
--------------------------------------------------------------------------------