├── .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 | --------------------------------------------------------------------------------