feat: hostname resolver strategy and static hosts
Add the hostname resolver strategy. It connects by the NetBox name instead of an IP, with resolver.hostname_domain appended to names without a dot. Its position in the chain decides whether it is always used or only as a fallback (e.g. [primary_ip, hostname]); names that are not valid DNS hostnames fall through to the next strategy. Add static hosts under hosts: for machines that are not in NetBox, with address, user, port, tags, and their own SSH options. They are checked before NetBox (by name or shortcut), never expire, survive cache refresh and clear, and work without any NetBox configured. The cache overlays them after every load and stores only their last use, so they appear in completion, aliases, the TUI and its recent list. SSH options merge global, static host, then rules; rules can match kind: static. Manage them with netssh hosts list|add|remove and in netssh configure, which can now also continue without NetBox. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5821dfbebd
commit
a950a82552
22 files changed
+1024
-49
No files matched your search
+59
-23
@@ -45,6 +45,7 @@ var managedSubcommands = map[string]bool{
|
||||
"sftp": true,
|
||||
"scp": true,
|
||||
"tunnel": true,
|
||||
"hosts": true,
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -75,7 +76,7 @@ func loadConfigOrSetup() *config.Config {
|
||||
if err != nil {
|
||||
fatalf("config: %v", err)
|
||||
}
|
||||
if !cfg.HasNetBox() {
|
||||
if !cfg.HasHosts() {
|
||||
fmt.Fprintln(os.Stderr, "No configuration found. Starting setup…")
|
||||
if err := setup.RunWizard(cfg); err != nil {
|
||||
fatalf("setup: %v", err)
|
||||
@@ -143,16 +144,35 @@ func runClient(client internalssh.Client, flags internalssh.WrapperFlags, args [
|
||||
return
|
||||
}
|
||||
|
||||
// Inject the configured default user if none was given on the command line.
|
||||
if cfg.SSH.DefaultUser != "" && dest.user == "" && !client.HasUserFlag(args) {
|
||||
args = prependArgs(client.UserArgs(cfg.SSH.DefaultUser), args, dest.dests)
|
||||
host := resolveTarget(cfg, dest.host)
|
||||
user, port := hostDefaults(cfg, host)
|
||||
// Inject the default user if none was given on the command line.
|
||||
if user != "" && dest.user == "" && !client.HasUserFlag(args) {
|
||||
args = prependArgs(client.UserArgs(user), args, dest.dests)
|
||||
}
|
||||
// Inject the configured default port if none was given on the command line.
|
||||
if cfg.SSH.DefaultPort > 0 && !client.HasPortFlag(args) {
|
||||
args = prependArgs(client.PortArgs(strconv.Itoa(cfg.SSH.DefaultPort)), args, dest.dests)
|
||||
// Inject the default port if none was given on the command line.
|
||||
if port > 0 && !client.HasPortFlag(args) {
|
||||
args = prependArgs(client.PortArgs(strconv.Itoa(port)), args, dest.dests)
|
||||
}
|
||||
|
||||
connectHost(cfg, client, flags, resolveTarget(cfg, dest.host), dest.dests, args)
|
||||
connectHost(cfg, client, flags, host, dest.dests, args)
|
||||
}
|
||||
|
||||
// hostDefaults returns the login user and port for host: those of a static
|
||||
// host if set, else ssh.default_user and ssh.default_port.
|
||||
func hostDefaults(cfg *config.Config, host resolvedHost) (string, int) {
|
||||
user, port := cfg.SSH.DefaultUser, cfg.SSH.DefaultPort
|
||||
if host.Kind == config.StaticKind {
|
||||
if static := cfg.StaticHost(host.Name); static != nil {
|
||||
if static.User != "" {
|
||||
user = static.User
|
||||
}
|
||||
if static.Port > 0 {
|
||||
port = static.Port
|
||||
}
|
||||
}
|
||||
}
|
||||
return user, port
|
||||
}
|
||||
|
||||
// prependArgs puts extra before args and shifts the destination indices.
|
||||
@@ -174,7 +194,7 @@ type resolvedHost struct {
|
||||
// resolveTarget finds host in the cache (also by shortcut) or NetBox and
|
||||
// resolves its address. It exits on failure.
|
||||
func resolveTarget(cfg *config.Config, host string) resolvedHost {
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
c := newCache(cfg)
|
||||
_ = c.Load()
|
||||
maybeBackgroundRefresh(cfg, c)
|
||||
|
||||
@@ -186,6 +206,21 @@ func resolveTarget(cfg *config.Config, host string) resolvedHost {
|
||||
return resolvedHost{Name: entry.Name, Kind: entry.Kind, IP: entry.IP, Tags: entry.Tags}
|
||||
}
|
||||
|
||||
// Static hosts from the config win and need no NetBox.
|
||||
if source == "" {
|
||||
var normalize func(string) string
|
||||
if cfg.Shortcuts.Enabled() {
|
||||
normalize = normalizeTarget(cfg.Shortcuts, false)
|
||||
}
|
||||
entry, found, err := c.GetStatic(host, normalize)
|
||||
if err != nil {
|
||||
fatalf("%v", err)
|
||||
}
|
||||
if found {
|
||||
return used(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// Cache hit with a fresh TTL — connect directly without querying NetBox.
|
||||
if entry, fresh := c.Get(host); fresh && useCache {
|
||||
return used(entry)
|
||||
@@ -211,7 +246,7 @@ func resolveTarget(cfg *config.Config, host string) resolvedHost {
|
||||
}
|
||||
|
||||
if !cfg.HasNetBox() {
|
||||
fatalf("netbox.url is not configured (~/.config/netssh.yaml)")
|
||||
fatalf("host %q is not in hosts: and no NetBox is configured (netssh configure)", host)
|
||||
}
|
||||
nbClient := newNetBoxClient(cfg)
|
||||
|
||||
@@ -316,7 +351,7 @@ func execClient(client internalssh.Client, result internalssh.Result, flags inte
|
||||
func runTUI() {
|
||||
cfg := loadConfigOrSetup()
|
||||
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
c := newCache(cfg)
|
||||
_ = c.Load()
|
||||
maybeBackgroundRefresh(cfg, c)
|
||||
|
||||
@@ -392,17 +427,18 @@ func tuiCommand(cfg *config.Config, host *tui.SelectedHost) (internalssh.Client,
|
||||
}
|
||||
|
||||
var args []string
|
||||
// User override from TUI edit mode takes priority, then config default.
|
||||
// User override from TUI edit mode takes priority, then static host and config defaults.
|
||||
defaultUser, defaultPort := hostDefaults(cfg, resolvedHost{Name: host.Name, Kind: host.Kind})
|
||||
user := host.User
|
||||
if user == "" {
|
||||
user = cfg.SSH.DefaultUser
|
||||
user = defaultUser
|
||||
}
|
||||
if user != "" {
|
||||
args = append(args, client.UserArgs(user)...)
|
||||
}
|
||||
port := host.Port
|
||||
if port == "" && cfg.SSH.DefaultPort > 0 {
|
||||
port = strconv.Itoa(cfg.SSH.DefaultPort)
|
||||
if port == "" && defaultPort > 0 {
|
||||
port = strconv.Itoa(defaultPort)
|
||||
}
|
||||
if port != "" {
|
||||
args = append(args, client.PortArgs(port)...)
|
||||
@@ -453,7 +489,7 @@ func rootCmd() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
root.AddCommand(notesCmd(), configureCmd(), searchCmd(), cacheCmd(), aliasCmd(), hookCmd(), shellInitCmd(), versionCmd(), uninstallCmd(), sftpCmd(), scpCmd(), tunnelCmd())
|
||||
root.AddCommand(notesCmd(), configureCmd(), searchCmd(), cacheCmd(), aliasCmd(), hookCmd(), shellInitCmd(), versionCmd(), uninstallCmd(), sftpCmd(), scpCmd(), tunnelCmd(), hostsCmd())
|
||||
|
||||
// cobra builds the "completion" command lazily; force init so we can extend it.
|
||||
root.InitDefaultCompletionCmd()
|
||||
@@ -575,7 +611,7 @@ func searchCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
c := newCache(cfg)
|
||||
if err := c.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -609,7 +645,7 @@ func cacheListCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
c := newCache(cfg)
|
||||
if err := c.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -635,7 +671,7 @@ func cacheClearCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
c := newCache(cfg)
|
||||
c.Clear()
|
||||
if err := c.Save(); err != nil {
|
||||
return err
|
||||
@@ -677,7 +713,7 @@ func cacheRefreshCmd() *cobra.Command {
|
||||
return fmt.Errorf("NetBox: %w", err)
|
||||
}
|
||||
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
c := newCache(cfg)
|
||||
_ = c.Load()
|
||||
|
||||
chain, err := resolver.New(cfg.Resolver)
|
||||
@@ -762,7 +798,7 @@ Or use in a script:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
c := newCache(cfg)
|
||||
if err := c.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -838,7 +874,7 @@ func shellInitCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
c := newCache(cfg)
|
||||
_ = c.Load()
|
||||
|
||||
// Regenerate alias files from the current cache every shell start
|
||||
@@ -897,7 +933,7 @@ After installation, reload your profile or open a new shell.`,
|
||||
// Generate the aliases file immediately so it is ready on the next shell start.
|
||||
cfg, cfgErr := config.Load()
|
||||
if cfgErr == nil && cfg.HasNetBox() {
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
c := newCache(cfg)
|
||||
if _ = c.Load(); true {
|
||||
if entries := buildAliasEntries(c, *cfg); len(entries) > 0 {
|
||||
if err := hook.WriteAliasFiles(entries); err != nil {
|
||||
|
||||
Reference in new issue
Block a user