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.
359 lines
12 KiB
Rust
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(())
|
|
}
|