# VM Persistence Management (Phase 2) # # Manages permanent and temporary VMs with lifecycle tracking. # Rule 1: Single purpose, Rule 4: Pure functions, Rule 5: Atomic operations # Error handling: Result pattern (hybrid, do/complete for bash operations) # Selective imports (ADR-025 Phase 3 Layer 2). # vm/persistence.nu star-import was dead — dropped. use primitives/result.nu [err is-err json-read json-write map match-result] use platform/vm/lifecycle.nu [vm-delete] export def "register-permanent-vm" [ vm_config: record # VM configuration ]: record { """ Register a VM as permanent (persists across reboots). Permanent VMs are automatically started on host reboot. """ if not ($vm_config.permanent // false) { return { success: false error: "VM must have permanent=true" } } # Create persistence record let now = (now | format date "%Y-%m-%dT%H:%M:%SZ") let persistence_info = { vm_name: $vm_config.name mode: "permanent" created_at: $now auto_start: true restart_policy: "always" start_order: 100 } # Save persistence data using json-write helper (no inline try-catch) let persist_file = (get-persistence-file $vm_config.name) let write_result = (json-write $persist_file $persistence_info) # Guard: Check write result if (is-err $write_result) { return {success: false, error: $write_result.err} } {success: true, vm_name: $vm_config.name, message: "Registered as permanent VM"} } export def "register-temporary-vm" [ vm_config: record # VM configuration ttl_hours: int = 24 # Time to live ]: record { """ Register a VM as temporary (auto-cleanup after TTL). Temporary VMs are automatically deleted after specified hours. """ if not ($vm_config.temporary // false) { return { success: false error: "VM must have temporary=true" } } # Calculate cleanup time let now = (now | format date "%Y-%m-%dT%H:%M:%SZ") let cleanup_time = ( $now + (($ttl_hours * 60 * 60) * 1_000_000_000) # Convert to nanoseconds ) let persistence_info = { vm_name: $vm_config.name mode: "temporary" created_at: $now ttl_hours: $ttl_hours scheduled_cleanup_at: $cleanup_time auto_cleanup: true cleanup_status: "pending" } let persist_file = (get-persistence-file $vm_config.name) let write_result = (json-write $persist_file $persistence_info) # Guard: Check write result if (is-err $write_result) { return {success: false, error: $write_result.err} } { success: true vm_name: $vm_config.name ttl_hours: $ttl_hours cleanup_scheduled_at: $cleanup_time message: $"Registered as temporary VM (cleanup in ($ttl_hours) hours)" } } export def "get-vm-persistence-info" [ vm_name: string # VM name ]: record { """Get persistence information for a VM""" let persist_file = (get-persistence-file $vm_name) # Guard: File exists check if not ($persist_file | path exists) { return {vm_name: $vm_name, mode: "unknown", error: "No persistence info found"} } # Read using json-read helper (no inline try-catch) (json-read $persist_file) | match-result {|data| $data} # On success, return data {|_err| {vm_name: $vm_name, mode: "unknown", error: "No persistence info found"}} # On error, return default } export def "list-permanent-vms" []: table { """List all permanent VMs""" let persist_dir = (get-persistence-dir) if not ($persist_dir | path exists) { return [] } # Use do/complete for bash command (no try-catch) let ls_result = (do { bash -c $"ls -1 ($persist_dir)/*.json 2>/dev/null" } | complete) if $ls_result.exit_code != 0 { return [] } $ls_result.stdout | lines | where {|f| ($f | length) > 0} | map {|f| # Guard: Check if file can be opened and parsed as JSON let json_result = (do { open $f | from json } | complete) if $json_result.exit_code == 0 { let data = ($json_result.stdout) if ($data.mode // "unknown") == "permanent" { $data } else { null } } else { null } } | compact } export def "list-temporary-vms" []: table { """List all temporary VMs""" let persist_dir = (get-persistence-dir) if not ($persist_dir | path exists) { return [] } # Use do/complete for bash command (no try-catch) let ls_result = (do { bash -c $"ls -1 ($persist_dir)/*.json 2>/dev/null" } | complete) if $ls_result.exit_code != 0 { return [] } $ls_result.stdout | lines | where {|f| ($f | length) > 0} | map {|f| # Guard: Check if file can be opened and parsed as JSON let json_result = (do { open $f | from json } | complete) if $json_result.exit_code == 0 { let data = ($json_result.stdout) if ($data.mode // "unknown") == "temporary" { $data } else { null } } else { null } } | compact } export def "find-expired-vms" []: table { """Find temporary VMs that have expired (TTL exceeded)""" let now = (now | format date "%Y-%m-%dT%H:%M:%SZ") let temp_vms = (list-temporary-vms) $temp_vms | filter {|vm| ($vm.scheduled_cleanup_at // 0) <= $now } } export def "cleanup-expired-vms" [ --dry-run = false # Dry-run mode --force = false # Force cleanup ]: record { """Clean up expired temporary VMs""" let expired = (find-expired-vms) if ($expired | length) == 0 { return { success: true cleaned_up: 0 message: "No VMs to clean up" } } let results = ( $expired | map {|vm| let delete_result = ( if $dry_run { { vm_name: $vm.vm_name success: true message: "[DRY-RUN] Would delete VM" } } else { vm-delete $vm.vm_name --force=$force } ) # Update cleanup status if $delete_result.success { update-cleanup-status $vm.vm_name "completed" | ignore } else { update-cleanup-status $vm.vm_name "failed" $delete_result.error | ignore } $delete_result } ) let successful = ($results | where success == true | length) { success: true total_expired: ($expired | length) cleaned_up: $successful failed: (($results | length) - $successful) details: $results } } export def "get-vm-uptime" [ vm_name: string # VM name ]: record { """Get VM uptime since creation""" let persist_info = (get-vm-persistence-info $vm_name) let now = (now | format date "%Y-%m-%dT%H:%M:%SZ") if ("created_at" in $persist_info) { let uptime_seconds = ($now - $persist_info.created_at) let uptime_hours = ($uptime_seconds / 3600) let uptime_days = ($uptime_hours / 24) { vm_name: $vm_name created_at: $persist_info.created_at uptime_seconds: $uptime_seconds uptime_hours: $uptime_hours uptime_days: $uptime_days formatted: $"($uptime_days | floor)d ($uptime_hours | floor)h ($uptime_seconds % 3600 | floor)s" } } else { { vm_name: $vm_name error: "Cannot determine creation time" } } } export def "get-vm-time-to-cleanup" [ vm_name: string # VM name ]: record { """Get time remaining until cleanup for temporary VM""" let persist_info = (get-vm-persistence-info $vm_name) let now = (now | format date "%Y-%m-%dT%H:%M:%SZ") if ($persist_info.mode // "") != "temporary" { return { vm_name: $vm_name error: "VM is not temporary" } } if not ("scheduled_cleanup_at" in $persist_info) { return { vm_name: $vm_name error: "Cleanup not scheduled" } } let cleanup_time = $persist_info.scheduled_cleanup_at let time_remaining = ($cleanup_time - $now) if $time_remaining <= 0 { return { vm_name: $vm_name status: "expired" time_remaining_seconds: 0 } } let hours = ($time_remaining / 3600_000_000_000) let minutes = (($time_remaining / 60_000_000_000) % 60) { vm_name: $vm_name status: "active" time_remaining_seconds: ($time_remaining / 1_000_000_000) time_remaining_formatted: $"($hours | floor)h ($minutes | floor)m" scheduled_cleanup_at: $cleanup_time } } export def "extend-vm-ttl" [ vm_name: string # VM name additional_hours: int # Hours to add ]: record { """Extend TTL for a temporary VM""" let persist_info = (get-vm-persistence-info $vm_name) if ($persist_info.mode // "") != "temporary" { return { success: false error: "VM is not temporary" } } let current_cleanup_time = ($persist_info.scheduled_cleanup_at // 0) let new_cleanup_time = ( $current_cleanup_time + (($additional_hours * 60 * 60) * 1_000_000_000) ) let updated_info = ( $persist_info | upsert scheduled_cleanup_at $new_cleanup_time ) let persist_file = (get-persistence-file $vm_name) # Use do/complete for bash command (no try-catch) let write_result = (do { bash -c $"cat > ($persist_file) << 'EOF'\n($updated_info | to json)\nEOF" } | complete) if $write_result.exit_code != 0 { return { success: false error: $write_result.stderr } } { success: true vm_name: $vm_name additional_hours: $additional_hours new_cleanup_time: $new_cleanup_time message: $"Extended TTL by ($additional_hours) hours" } } def get-persistence-dir []: string { """Get persistence directory""" $"{{paths.workspace}}/vms/persistence" } def get-persistence-file [vm_name: string]: string { """Get persistence file for VM""" $"(get-persistence-dir)/($vm_name).json" } def update-cleanup-status [ vm_name: string status: string error?: string ]: record { """Update cleanup status in persistence file""" let persist_info = (get-vm-persistence-info $vm_name) let now = (now | format date "%Y-%m-%dT%H:%M:%SZ") let updated = ( $persist_info | upsert cleanup_status $status | upsert last_cleanup_attempt $now | if ($error != null) { upsert cleanup_error $error } else { . } ) let persist_file = (get-persistence-file $vm_name) # Use do/complete for bash command (no try-catch) let write_result = (do { bash -c $"cat > ($persist_file) << 'EOF'\n($updated | to json)\nEOF" } | complete) if $write_result.exit_code != 0 { return {success: false, error: $write_result.stderr} } {success: true} } export def "get-vm-persistence-stats" []: record { """Get statistics about persistent VMs""" let permanent = (list-permanent-vms) let temporary = (list-temporary-vms) let expired = (find-expired-vms) { total_vms: (($permanent | length) + ($temporary | length)) permanent_vms: ($permanent | length) temporary_vms: ($temporary | length) expired_vms: ($expired | length) auto_cleanup_enabled: ($temporary | where auto_cleanup == true | length) } }