Qlean
Qlean is a system-level isolation testing library built on QEMU/KVM. It spins up lightweight VMs in your Rust tests so privileged or risky operations stay off the host.
Overview
Qlean targets two common needs in system-level testing:
1. Complete Resource Isolation
Some tests need root privileges or direct access to kernel interfaces. Running them on the host can leave the machine in a bad state when a test fails. Qlean runs each test in its own VM so failures stay contained and the host stays stable.
2. Convenient Distributed Testing
For distributed or multi-node scenarios, Qlean lets you create and coordinate several VMs from test code—no separate cluster setup or orchestration layer required.
Key Features
- 🔒 Complete Isolation: Based on QEMU/KVM, providing full virtual machine isolation
- 🔄 Distributed Testing: Easily create and manage multiple virtual machines
- 🛡️ RAII-style Interface: Automatic resource management ensures VMs are properly cleaned up
- 📦 Out-of-the-Box: Automated image downloading with verification, no manual configuration needed
- 🐧 Linux Native: Native support for Linux hosts with multiple guest distributions and architectures
Usage
Host Setup
Install CLI tools
Install and configure QEMU, libvirt, and xorriso on your Linux host before using Qlean. On Debian or Ubuntu, see the setup guide for step-by-step instructions.
Configure qemu-bridge-helper
Qlean uses qemu-bridge-helper to manage networking for multiple virtual machines, so it requires proper configuration.
Grant CAP_NET_ADMIN to the default network helper:
qemu-bridge-helper denies all bridges by default, so you must allow the qlbr0 bridge that Qlean creates:
Getting Started
Add the dependency to your Cargo.toml:
[]
= "0.3"
= { = "1", = ["full"] }
= "0.3"
= { = "0.3", = ["env-filter", "local-time"] }
Qlean uses tracing and indicatif for structured logs and progress bars (for example while downloading images). To see that output in your own tests, add tracing-indicatif and tracing-subscriber as above and install a global subscriber once per process. A helper guarded with std::sync::Once works well when many tests share the same setup:
use Once;
use IndicatifLayer;
use ;
static INIT: Once = new;
Call init_tracing() at the start of each test (or from a shared test harness). Adjust verbosity with RUST_LOG, for example RUST_LOG=debug,qlean=trace.
Basic Example
A minimal single-VM test:
use Result;
use ;
async
A distributed test with two VMs on the same virtual network:
use Result;
use ;
async
More examples live in the tests directory.
Network Configuration
Qlean uses a dedicated libvirt virtual network for isolated, reproducible connectivity between test VMs. The default definition is written to ~/.local/share/qlean/network.xml:
qlean
This defines a NAT network named qlean in libvirt, backed by the Linux bridge qlbr0 at 192.168.221.1. DHCP hands out addresses in 192.168.221.2–192.168.221.254 on the 192.168.221.0/24 subnet so VMs can reach each other, the host, and the outside world through NAT.
[!NOTE] If
192.168.221.0/24conflicts with your LAN, change the IP range in that file, but leave<name>qlean</name>and<bridge name='qlbr0'/>as they are—Qlean expects those identifiers.
API Reference
Top-Level Interface
-
is_kvm_available()- Check if KVM is available on the host. -
with_machine(image, config, f)— Run an async closure with one VM; initializes on entry and shuts down on exit. -
with_pool(f)— Run an async closure with aMachinePool; shuts down all pool members on exit. -
ImageConfig- Configuration for a virtual machine image. -
MachineConfig- Configuration for a virtual machine.
Image Interface
Image::new(config)- Create a new image with specified configuration.
Machine Core Interface
Machine::new(image, config)- Create a new machine instance.Machine::init()- Initialize the machine (first boot with cloud-init).Machine::spawn()- Start the machine (normal boot).Machine::exec(command)- Execute a command in the VM and return the output.Machine::shutdown()- Gracefully shutdown the virtual machine.Machine::upload(src, dst)- Upload a file or directory to the VM.Machine::download(src, dst)- Download a file or directory from the VM.Machine::get_ip()- Get the IP address of the VM.Machine::is_running()- Check if the VM is currently running.
Machine Pool Interface
MachinePool::new()- Create a new, empty machine pool.MachinePool::add(name, image, config)- Add a new machine instance to the pool.MachinePool::get(name)- Get a machine instance by the name.MachinePool::init_all()- Initialize all machines in the pool concurrently.MachinePool::spawn_all()- Spawn all machines in the pool concurrently.MachinePool::shutdown_all()- Shutdown all machines in the pool concurrently.
std::fs Compatible Interface
The following methods provide filesystem operations compatible with std::fs semantics:
Machine::copy(from, to)- Copy a file within the VM.Machine::create_dir(path)- Create a directory.Machine::create_dir_all(path)- Create a directory and all missing parent directories.Machine::exists(path)- Check if a path exists.Machine::hard_link(src, dst)- Create a hard link.Machine::metadata(path)- Get file/directory metadata.Machine::read(path)- Read file contents as bytes.Machine::read_dir(path)- Read directory entries.Machine::read_link(path)- Read symbolic link target.Machine::read_to_string(path)- Read file contents as string.Machine::remove_dir_all(path)- Remove a directory after removing all its contents.Machine::remove_file(path)- Remove a file.Machine::rename(from, to)- Rename or move a file/directory.Machine::set_permissions(path, perm)- Set file/directory permissions.Machine::write(path, contents)- Write bytes to a file.
License
This project is licensed under the MIT license.