Files
harmony/examples/kvm_vm_examples/src/main.rs
Jean-Gabriel Gill-Couture 50f62b6437 chore: warning sweep — auto-fix pass + scoped allows for generated code
Workspace warning count: 408 → 105.

Three buckets cleared:

* Auto-fixable (`cargo fix` + `cargo clippy --fix`): unused imports
  removed, unused variables prefixed with `_`, deprecated method
  calls updated. Applied across harmony, harmony-k8s, harmony-agent,
  harmony_inventory_agent, the fleet/ workspace, and ~15 examples.
* Generated code (opnsense-api/src/generated/): 269 snake_case
  warnings + ~10 unreachable-pattern warnings come from
  CamelCase-preserving bindings to OPNsense's HAProxy/Caddy XML
  schemas. Scoped a single `#[allow(non_snake_case,
  unreachable_patterns)]` at `pub mod generated;` rather than
  fighting the codegen — renaming would break serde round-trips
  and the codegen would regenerate them anyway.
* opnsense-codegen parser's defensive `let...else` guards on
  `XmlNode` (currently single-variant): file-level
  `#![allow(irrefutable_let_patterns)]` with a comment explaining
  why we keep the `else` arms (they re-arm if the IR grows a
  second variant).

`harmony_inventory_agent::local_presence::{DiscoveryEvent,
discover_agents}` re-exports were stripped twice by the auto-fix
passes (consumers live in another crate, so the local crate looks
"unused" to lint). Anchored with explicit `pub use` + an
`#[allow(unused_imports)]` annotation noting why.

All 151 harmony lib tests still pass. Remaining ~105 warnings are
mostly real dead code in non-fleet modules + a handful of
unused-imports/variables clippy couldn't auto-resolve; cleared in
the next pass.
2026-05-06 22:51:44 -04:00

359 lines
12 KiB
Rust

//! KVM VM examples demonstrating various configurations.
//!
//! Each subcommand creates a different VM setup. All VMs are managed
//! via libvirt — you need a working KVM hypervisor on the host.
//!
//! # Prerequisites
//!
//! ```bash
//! # Manjaro / Arch
//! sudo pacman -S qemu-full libvirt virt-install dnsmasq ebtables
//! sudo systemctl enable --now libvirtd
//! sudo usermod -aG libvirt $USER
//! ```
//!
//! # Environment variables
//!
//! - `HARMONY_KVM_URI`: libvirt URI (default: `qemu:///system`)
//! - `HARMONY_KVM_IMAGE_DIR`: disk image directory (default: `~/.local/share/harmony/kvm/images`)
//!
//! # Usage
//!
//! ```bash
//! # Simple Alpine VM (tiny, boots in seconds — great for testing)
//! cargo run -p kvm-vm-examples -- alpine
//!
//! # Ubuntu Server with cloud-init
//! cargo run -p kvm-vm-examples -- ubuntu
//!
//! # Multi-disk worker node (Ceph OSD style)
//! cargo run -p kvm-vm-examples -- worker
//!
//! # Multi-NIC gateway (OPNsense style: WAN + LAN)
//! cargo run -p kvm-vm-examples -- gateway
//!
//! # Full HA cluster: 1 gateway + 3 control plane + 3 workers
//! cargo run -p kvm-vm-examples -- ha-cluster
//!
//! # Clean up all VMs and networks from a scenario
//! cargo run -p kvm-vm-examples -- clean <scenario>
//! ```
use clap::{Parser, Subcommand};
use harmony::modules::kvm::config::init_executor;
use harmony::modules::kvm::{
BootDevice, ForwardMode, KvmExecutor, NetworkConfig, NetworkRef, VmConfig,
};
use log::info;
#[derive(Parser)]
#[command(name = "kvm-vm-examples")]
#[command(about = "KVM VM examples for various infrastructure setups")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Minimal Alpine Linux VM — fast boot, ~150MB ISO
Alpine,
/// Ubuntu Server 24.04 — standard server with 1 disk
Ubuntu,
/// Worker node with multiple disks (OS + Ceph OSD storage)
Worker,
/// Gateway/firewall with 2 NICs (WAN + LAN)
Gateway,
/// Full HA cluster: gateway + 3 control plane + 3 worker nodes
HaCluster,
/// Tear down all VMs and networks for a scenario
Clean {
/// Scenario to clean: alpine, ubuntu, worker, gateway, ha-cluster
scenario: String,
},
/// Show status of all VMs in a scenario
Status {
/// Scenario: alpine, ubuntu, worker, gateway, ha-cluster
scenario: String,
},
}
const ALPINE_ISO: &str =
"https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/x86_64/alpine-virt-3.21.3-x86_64.iso";
const UBUNTU_ISO: &str = "https://releases.ubuntu.com/24.04.2/ubuntu-24.04.2-live-server-amd64.iso";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let cli = Cli::parse();
let executor = init_executor()?;
match cli.command {
Commands::Alpine => deploy_alpine(&executor).await?,
Commands::Ubuntu => deploy_ubuntu(&executor).await?,
Commands::Worker => deploy_worker(&executor).await?,
Commands::Gateway => deploy_gateway(&executor).await?,
Commands::HaCluster => deploy_ha_cluster(&executor).await?,
Commands::Clean { scenario } => clean(&executor, &scenario).await?,
Commands::Status { scenario } => status(&executor, &scenario).await?,
}
Ok(())
}
// ── Alpine: minimal VM ──────────────────────────────────────────────────
async fn deploy_alpine(executor: &KvmExecutor) -> Result<(), Box<dyn std::error::Error>> {
let net = NetworkConfig::builder("alpine-net")
.subnet("192.168.110.1", 24)
.forward(ForwardMode::Nat)
.build();
executor.ensure_network(net).await?;
let vm = VmConfig::builder("alpine-vm")
.vcpus(1)
.memory_mib(512)
.disk(2)
.network(NetworkRef::named("alpine-net"))
.cdrom(ALPINE_ISO)
.boot_order([BootDevice::Cdrom, BootDevice::Disk])
.build();
executor.ensure_vm(vm.clone()).await?;
executor.start_vm(&vm.name).await?;
info!("Alpine VM running. Connect: virsh console {}", vm.name);
info!("Login: root (no password). Install: setup-alpine");
Ok(())
}
// ── Ubuntu Server: standard setup ───────────────────────────────────────
async fn deploy_ubuntu(executor: &KvmExecutor) -> Result<(), Box<dyn std::error::Error>> {
let net = NetworkConfig::builder("ubuntu-net")
.subnet("192.168.120.1", 24)
.forward(ForwardMode::Nat)
.build();
executor.ensure_network(net).await?;
let vm = VmConfig::builder("ubuntu-server")
.vcpus(2)
.memory_gb(4)
.disk(25)
.network(NetworkRef::named("ubuntu-net"))
.cdrom(UBUNTU_ISO)
.boot_order([BootDevice::Cdrom, BootDevice::Disk])
.build();
executor.ensure_vm(vm.clone()).await?;
executor.start_vm(&vm.name).await?;
info!(
"Ubuntu Server VM running. Connect: virsh console {}",
vm.name
);
info!("Follow the interactive installer to complete setup.");
Ok(())
}
// ── Worker: multi-disk for Ceph ─────────────────────────────────────────
async fn deploy_worker(executor: &KvmExecutor) -> Result<(), Box<dyn std::error::Error>> {
let net = NetworkConfig::builder("worker-net")
.subnet("192.168.130.1", 24)
.forward(ForwardMode::Nat)
.build();
executor.ensure_network(net).await?;
let vm = VmConfig::builder("worker-node")
.vcpus(4)
.memory_gb(8)
.disk(60) // vda: OS
.disk(100) // vdb: Ceph OSD 1
.disk(100) // vdc: Ceph OSD 2
.network(NetworkRef::named("worker-net"))
.cdrom(ALPINE_ISO) // Use Alpine for fast testing
.boot_order([BootDevice::Cdrom, BootDevice::Disk])
.build();
executor.ensure_vm(vm.clone()).await?;
executor.start_vm(&vm.name).await?;
info!("Worker node running with 3 disks (vda=60G OS, vdb=100G OSD, vdc=100G OSD)");
info!("Connect: virsh console {}", vm.name);
Ok(())
}
// ── Gateway: dual-NIC firewall ──────────────────────────────────────────
async fn deploy_gateway(executor: &KvmExecutor) -> Result<(), Box<dyn std::error::Error>> {
// WAN: NAT network (internet access)
let wan = NetworkConfig::builder("gw-wan")
.subnet("192.168.140.1", 24)
.forward(ForwardMode::Nat)
.build();
// LAN: isolated network (no internet, internal only)
let lan = NetworkConfig::builder("gw-lan")
.subnet("10.100.0.1", 24)
.isolated()
.build();
executor.ensure_network(wan).await?;
executor.ensure_network(lan).await?;
let vm = VmConfig::builder("gateway-vm")
.vcpus(2)
.memory_gb(2)
.disk(10)
.network(NetworkRef::named("gw-wan")) // First NIC = WAN
.network(NetworkRef::named("gw-lan")) // Second NIC = LAN
.cdrom(ALPINE_ISO)
.boot_order([BootDevice::Cdrom, BootDevice::Disk])
.build();
executor.ensure_vm(vm.clone()).await?;
executor.start_vm(&vm.name).await?;
info!("Gateway VM running with 2 NICs: WAN (gw-wan) + LAN (gw-lan)");
info!("Connect: virsh console {}", vm.name);
Ok(())
}
// ── HA Cluster: full OKD-style deployment ───────────────────────────────
async fn deploy_ha_cluster(executor: &KvmExecutor) -> Result<(), Box<dyn std::error::Error>> {
// Network: NAT for external access, all nodes on the same subnet
let cluster_net = NetworkConfig::builder("ha-cluster")
.bridge("virbr-ha")
.subnet("10.200.0.1", 24)
.forward(ForwardMode::Nat)
.build();
executor.ensure_network(cluster_net).await?;
// Gateway / firewall / load balancer
let gateway = VmConfig::builder("ha-gateway")
.vcpus(2)
.memory_gb(2)
.disk(10)
.network(NetworkRef::named("ha-cluster"))
.boot_order([BootDevice::Network, BootDevice::Disk])
.build();
executor.ensure_vm(gateway.clone()).await?;
info!("Defined: {} (gateway/firewall)", gateway.name);
// Control plane nodes
for i in 1..=3 {
let cp = VmConfig::builder(format!("ha-cp-{i}"))
.vcpus(4)
.memory_gb(16)
.disk(120)
.network(NetworkRef::named("ha-cluster"))
.boot_order([BootDevice::Network, BootDevice::Disk])
.build();
executor.ensure_vm(cp.clone()).await?;
info!("Defined: {} (control plane)", cp.name);
}
// Worker nodes with Ceph storage
for i in 1..=3 {
let worker = VmConfig::builder(format!("ha-worker-{i}"))
.vcpus(8)
.memory_gb(32)
.disk(120) // vda: OS
.disk(200) // vdb: Ceph OSD
.network(NetworkRef::named("ha-cluster"))
.boot_order([BootDevice::Network, BootDevice::Disk])
.build();
executor.ensure_vm(worker.clone()).await?;
info!("Defined: {} (worker + Ceph)", worker.name);
}
info!("HA cluster defined (7 VMs). Start individually or use PXE boot.");
info!(
"To start all: for vm in ha-gateway ha-cp-{{1..3}} ha-worker-{{1..3}}; do virsh start $vm; done"
);
Ok(())
}
// ── Clean up ────────────────────────────────────────────────────────────
async fn clean(executor: &KvmExecutor, scenario: &str) -> Result<(), Box<dyn std::error::Error>> {
let (vms, nets) = match scenario {
"alpine" => (vec!["alpine-vm"], vec!["alpine-net"]),
"ubuntu" => (vec!["ubuntu-server"], vec!["ubuntu-net"]),
"worker" => (vec!["worker-node"], vec!["worker-net"]),
"gateway" => (vec!["gateway-vm"], vec!["gw-wan", "gw-lan"]),
"ha-cluster" => (
vec![
"ha-gateway",
"ha-cp-1",
"ha-cp-2",
"ha-cp-3",
"ha-worker-1",
"ha-worker-2",
"ha-worker-3",
],
vec!["ha-cluster"],
),
other => {
eprintln!("Unknown scenario: {other}");
eprintln!("Available: alpine, ubuntu, worker, gateway, ha-cluster");
std::process::exit(1);
}
};
for vm in &vms {
info!("Cleaning up VM: {vm}");
let _ = executor.destroy_vm(vm).await;
let _ = executor.undefine_vm(vm).await;
}
for net in &nets {
info!("Cleaning up network: {net}");
let _ = executor.delete_network(net).await;
}
info!("Cleanup complete for scenario: {scenario}");
Ok(())
}
// ── Status ──────────────────────────────────────────────────────────────
async fn status(executor: &KvmExecutor, scenario: &str) -> Result<(), Box<dyn std::error::Error>> {
let vms: Vec<&str> = match scenario {
"alpine" => vec!["alpine-vm"],
"ubuntu" => vec!["ubuntu-server"],
"worker" => vec!["worker-node"],
"gateway" => vec!["gateway-vm"],
"ha-cluster" => vec![
"ha-gateway",
"ha-cp-1",
"ha-cp-2",
"ha-cp-3",
"ha-worker-1",
"ha-worker-2",
"ha-worker-3",
],
other => {
eprintln!("Unknown scenario: {other}");
std::process::exit(1);
}
};
println!("{:<20} STATUS", "VM");
println!("{}", "-".repeat(35));
for vm in &vms {
let status = match executor.vm_status(vm).await {
Ok(s) => format!("{s:?}"),
Err(_) => "not found".to_string(),
};
println!("{:<20} {}", vm, status);
}
Ok(())
}