feat(ssh): jump hosts, host key checking, and configurable ssh options
Add SSH settings that apply only to netssh connections, so ~/.ssh/config does not need global changes: - proxy_jump (passed as -J; "none" disables an inherited jump host) - host_key_checking: strict | accept-new | off (off prints a notice) - target: ip (default) | name | hostname; "name" connects to the NetBox name with -o HostName=<ip> and -o HostKeyAlias=<name>, so `Host *.example.com` rules apply and known_hosts entries follow the device instead of a reused IP - address_family: any | inet | inet6 - options (-o Key=Value) and extra_args Settings layer global -> netboxes[].ssh -> ssh.rules (match by tag, instance, kind, or name glob). Options given on the command line always win: -o values are inserted after the user's own, and extra_args before them. New wrapper flags: --jump HOST, --no-jump, and --dry-run, which prints the final ssh command instead of connecting. The TUI uses the same options. The setup wizard keeps all new settings when it saves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
129a7201bc
commit
2b46c4f74d
8 files changed
+738
-19
No files matched your search
@@ -0,0 +1,90 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func loadYAML(t *testing.T, data string) (*Config, error) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", dir)
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
if err := os.WriteFile(filepath.Join(dir, "netssh.yaml"), []byte(data), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Load()
|
||||
}
|
||||
|
||||
func TestSSHOptionsLayering(t *testing.T) {
|
||||
cfg, err := loadYAML(t, `
|
||||
netboxes:
|
||||
- name: prod
|
||||
url: https://prod.example.com
|
||||
ssh:
|
||||
proxy_jump: jh-prod.example.com
|
||||
host_key_checking: strict
|
||||
- name: staging
|
||||
url: https://staging.example.com
|
||||
ssh:
|
||||
host_key_checking: off
|
||||
ssh:
|
||||
proxy_jump: jh.example.com
|
||||
extra_args: ["-4"]
|
||||
options:
|
||||
ServerAliveInterval: 60
|
||||
ServerAliveCountMax: 5
|
||||
rules:
|
||||
- match: {tag: dmz}
|
||||
proxy_jump: jh-dmz.example.com
|
||||
options: {ServerAliveInterval: 10}
|
||||
- match: {instance: prod, name: "db*"}
|
||||
proxy_jump: none
|
||||
completion:
|
||||
max_results: 200
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Completion.MaxResults != 200 {
|
||||
t.Fatalf("max_results: %d", cfg.Completion.MaxResults)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
host SSHHost
|
||||
jump, keys, alive string
|
||||
}{
|
||||
{SSHHost{Name: "web"}, "jh.example.com", "", "60"},
|
||||
{SSHHost{Instance: "prod", Name: "web"}, "jh-prod.example.com", "strict", "60"},
|
||||
{SSHHost{Instance: "staging", Name: "web", Tags: []string{"DMZ"}}, "jh-dmz.example.com", "off", "10"},
|
||||
{SSHHost{Instance: "prod", Name: "db01", Tags: []string{"dmz"}}, "none", "strict", "10"},
|
||||
} {
|
||||
got := cfg.SSHOptionsFor(tc.host)
|
||||
if got.ProxyJump != tc.jump || got.HostKeyChecking != tc.keys || got.Options["serveraliveinterval"] != tc.alive || got.Options["serveralivecountmax"] != "5" {
|
||||
t.Errorf("%+v: got %+v", tc.host, got)
|
||||
}
|
||||
if !reflect.DeepEqual(got.ExtraArgs, []string{"-4"}) {
|
||||
t.Errorf("extra args: %v", got.ExtraArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHOptionsValidation(t *testing.T) {
|
||||
for _, data := range []string{
|
||||
"ssh:\n host_key_checking: maybe\n",
|
||||
"ssh:\n target: dns\n",
|
||||
"ssh:\n address_family: ipx\n",
|
||||
"ssh:\n proxy_jump: \"-oProxyCommand=x\"\n",
|
||||
"ssh:\n options:\n \"bad key\": 1\n",
|
||||
"ssh:\n rules:\n - proxy_jump: jh\n",
|
||||
"netboxes:\n - name: a\n url: https://a.example\n ssh:\n target: nope\n",
|
||||
} {
|
||||
if _, err := loadYAML(t, data); err == nil {
|
||||
t.Errorf("accepted invalid config:\n%s", data)
|
||||
} else if !strings.Contains(err.Error(), "ssh") {
|
||||
t.Errorf("unclear error %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user