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>
143 lines
4.2 KiB
Go
143 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
|
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/hook"
|
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/setup"
|
|
)
|
|
|
|
func hostsCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "hosts",
|
|
Short: "Manage static hosts that are not in NetBox",
|
|
Long: `Static hosts are defined under hosts: in the config. They are found before
|
|
NetBox, work offline, and support shortcuts, rules (kind: static), aliases,
|
|
completion, sftp, scp, and tunnels like NetBox hosts.`,
|
|
RunE: func(cmd *cobra.Command, args []string) error { return listHosts() },
|
|
}
|
|
cmd.AddCommand(hostsListCmd(), hostsAddCmd(), hostsRemoveCmd())
|
|
return cmd
|
|
}
|
|
|
|
func hostsListCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "list",
|
|
Short: "List static hosts",
|
|
RunE: func(cmd *cobra.Command, args []string) error { return listHosts() },
|
|
}
|
|
}
|
|
|
|
func listHosts() error {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(cfg.Hosts) == 0 {
|
|
fmt.Println("No static hosts. Add one with: netssh hosts add NAME ADDRESS")
|
|
return nil
|
|
}
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(w, "NAME\tADDRESS\tUSER\tPORT\tTAGS")
|
|
for _, host := range cfg.Hosts {
|
|
port := ""
|
|
if host.Port > 0 {
|
|
port = fmt.Sprint(host.Port)
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", host.Name, host.Address, host.User, port, strings.Join(host.Tags, ","))
|
|
}
|
|
return w.Flush()
|
|
}
|
|
|
|
func hostsAddCmd() *cobra.Command {
|
|
var host config.StaticHost
|
|
var replace bool
|
|
cmd := &cobra.Command{
|
|
Use: "add NAME ADDRESS",
|
|
Short: "Add a static host (or replace one with --replace)",
|
|
Example: ` netssh hosts add router 192.168.1.1 --user admin
|
|
netssh hosts add backup backup.example.org --port 2222 --tag offsite`,
|
|
Args: cobra.ExactArgs(2),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
host.Name, host.Address = args[0], args[1]
|
|
if existing := cfg.StaticHost(host.Name); existing != nil {
|
|
if !replace {
|
|
return fmt.Errorf("static host %q already exists; use --replace to overwrite it", existing.Name)
|
|
}
|
|
host.SSH = existing.SSH // keep SSH options set in the config or wizard
|
|
*existing = host
|
|
} else {
|
|
cfg.Hosts = append(cfg.Hosts, host)
|
|
}
|
|
return saveHosts(cfg)
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&host.User, "user", "", "Login user (default: ssh.default_user)")
|
|
cmd.Flags().IntVar(&host.Port, "port", 0, "SSH port (default: ssh.default_port or 22)")
|
|
cmd.Flags().StringSliceVar(&host.Tags, "tag", nil, "Tag for ssh.rules (repeatable)")
|
|
cmd.Flags().BoolVar(&replace, "replace", false, "Overwrite an existing static host")
|
|
return cmd
|
|
}
|
|
|
|
func hostsRemoveCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "remove NAME",
|
|
Aliases: []string{"rm"},
|
|
Short: "Remove a static host",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i, host := range cfg.Hosts {
|
|
if strings.EqualFold(host.Name, args[0]) {
|
|
cfg.Hosts = append(cfg.Hosts[:i], cfg.Hosts[i+1:]...)
|
|
return saveHosts(cfg)
|
|
}
|
|
}
|
|
return fmt.Errorf("no static host %q", args[0])
|
|
},
|
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
|
cfg, err := config.Load()
|
|
if err != nil || len(args) > 0 {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
var names []string
|
|
for _, host := range cfg.Hosts {
|
|
if strings.HasPrefix(strings.ToLower(host.Name), strings.ToLower(toComplete)) {
|
|
names = append(names, host.Name)
|
|
}
|
|
}
|
|
return names, cobra.ShellCompDirectiveNoFileComp
|
|
},
|
|
}
|
|
}
|
|
|
|
// saveHosts writes the config and regenerates the shell aliases.
|
|
func saveHosts(cfg *config.Config) error {
|
|
if err := setup.Save(*cfg); err != nil {
|
|
return err
|
|
}
|
|
loaded, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c := newCache(loaded)
|
|
_ = c.Load()
|
|
if err := hook.WriteAliasFiles(hook.BuildAliasEntries(c.All(), *loaded)); err != nil {
|
|
fmt.Fprintf(os.Stderr, "warning: could not write alias files: %v\n", err)
|
|
}
|
|
return nil
|
|
}
|