211 lines
5.7 KiB
Text
211 lines
5.7 KiB
Text
|
|
# 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
|
||
|
|
}
|
||
|
|
}
|