# VM SSH Connectivity and Provisioning # # SSH operations for VMs: connection, provisioning, file transfer. # Rule 1: Single purpose, Rule 2: Explicit types # Selective imports (ADR-025 Phase 3 Layer 2). use platform/vm/backend_libvirt.nu [libvirt-get-vm-ip] use platform/vm/persistence.nu [get-vm-state] export def "vm-ssh" [ vm_name: string # VM name --command: string # Command to execute (optional) ]: record { """ SSH into VM or execute command. Handles VM IP discovery and SSH connectivity. Examples: # SSH shell provisioning vm ssh dev-rust # Execute command provisioning vm ssh dev-rust --command "uname -a" """ # Get VM IP let ip = (get-vm-ip $vm_name) if ($ip | is-empty) { return { success: false error: $"Cannot determine IP for VM ($vm_name)" } } # Wait for SSH if needed let ssh_ready = (wait-for-ssh $ip) if not $ssh_ready { return { success: false error: $"SSH not responding on ($ip)" } } # Execute SSH if ($command | is-empty) { # Interactive shell bash -c $"ssh -o StrictHostKeyChecking=no root@($ip)" {success: true} } else { # Execute command (no try-catch) let output = (do { bash -c $"ssh -o StrictHostKeyChecking=no root@($ip) '($command)'" } | complete) { success: ($output.exit_code == 0) output: $output.stdout error: $output.stderr } } } export def "vm-scp-to" [ vm_name: string # VM name local_path: string # Local file path remote_path: string # Remote file path ]: record { """Copy file from host to VM""" let ip = (get-vm-ip $vm_name) if ($ip | is-empty) { return {success: false, error: $"Cannot determine IP for VM ($vm_name)"} } let wait = (wait-for-ssh $ip) if not $wait { return {success: false, error: $"SSH not ready on ($ip)"} } # Copy file via SCP (no try-catch) let result = (do { bash -c $"scp -r -o StrictHostKeyChecking=no ($local_path) root@($ip):($remote_path)" } | complete) { success: ($result.exit_code == 0) message: $"Copied ($local_path) to ($ip):($remote_path)" error: (if $result.exit_code != 0 { $result.stderr } else { "" }) } } export def "vm-scp-from" [ vm_name: string # VM name remote_path: string # Remote file path local_path: string # Local file path ]: record { """Copy file from VM to host""" let ip = (get-vm-ip $vm_name) if ($ip | is-empty) { return {success: false, error: $"Cannot determine IP for VM ($vm_name)"} } let wait = (wait-for-ssh $ip) if not $wait { return {success: false, error: $"SSH not ready on ($ip)"} } # Copy file via SCP (no try-catch) let result = (do { bash -c $"scp -r -o StrictHostKeyChecking=no root@($ip):($remote_path) ($local_path)" } | complete) { success: ($result.exit_code == 0) message: $"Copied ($ip):($remote_path) to ($local_path)" error: (if $result.exit_code != 0 { $result.stderr } else { "" }) } } export def "vm-exec" [ vm_name: string # VM name command: string # Command to execute ]: record { """Execute command in VM and return output""" if ($command | is-empty) { return {success: false, error: "Command required"} } vm-ssh $vm_name --command=$command } def get-vm-ip [vm_name: string]: string { """Get VM IP address with caching""" # Try to get from libvirt first (fast) let libvirt_ip = (libvirt-get-vm-ip $vm_name) if ($libvirt_ip | length) > 0 { return $libvirt_ip } # Fallback: query state let state = (get-vm-state $vm_name) $state.ip_address // "" } def wait-for-ssh [ip: string, --timeout: int = 300]: bool { """Wait for 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 2sec while { $attempts < $max_attempts } { let elapsed = ((date now) - $start_time) if $elapsed >= $timeout_duration { return false } # Check SSH availability (no try-catch) let ssh_check = (do { bash -c $"ssh-keyscan -t rsa ($ip) 2>/dev/null" } | complete) if $ssh_check.exit_code == 0 { return true } # Wait before retry sleep 2sec $attempts += 1 } false } export def "vm-provision" [ vm_name: string # VM name script: string # Script to execute ]: record { """ Provision VM with script. Uploads and executes provisioning script. """ # Write script to temp file let temp_script = $"/tmp/provision-($vm_name)-($env.RANDOM).sh" # Create script file (no try-catch) let create_result = (do { bash -c $"cat > ($temp_script) << 'SCRIPT'\n($script)\nSCRIPT" } | complete) if $create_result.exit_code != 0 { return {success: false, error: $"Failed to create script: ($create_result.stderr)"} } # SCP script to VM let scp_result = (vm-scp-to $vm_name $temp_script "/tmp/provision.sh") if not $scp_result.success { bash -c $"rm -f ($temp_script)" return $scp_result } # Execute script let exec_result = (vm-ssh $vm_name --command "bash /tmp/provision.sh") # Cleanup bash -c $"rm -f ($temp_script)" vm-ssh $vm_name --command "rm -f /tmp/provision.sh" | ignore { success: $exec_result.success output: $exec_result.output error: $exec_result.error } }