53 lines
2.5 KiB
Text
53 lines
2.5 KiB
Text
|
|
# SSH client primitives — extracted from cli/components.nu (ADR-026 slice 2a-iii).
|
|||
|
|
|
|||
|
|
# Get SSH connection info for a server (user, key path, host) from servers.ncl.
|
|||
|
|
export def comp-server-ssh-info [ws_root: string, infra: string, server: string]: nothing -> record {
|
|||
|
|
let servers_path = ($ws_root | path join "infra" $infra "servers.ncl")
|
|||
|
|
if not ($servers_path | path exists) {
|
|||
|
|
return { user: "root", key: "", host: $server }
|
|||
|
|
}
|
|||
|
|
let raw = (ncl-eval-soft $servers_path (default-ncl-paths $ws_root) null)
|
|||
|
|
if ($raw == null) {
|
|||
|
|
return { user: "root", key: "", host: $server }
|
|||
|
|
}
|
|||
|
|
let srv = ($raw | get -o servers | default [] | where {|s| ($s.hostname? | default "") == $server } | get 0?)
|
|||
|
|
let user = if ($srv | is-not-empty) { ($srv | get -o installer_user | default "root") } else { "root" }
|
|||
|
|
let key_raw = if ($srv | is-not-empty) { ($srv | get -o ssh_key_path | default "") } else { "" }
|
|||
|
|
let key = if ($key_raw | is-not-empty) { $key_raw | path expand } else { "" }
|
|||
|
|
let state_path = ($ws_root | path join "infra" $infra ".servers-state.json")
|
|||
|
|
let srv_state = if ($state_path | path exists) {
|
|||
|
|
$state_path | open | get -o $server | default {}
|
|||
|
|
} else { {} }
|
|||
|
|
let host = if ($srv | is-not-empty) {
|
|||
|
|
let np = ($srv | get -o networking.public_ip | default "")
|
|||
|
|
if ($np | is-not-empty) {
|
|||
|
|
$np
|
|||
|
|
} else {
|
|||
|
|
# floating_ip is a Hetzner API assignment name, not an SSH target.
|
|||
|
|
# Always use public_ip from state for SSH — the server's primary reachable address.
|
|||
|
|
let state_ip = ($srv_state | get -o public_ip | default "")
|
|||
|
|
if ($state_ip | is-not-empty) { $state_ip } else { $server }
|
|||
|
|
}
|
|||
|
|
} else { $server }
|
|||
|
|
{ user: $user, key: $key, host: $host }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Run a command on a remote host via SSH (with or without explicit identity file).
|
|||
|
|
# ConnectTimeout bounds the connect phase; ServerAlive (5s × 3) tears down a
|
|||
|
|
# session that goes silent after connect so a stalled remote command can never
|
|||
|
|
# hang the caller indefinitely. Callers that run kubectl wrap it in `timeout`.
|
|||
|
|
export def comp-ssh-run [user: string, key: string, host: string, cmd: string]: nothing -> record {
|
|||
|
|
let opts = [
|
|||
|
|
-o ConnectTimeout=10
|
|||
|
|
-o ServerAliveInterval=5
|
|||
|
|
-o ServerAliveCountMax=3
|
|||
|
|
-o BatchMode=yes
|
|||
|
|
]
|
|||
|
|
if ($key | is-not-empty) {
|
|||
|
|
do { ^ssh -i $key ...$opts $"($user)@($host)" $cmd } | complete
|
|||
|
|
} else {
|
|||
|
|
do { ^ssh ...$opts $"($user)@($host)" $cmd } | complete
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|