317 lines
9 KiB
Text
317 lines
9 KiB
Text
|
|
# libvirt Backend Integration
|
||
|
|
#
|
||
|
|
# Low-level libvirt operations using virsh CLI.
|
||
|
|
# Rule 1: Single purpose, Rule 2: Explicit types, Rule 3: Early return
|
||
|
|
# Error handling: Result pattern (hybrid, no inline try-catch)
|
||
|
|
|
||
|
|
# Selective imports (ADR-025 Phase 3 Layer 2).
|
||
|
|
use primitives/result.nu [bash-check bash-or bash-wrap err is-err match-result ok]
|
||
|
|
|
||
|
|
export def "libvirt-create-vm" [
|
||
|
|
config: record # VM configuration
|
||
|
|
]: record {
|
||
|
|
"""Create a virtual machine using libvirt"""
|
||
|
|
|
||
|
|
# Rule 3: Early return for validation
|
||
|
|
if ($config.name | is-empty) {
|
||
|
|
return {
|
||
|
|
success: false
|
||
|
|
error: "VM name required"
|
||
|
|
vm_id: null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Generate libvirt XML domain definition
|
||
|
|
let xml = (generate-libvirt-xml $config)
|
||
|
|
|
||
|
|
# Write XML to temporary file
|
||
|
|
let temp_file = $"/tmp/vm-($config.name)-($env.RANDOM).xml"
|
||
|
|
bash -c $"cat > ($temp_file) << 'EOF'\n($xml)\nEOF"
|
||
|
|
|
||
|
|
# Define domain in libvirt using bash-check helper
|
||
|
|
let define_result = (bash-check $"virsh define ($temp_file)")
|
||
|
|
|
||
|
|
# Cleanup temp file (use bash-or for safe execution)
|
||
|
|
bash -or $"rm -f ($temp_file)" null
|
||
|
|
|
||
|
|
# Guard: Check define result
|
||
|
|
if (is-err $define_result) {
|
||
|
|
return {
|
||
|
|
success: false
|
||
|
|
error: $define_result.err
|
||
|
|
vm_id: null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Get domain ID using bash-or with null fallback
|
||
|
|
let domain_id = (bash-or $"virsh domid ($config.name) | tr -d '\n'" null)
|
||
|
|
|
||
|
|
{
|
||
|
|
success: true
|
||
|
|
vm_id: $domain_id
|
||
|
|
vm_name: $config.name
|
||
|
|
message: $"VM ($config.name) defined successfully"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
def generate-libvirt-xml [config: record]: string {
|
||
|
|
"""Generate libvirt domain XML from VM config"""
|
||
|
|
|
||
|
|
let memory_kb = ($config.memory_mb * 1024)
|
||
|
|
let vcpus = $config.cpu
|
||
|
|
|
||
|
|
$"<domain type='kvm'>
|
||
|
|
<name>($config.name)</name>
|
||
|
|
<memory unit='KiB'>($memory_kb)</memory>
|
||
|
|
<currentMemory unit='KiB'>($memory_kb)</currentMemory>
|
||
|
|
<vcpu placement='static'>($vcpus)</vcpu>
|
||
|
|
<os>
|
||
|
|
<type arch='x86_64'>hvm</type>
|
||
|
|
<boot dev='hd'/>
|
||
|
|
</os>
|
||
|
|
<devices>
|
||
|
|
<emulator>/usr/bin/qemu-system-x86_64</emulator>
|
||
|
|
<disk type='file' device='disk'>
|
||
|
|
<driver name='qemu' type='qcow2'/>
|
||
|
|
<source file='(.get-vm-disk-path $config.name)'/>
|
||
|
|
<target dev='vda' bus='virtio'/>
|
||
|
|
</disk>
|
||
|
|
<interface type='network'>
|
||
|
|
<mac address='(.generate-mac-address)'/>
|
||
|
|
<source network='default'/>
|
||
|
|
<model type='rtl8139'/>
|
||
|
|
</interface>
|
||
|
|
<console type='pty'>
|
||
|
|
<target type='serial' port='0'/>
|
||
|
|
</console>
|
||
|
|
<memballoon model='virtio'/>
|
||
|
|
</devices>
|
||
|
|
</domain>"
|
||
|
|
}
|
||
|
|
|
||
|
|
export def "libvirt-start-vm" [
|
||
|
|
vm_name: string # VM name
|
||
|
|
]: record {
|
||
|
|
"""Start a virtual machine"""
|
||
|
|
|
||
|
|
# Guard: Input validation
|
||
|
|
if ($vm_name | is-empty) {
|
||
|
|
return {success: false, error: "VM name required"}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Execute using bash-check helper (no inline try-catch)
|
||
|
|
let result = (bash-check $"virsh start ($vm_name)")
|
||
|
|
|
||
|
|
# Guard: Check result
|
||
|
|
if (is-err $result) {
|
||
|
|
return {success: false, error: $result.err, vm_name: $vm_name}
|
||
|
|
}
|
||
|
|
|
||
|
|
{success: true, vm_name: $vm_name, message: $"VM ($vm_name) started"}
|
||
|
|
}
|
||
|
|
|
||
|
|
export def "libvirt-stop-vm" [
|
||
|
|
vm_name: string # VM name
|
||
|
|
--force = false # Force shutdown
|
||
|
|
]: record {
|
||
|
|
"""Stop a virtual machine"""
|
||
|
|
|
||
|
|
# Guard: Input validation
|
||
|
|
if ($vm_name | is-empty) {
|
||
|
|
return {success: false, error: "VM name required"}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Guard: Build command based on flags
|
||
|
|
let cmd = (if $force { $"virsh destroy ($vm_name)" } else { $"virsh shutdown ($vm_name)" })
|
||
|
|
|
||
|
|
# Execute using bash-check helper (no inline try-catch)
|
||
|
|
let result = (bash-check $cmd)
|
||
|
|
|
||
|
|
# Guard: Check result
|
||
|
|
if (is-err $result) {
|
||
|
|
return {success: false, error: $result.err, vm_name: $vm_name}
|
||
|
|
}
|
||
|
|
|
||
|
|
{success: true, vm_name: $vm_name, message: $"VM ($vm_name) stopped"}
|
||
|
|
}
|
||
|
|
|
||
|
|
export def "libvirt-delete-vm" [
|
||
|
|
vm_name: string # VM name
|
||
|
|
]: record {
|
||
|
|
"""Delete a virtual machine and its disk"""
|
||
|
|
|
||
|
|
# Guard: Input validation
|
||
|
|
if ($vm_name | is-empty) {
|
||
|
|
return {success: false, error: "VM name required"}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Guard: Check if running using bash-or helper (no inline try-catch)
|
||
|
|
let is_running = (
|
||
|
|
(bash-or $"virsh domstate ($vm_name) | grep -q running; echo $?" "1") | str trim == "0"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Stop VM if running
|
||
|
|
if $is_running {
|
||
|
|
let stop_result = (libvirt-stop-vm $vm_name --force)
|
||
|
|
if not $stop_result.success {
|
||
|
|
return $stop_result
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Undefine domain using bash-check helper
|
||
|
|
let undefine_result = (bash-check $"virsh undefine ($vm_name)")
|
||
|
|
|
||
|
|
# Guard: Check undefine result
|
||
|
|
if (is-err $undefine_result) {
|
||
|
|
return {success: false, error: $undefine_result.err, vm_name: $vm_name}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Delete disk using bash-or helper (safe, ignores errors)
|
||
|
|
let disk_path = (get-vm-disk-path $vm_name)
|
||
|
|
bash -or $"rm -f ($disk_path)" null
|
||
|
|
|
||
|
|
{success: true, vm_name: $vm_name, message: $"VM ($vm_name) deleted"}
|
||
|
|
}
|
||
|
|
|
||
|
|
export def "libvirt-list-vms" []: table {
|
||
|
|
"""List all libvirt VMs"""
|
||
|
|
|
||
|
|
# Guard: List VMs using bash-wrap helper
|
||
|
|
let list_result = (bash-wrap "virsh list --all --name")
|
||
|
|
|
||
|
|
# Guard: Check if listing succeeded
|
||
|
|
if (is-err $list_result) {
|
||
|
|
return [] # Return empty list on error
|
||
|
|
}
|
||
|
|
|
||
|
|
# Process VM list
|
||
|
|
$list_result.ok
|
||
|
|
| lines
|
||
|
|
| where {|x| ($x | length) > 0}
|
||
|
|
| each {|vm_name|
|
||
|
|
# Get state using bash-or helper with fallback
|
||
|
|
let state = (bash-or $"virsh domstate ($vm_name) | tr -d '\n'" "unknown")
|
||
|
|
|
||
|
|
{
|
||
|
|
name: $vm_name
|
||
|
|
state: $state
|
||
|
|
backend: "libvirt"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export def "libvirt-get-vm-info" [
|
||
|
|
vm_name: string # VM name
|
||
|
|
]: record {
|
||
|
|
"""Get detailed VM information from libvirt"""
|
||
|
|
|
||
|
|
# Guard: Input validation
|
||
|
|
if ($vm_name | is-empty) {
|
||
|
|
return {error: "VM name required"}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Get state using bash-or helper
|
||
|
|
let state = (bash-or $"virsh domstate ($vm_name) | tr -d '\n'" "unknown")
|
||
|
|
|
||
|
|
# Get domain ID using bash-or helper
|
||
|
|
let domain_id = (bash-or $"virsh domid ($vm_name) | tr -d '\n'" null)
|
||
|
|
|
||
|
|
# Get detailed info using bash-wrap helper
|
||
|
|
let info = (
|
||
|
|
(bash-wrap $"virsh dominfo ($vm_name)")
|
||
|
|
| match-result
|
||
|
|
{|output|
|
||
|
|
$output | lines
|
||
|
|
| reduce fold {|line, acc|
|
||
|
|
let parts = ($line | split row " " | where {|x| ($x | length) > 0})
|
||
|
|
if ($parts | length) >= 2 {
|
||
|
|
let key = ($parts | get 0)
|
||
|
|
let value = ($parts | skip 1 | str join " ")
|
||
|
|
{($key): $value} | merge $acc
|
||
|
|
} else {
|
||
|
|
$acc
|
||
|
|
}
|
||
|
|
} {}
|
||
|
|
}
|
||
|
|
{|_err| {}} # Return empty record on error
|
||
|
|
)
|
||
|
|
|
||
|
|
{
|
||
|
|
name: $vm_name
|
||
|
|
state: $state
|
||
|
|
id: $domain_id
|
||
|
|
cpu_cores: (($info | get "CPU(s)" 2>/dev/null) // "unknown")
|
||
|
|
memory_mb: (($info | get "Max memory" 2>/dev/null) // "unknown")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export def "libvirt-get-vm-ip" [
|
||
|
|
vm_name: string # VM name
|
||
|
|
]: string {
|
||
|
|
"""Get VM IP address from libvirt"""
|
||
|
|
|
||
|
|
# Guard: Input validation
|
||
|
|
if ($vm_name | is-empty) {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
# Get IP using bash-wrap helper
|
||
|
|
(bash-wrap $"virsh domifaddr ($vm_name)")
|
||
|
|
| match-result
|
||
|
|
{|output|
|
||
|
|
$output
|
||
|
|
| lines
|
||
|
|
| skip 2 # Skip header
|
||
|
|
| where {|x| ($x | length) > 0}
|
||
|
|
| get 0? # Optional access
|
||
|
|
| split row " "
|
||
|
|
| where {|x| ($x | length) > 0}
|
||
|
|
| get 2? # Optional access
|
||
|
|
| split row "/"
|
||
|
|
| get 0
|
||
|
|
}
|
||
|
|
{|_err| ""} # Return empty string on error
|
||
|
|
}
|
||
|
|
|
||
|
|
def get-vm-disk-path [vm_name: string]: string {
|
||
|
|
"""Get disk path for VM"""
|
||
|
|
$"{{paths.workspace}}/vms/permanent/($vm_name)/disk.qcow2"
|
||
|
|
}
|
||
|
|
|
||
|
|
def generate-mac-address []: string {
|
||
|
|
"""Generate random MAC address"""
|
||
|
|
let random = (bash -c "echo $((RANDOM % 256))").0
|
||
|
|
$"52:54:00:($random | fill -a r -w 2 -c 0):($random | fill -a r -w 2 -c 0):($random | fill -a r -w 2 -c 0)"
|
||
|
|
}
|
||
|
|
|
||
|
|
export def "libvirt-create-disk" [
|
||
|
|
vm_name: string # VM name
|
||
|
|
size_gb: int # Disk size in GB
|
||
|
|
]: record {
|
||
|
|
"""Create QCOW2 disk for VM"""
|
||
|
|
|
||
|
|
# Guard: Input validation
|
||
|
|
if ($vm_name | is-empty) {
|
||
|
|
return {success: false, error: "VM name required", path: null}
|
||
|
|
}
|
||
|
|
if $size_gb <= 0 {
|
||
|
|
return {success: false, error: "Size must be positive", path: null}
|
||
|
|
}
|
||
|
|
|
||
|
|
let disk_path = (get-vm-disk-path $vm_name)
|
||
|
|
let disk_dir = ($disk_path | path dirname)
|
||
|
|
|
||
|
|
# Create directory (safe to ignore errors)
|
||
|
|
bash -or $"mkdir -p ($disk_dir)" null
|
||
|
|
|
||
|
|
# Create QCOW2 disk using bash-check helper
|
||
|
|
let result = (bash-check $"qemu-img create -f qcow2 ($disk_path) ($size_gb)G")
|
||
|
|
|
||
|
|
# Guard: Check result
|
||
|
|
if (is-err $result) {
|
||
|
|
return {success: false, error: $result.err, path: null}
|
||
|
|
}
|
||
|
|
|
||
|
|
{success: true, path: $disk_path, size_gb: $size_gb, format: "qcow2"}
|
||
|
|
}
|