386 lines
10 KiB
Text
386 lines
10 KiB
Text
# VM State Recovery (Phase 2)
|
|
#
|
|
# Recovers VM state after host reboot and restarts permanent VMs.
|
|
# Rule 1: Single purpose, Rule 5: Atomic operations
|
|
|
|
# Selective imports (ADR-025 Phase 3 Layer 2).
|
|
use platform/vm/vm_persistence.nu [get-vm-persistence-info list-permanent-vms]
|
|
use platform/vm/lifecycle.nu [vm-info vm-start]
|
|
|
|
export def "recover-vms-on-boot" []: record {
|
|
"""
|
|
Recover and restart VMs after host reboot.
|
|
|
|
Executed during system startup to restore permanent VMs.
|
|
"""
|
|
|
|
print "🔄 VM State Recovery: Starting permanent VM restoration..."
|
|
|
|
# Get all permanent VMs
|
|
let permanent_vms = (list-permanent-vms)
|
|
|
|
if ($permanent_vms | length) == 0 {
|
|
return {
|
|
success: true
|
|
message: "No permanent VMs to recover"
|
|
}
|
|
}
|
|
|
|
# Sort by start order
|
|
let sorted_vms = (
|
|
$permanent_vms
|
|
| sort-by {|vm| ($vm.start_order // 100)}
|
|
)
|
|
|
|
# Start each VM in order
|
|
let results = (
|
|
$sorted_vms
|
|
| map {|vm|
|
|
start-permanent-vm-on-boot $vm
|
|
}
|
|
)
|
|
|
|
let successful = ($results | where success == true | length)
|
|
let failed = ($results | where success == false | length)
|
|
|
|
print $"✓ VM Recovery Complete: ($successful) started, ($failed) failed"
|
|
|
|
{
|
|
success: ($failed == 0)
|
|
total: ($sorted_vms | length)
|
|
started: $successful
|
|
failed: $failed
|
|
details: $results
|
|
}
|
|
}
|
|
|
|
def start-permanent-vm-on-boot [vm_info: record]: record {
|
|
"""Start a permanent VM on boot with retry logic"""
|
|
|
|
let vm_name = $vm_info.vm_name
|
|
let start_retries = ($vm_info.start_retries // 3)
|
|
|
|
print $" Starting VM: ($vm_name)..."
|
|
|
|
# Wait for VM state to stabilize
|
|
let wait_delay = ($vm_info.start_delay_seconds // 0)
|
|
if $wait_delay > 0 {
|
|
print $" Waiting ($wait_delay) seconds before start..."
|
|
sleep ($wait_delay)s
|
|
}
|
|
|
|
# Try to start VM with retries
|
|
let start_result = (
|
|
(0..$start_retries)
|
|
| reduce {|attempt, result_so_far|
|
|
if $result_so_far.success {
|
|
return $result_so_far
|
|
}
|
|
|
|
# Attempt to start VM (no try-catch, guard pattern)
|
|
let try_result = (vm-start $vm_name)
|
|
|
|
if $try_result.success {
|
|
{success: true, attempt: ($attempt + 1)}
|
|
} else if $attempt < $start_retries {
|
|
print $" Retry $($attempt + 1)/$start_retries in 10 seconds..."
|
|
sleep 10s
|
|
$result_so_far
|
|
} else {
|
|
$try_result
|
|
}
|
|
}
|
|
)
|
|
|
|
if not $start_result.success {
|
|
print $" ✗ Failed to start ($vm_name): ($start_result.error)"
|
|
return {
|
|
success: false
|
|
vm_name: $vm_name
|
|
error: $start_result.error
|
|
}
|
|
}
|
|
|
|
print $" ✓ Started ($vm_name)"
|
|
|
|
{
|
|
success: true
|
|
vm_name: $vm_name
|
|
message: "VM started successfully"
|
|
}
|
|
}
|
|
|
|
export def "save-vm-state-snapshot" [
|
|
vm_name: string # VM name
|
|
]: record {
|
|
"""Save a snapshot of VM state for recovery"""
|
|
|
|
let vm_info = (vm-info $vm_name)
|
|
|
|
if ("error" in $vm_info) {
|
|
return {
|
|
success: false
|
|
error: $vm_info.error
|
|
}
|
|
}
|
|
|
|
let snapshot = {
|
|
vm_name: $vm_name
|
|
snapshot_time: (date now)
|
|
vm_state: $vm_info.status
|
|
ip_address: ($vm_info.ip_address // "")
|
|
cpu_cores: $vm_info.cpu_cores
|
|
memory_mb: $vm_info.memory_mb
|
|
}
|
|
|
|
let snapshot_file = (get-snapshot-file $vm_name)
|
|
|
|
# Save snapshot (no try-catch)
|
|
let save_result = (do { bash -c $"cat > ($snapshot_file) << 'EOF'\n($snapshot | to json)\nEOF" } | complete)
|
|
if $save_result.exit_code != 0 {
|
|
return {
|
|
success: false
|
|
error: $"Failed to save state snapshot: ($save_result.stderr)"
|
|
}
|
|
}
|
|
|
|
{
|
|
success: true
|
|
vm_name: $vm_name
|
|
message: "State snapshot saved"
|
|
}
|
|
}
|
|
|
|
export def "restore-vm-state-snapshot" [
|
|
vm_name: string # VM name
|
|
]: record {
|
|
"""Restore VM from saved state snapshot"""
|
|
|
|
let snapshot_file = (get-snapshot-file $vm_name)
|
|
|
|
if not ($snapshot_file | path exists) {
|
|
return {
|
|
success: false
|
|
error: "No snapshot found for VM"
|
|
}
|
|
}
|
|
|
|
# Load snapshot (no try-catch)
|
|
let snap_result = (do { open $snapshot_file | from json } | complete)
|
|
if $snap_result.exit_code != 0 {
|
|
return {
|
|
success: false
|
|
error: $"Failed to load snapshot: ($snap_result.stderr)"
|
|
}
|
|
}
|
|
|
|
let snapshot = ($snap_result.stdout)
|
|
|
|
# Only restore if it was running
|
|
if $snapshot.vm_state != "running" {
|
|
return {
|
|
success: true
|
|
message: "VM was not running at snapshot time"
|
|
}
|
|
}
|
|
|
|
# Start the VM (no try-catch)
|
|
vm-start $vm_name
|
|
}
|
|
|
|
export def "register-vm-autostart" [
|
|
vm_name: string # VM name
|
|
--start-order: int = 100 # Priority order (lower starts first)
|
|
--start-delay: int = 0 # Seconds to delay before start
|
|
--wait-for-ssh: bool = true # Wait for SSH before continuing
|
|
--max-retries: int = 3 # Max start attempts
|
|
]: record {
|
|
"""Register VM for automatic startup on boot"""
|
|
|
|
let persist_info = (get-vm-persistence-info $vm_name)
|
|
|
|
if not ("mode" in $persist_info) {
|
|
return {
|
|
success: false
|
|
error: "VM not found in persistence"
|
|
}
|
|
}
|
|
|
|
let updated = (
|
|
$persist_info
|
|
| upsert auto_start true
|
|
| upsert start_order $start_order
|
|
| upsert start_delay_seconds $start_delay
|
|
| upsert wait_for_ssh $wait_for_ssh
|
|
| upsert max_start_retries $max_retries
|
|
)
|
|
|
|
let persist_file = (get-persistence-file $vm_name)
|
|
|
|
# Save autostart configuration (no try-catch)
|
|
let save_result = (do { bash -c $"cat > ($persist_file) << 'EOF'\n($updated | to json)\nEOF" } | complete)
|
|
if $save_result.exit_code != 0 {
|
|
return {
|
|
success: false
|
|
error: $"Failed to save autostart configuration: ($save_result.stderr)"
|
|
}
|
|
}
|
|
|
|
{
|
|
success: true
|
|
vm_name: $vm_name
|
|
start_order: $start_order
|
|
message: "VM registered for autostart"
|
|
}
|
|
}
|
|
|
|
export def "get-vms-pending-recovery" []: table {
|
|
"""Get list of VMs that need recovery after crash"""
|
|
|
|
let permanent_vms = (list-permanent-vms)
|
|
|
|
$permanent_vms
|
|
| filter {|vm|
|
|
($vm.auto_start // false) == true
|
|
}
|
|
| map {|vm|
|
|
{
|
|
vm_name: $vm.vm_name
|
|
auto_start: true
|
|
start_order: ($vm.start_order // 100)
|
|
start_delay_seconds: ($vm.start_delay_seconds // 0)
|
|
}
|
|
}
|
|
| sort-by {|vm| $vm.start_order}
|
|
}
|
|
|
|
export def "wait-for-vm-ssh" [
|
|
vm_name: string # VM name
|
|
--timeout: int = 300 # Timeout in seconds
|
|
]: record {
|
|
"""Wait for VM SSH to become available"""
|
|
|
|
let start_time = (date now)
|
|
let timeout_duration = ($timeout)sec
|
|
mut attempts = 0
|
|
let max_attempts = ($timeout / 2) + 1 # Safety limit based on sleep 2s
|
|
|
|
while { $attempts < $max_attempts } {
|
|
let elapsed = ((date now) - $start_time)
|
|
|
|
if $elapsed >= $timeout_duration {
|
|
return {
|
|
success: false
|
|
error: $"SSH timeout after ($timeout_seconds) seconds"
|
|
}
|
|
}
|
|
|
|
# Check SSH availability (no try-catch)
|
|
let ssh_check = (do { vm-ssh $vm_name --command "echo ok" } | complete)
|
|
|
|
if $ssh_check.exit_code == 0 {
|
|
return {
|
|
success: true
|
|
message: "SSH ready"
|
|
}
|
|
}
|
|
|
|
sleep 2s
|
|
$attempts += 1
|
|
}
|
|
|
|
{
|
|
success: false
|
|
error: $"SSH timeout after ($timeout) seconds"
|
|
}
|
|
}
|
|
|
|
export def "create-vm-recovery-script" []: record {
|
|
"""Create systemd service for VM recovery on boot"""
|
|
|
|
let script_path = "/usr/local/bin/vm-recovery.sh"
|
|
let service_path = "/etc/systemd/system/vm-recovery.service"
|
|
|
|
# Create recovery script
|
|
let script_content = '#!/bin/bash
|
|
echo "Starting VM recovery..."
|
|
nu -c "use platform/vm/state_recovery.nu *; recover-vms-on-boot"
|
|
echo "VM recovery complete"
|
|
'
|
|
|
|
# Create recovery script (no try-catch)
|
|
let create_result = (do { bash -c $"cat > ($script_path) << 'SCRIPT'\n($script_content)\nSCRIPT" } | complete)
|
|
if $create_result.exit_code != 0 {
|
|
return {
|
|
success: false
|
|
error: $"Failed to create recovery script: ($create_result.stderr)"
|
|
}
|
|
}
|
|
|
|
let chmod_result = (do { bash -c $"chmod +x ($script_path)" } | complete)
|
|
if $chmod_result.exit_code != 0 {
|
|
return {
|
|
success: false
|
|
error: $"Failed to set script permissions: ($chmod_result.stderr)"
|
|
}
|
|
}
|
|
|
|
# Create systemd service
|
|
let service_content = '[Unit]
|
|
Description=VM Recovery Service
|
|
After=network.target libvirtd.service
|
|
Wants=libvirtd.service
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/usr/local/bin/vm-recovery.sh
|
|
RemainAfterExit=yes
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
'
|
|
|
|
# Create systemd service (no try-catch)
|
|
let service_write_result = (do { bash -c $"cat > ($service_path) << 'SERVICE'\n($service_content)\nSERVICE" } | complete)
|
|
if $service_write_result.exit_code != 0 {
|
|
return {
|
|
success: false
|
|
error: $"Failed to write systemd service file: ($service_write_result.stderr)"
|
|
}
|
|
}
|
|
|
|
let daemon_reload_result = (do { bash -c "systemctl daemon-reload || true" } | complete)
|
|
if $daemon_reload_result.exit_code != 0 {
|
|
return {
|
|
success: false
|
|
error: $"Failed to reload systemd: ($daemon_reload_result.stderr)"
|
|
}
|
|
}
|
|
|
|
let enable_result = (do { bash -c "systemctl enable vm-recovery.service || true" } | complete)
|
|
if $enable_result.exit_code != 0 {
|
|
return {
|
|
success: false
|
|
error: $"Failed to enable systemd service: ($enable_result.stderr)"
|
|
}
|
|
}
|
|
|
|
{
|
|
success: true
|
|
script_path: $script_path
|
|
service_path: $service_path
|
|
message: "VM recovery service created"
|
|
}
|
|
}
|
|
|
|
def get-snapshot-file [vm_name: string]: string {
|
|
"""Get snapshot file path"""
|
|
$"{{paths.workspace}}/vms/snapshots/($vm_name).json"
|
|
}
|
|
|
|
def get-persistence-file [vm_name: string]: string {
|
|
"""Get persistence file"""
|
|
$"{{paths.workspace}}/vms/persistence/($vm_name).json"
|
|
}
|