provisioning-core/nulib/platform/vm/backend_libvirt.nu

317 lines
9 KiB
Text
Raw Permalink Normal View History

# 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"}
}