diff --git a/CHANGELOG.md b/CHANGELOG.md index de01afc..1e95b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## v0.0.21 + +- Ship a zsh completion script built for large caches: 5000 hosts complete in under 0.1 s instead of hanging for seconds with Cobra's generated script. +- Add `completion.max_results` to limit the number of hostnames offered per completion. +- Register zsh completion in the managed `.zshrc` block (`fpath` and `compdef`), also when `compinit` already ran, e.g. with oh-my-zsh. The installer reports which completions it set up. +- Add `netssh uninstall [--keep-config] [--dry-run] [--yes]` to remove the binary, profile entries of all versions, completions, cache, and config (with a private backup). +- Write marked `# >>> netssh >>>` blocks from `netssh hook install` and the wizard, replacing unmarked entries of older versions instead of appending. +- Add jump host support: `ssh.proxy_jump` globally, per NetBox instance, and per rule, plus `--jump HOST` and `--no-jump`. +- Add `ssh.host_key_checking` (`strict`, `accept-new`, `off`) for netssh connections only. +- Add `ssh.target: name|hostname` so `~/.ssh/config` host patterns apply and known_hosts entries belong to the device, and `ssh.address_family`. +- Add `ssh.options` and `ssh.extra_args`, and `ssh.rules` that match by tag, instance, kind, or name pattern. +- Add `netssh --version` / `netssh version` and `netssh --dry-run` to print the final ssh command. + ## v0.0.20 - Configure multiple named NetBox instances and search them together in parallel. diff --git a/README.md b/README.md index d332cc3..4466e07 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ netssh -p 2222 admin@app-server-03 uptime - **Host shortcuts** — type `web01` instead of `web01.example.com`; configurable domain strip and hyphen folding - **Shell aliases** — `netssh alias` generates shell aliases for all cached hosts - **Automatic 24h cache refresh** — the cache is refreshed in the background every 24 hours +- **Jump hosts and SSH options** — `proxy_jump`, host key checking, connection target, and free `-o` options per netssh, per NetBox instance, or per tag/name rule — without touching `~/.ssh/config` +- **Dry run** — `netssh --dry-run host` prints the final `ssh` command instead of connecting +- **Clean uninstall** — `netssh uninstall` removes the binary, profile entries of all versions, completions, cache, and config ## Installation @@ -135,6 +138,11 @@ cache: ssh: default_user: admin # used when no user is specified on the command line default_port: 2222 # optional; injected as -p if not specified on the command line + # See "SSH options" below for proxy_jump, host_key_checking, target, + # address_family, extra_args, options, and rules. + +completion: + max_results: 0 # hostnames offered per ; 0 = unlimited shortcuts: domains: # strip these suffixes to create short aliases @@ -240,6 +248,74 @@ netssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no db-primary The process is replaced by `ssh` with the resolved IP — your `~/.ssh/config`, agent, and keys all work as normal. +### Dry run and version + +```sh +netssh --dry-run web01 # print the final ssh command instead of connecting +netssh --version # or: netssh version +``` + +Long options (`--dry-run`, `--jump`, `--no-jump`) are netssh's own; OpenSSH has +no long options. They must come before the hostname — everything after the +hostname is the remote command. + +### SSH options + +All settings below go under `ssh:`. The same keys (except `rules`) can be set +per NetBox instance under `netboxes[].ssh` (or `netbox.ssh`), and per rule. +More specific settings win: global → instance → matching rules (in order) → +command-line flags. Options you pass on the command line always take precedence +over the config. + +```yaml +ssh: + proxy_jump: "jumphost.example.com" # passed as -J; "none" disables an inherited jump host + host_key_checking: accept-new # strict | accept-new | off; unset = ssh's own setting + target: ip # ip (default) | name | hostname + address_family: inet # any | inet (-4) | inet6 (-6) + extra_args: ["-A"] # inserted before your own arguments + options: # passed as -o Key=Value + ServerAliveInterval: 60 + ServerAliveCountMax: 5 + rules: # every set match field must match + - match: {tag: dmz} # tag name or slug + proxy_jump: "dmz-jump.example.com" + - match: {instance: lab, name: "*.lab.example.com", kind: vm} + host_key_checking: off + +netboxes: + - name: prod + url: "https://netbox.example.com" + token: "nbt_…" + ssh: + proxy_jump: "jumphost.example.com" + - name: staging + url: "https://netbox-staging.example.com" + token: "nbt_…" + ssh: + host_key_checking: off +``` + +**Jump hosts.** Override the configured jump host for a single call with +`--jump other.example.com` (or ssh's own `-J`), or disable all jump hosts — +including `ProxyJump` from `~/.ssh/config` — with `--no-jump`. + +**Host key checking.** `strict` sets `StrictHostKeyChecking=yes`, `accept-new` +accepts keys of new hosts but rejects changed ones. `off` sets +`StrictHostKeyChecking=no`, `UserKnownHostsFile=/dev/null`, and `LogLevel=ERROR` +for netssh connections only and prints a short notice. Use it only for hosts +that are reinstalled often, such as staging; it disables protection against +man-in-the-middle attacks. + +**Connection target.** By default netssh calls `ssh `. With `target: name`, +it calls `ssh -o HostName= -o HostKeyAlias= `: the resolved IP +is still used, but `Host *.example.com` rules in `~/.ssh/config` apply, and +known_hosts entries belong to the device instead of an IP that may be reused. +With `target: hostname`, ssh resolves the NetBox name itself (DNS or +`~/.ssh/config`). Names that are not valid hostnames fall back to the IP. +Switching to `name` stores new known_hosts entries under the device name, so +ssh asks once to confirm each host key. + ### Default username Set `ssh.default_user` in the config to avoid typing a username every time: @@ -422,7 +498,7 @@ netssh hook install --shell zsh netssh hook install --shell fish ``` -This appends a single line (`netssh shell-init`) to your shell profile. To remove it: +This adds a marked block (`# >>> netssh >>>` … `# <<< netssh <<<`) to your shell profile. To remove it: ```sh netssh hook uninstall @@ -465,14 +541,20 @@ netssh completion install --shell fish | zsh | `~/.zfunc/_netssh` | | fish | `~/.config/fish/completions/netssh.fish` | -For zsh, make sure `~/.zfunc` is in your `fpath` (add to `~/.zshrc`): +For zsh, the netssh block in `~/.zshrc` (written by the installer and by +`netssh hook install --shell zsh`) adds `~/.zfunc` to `fpath` and registers the +completion with `compdef`, also when `compinit` already ran earlier — for +example via oh-my-zsh. Without the block, add `fpath=(~/.zfunc $fpath)` before +`compinit`. If `` still completes files, clear the completion cache: ```zsh -fpath=(~/.zfunc $fpath) -autoload -Uz compinit && compinit +rm -f ~/.zcompdump* && exec zsh ``` Completions are served from the local cache — no network request on every ``. +The zsh script that `netssh completion zsh` prints is built for large caches: +5000 hosts complete in well under a second. To limit the number of hostnames +offered, set `completion.max_results`. ## Shell Hook @@ -502,15 +584,25 @@ Reload with: source /home/user/.bashrc What gets written to the profile: -**bash / zsh** (`~/.bashrc` or `~/.zshrc`): +**bash** (`~/.bashrc`): ```bash -netssh shell-init # netssh cache auto-refresh -source ~/.cache/netssh/aliases.sh 2>/dev/null # netssh aliases +# >>> netssh >>> +netssh shell-init +[ ! -f '/home/user/.cache/netssh/aliases.sh' ] || . '/home/user/.cache/netssh/aliases.sh' +# <<< netssh <<< +``` + +**zsh** (`~/.zshrc`) additionally registers the completion: +```zsh +fpath=('/home/user/.zfunc' $fpath) +if (( $+functions[compdef] )); then autoload -Uz _netssh && compdef _netssh netssh; fi ``` **fish** (`~/.config/fish/config.fish`): ```fish -netssh shell-init # netssh cache auto-refresh +# >>> netssh >>> +netssh shell-init +# <<< netssh <<< ``` Fish alias file (`~/.config/fish/conf.d/netssh.fish`) is written automatically and sourced by fish on every shell start without any profile changes. @@ -521,7 +613,7 @@ Fish alias file (`~/.config/fish/conf.d/netssh.fish`) is written automatically a | zsh | `~/.zshrc` | `~/.cache/netssh/aliases.sh` | | fish | `~/.config/fish/config.fish` | `~/.config/fish/conf.d/netssh.fish` | -The install is idempotent — running it again changes nothing if the lines are already present. +The install is idempotent — running it again changes nothing if the block is already present. Unmarked lines written by older versions are replaced by the block. Reload your profile after installation to activate immediately: @@ -550,7 +642,7 @@ netssh hook uninstall # auto-detects $SHELL netssh hook uninstall --shell zsh ``` -Removes both the `netssh shell-init` line and the `source aliases.sh` line from the profile. The alias files (`aliases.sh`, `netssh.fish`) are left on disk — delete them manually if desired. +Removes the netssh block and unmarked entries of older versions from the profile. The alias files (`aliases.sh`, `netssh.fish`) are left on disk. To remove netssh completely, use `netssh uninstall`. ### Relation to the connect-time auto-refresh @@ -563,6 +655,24 @@ The shell hook and the connect-time trigger (which fires on each `netssh` invoca Both are non-blocking — the refresh runs as a background process and never delays your prompt or SSH connection. They share `~/.cache/netssh/last_refresh`, so only one background refresh runs per 24-hour window regardless of how many shells or connections are opened. +## Uninstall + +```sh +netssh uninstall --dry-run # show what would be removed +netssh uninstall # asks for confirmation +netssh uninstall --keep-config # keep config and notes for a reinstall +``` + +`netssh uninstall` removes: + +- netssh blocks and unmarked entries of older versions from `~/.bashrc`, `~/.bash_profile`, `~/.bash_login`, `~/.profile`, `~/.zshrc`, `~/.zprofile`, `~/.zshenv`, and `config.fish` +- completion files for bash, zsh (also the oh-my-zsh cache), fish, and the command alias +- the cache directory `~/.cache/netssh/` including per-instance caches and aliases +- `~/.config/netssh.yaml` (a private backup `netssh.yaml.backup-` is kept, because it contains API tokens) and local notes +- the netssh binary; if it is in a protected directory, netssh prints the `sudo rm` command to run + +Afterwards open a new terminal; for zsh also run `rm -f ~/.zcompdump*`. + ## Development Renovate monitors the release workflow's actions, the Go version in `go.mod`, and @@ -582,9 +692,9 @@ This is a **vibe-coded** project: the entire codebase — architecture, implemen ## How it works -1. `netssh` checks whether the first argument is a known subcommand (`configure`, `search`, `cache`, `completion`). If not, it enters SSH wrapper mode. +1. `netssh` checks whether the first argument is a known subcommand (`configure`, `search`, `cache`, `completion`, `version`, `uninstall`, …). If not, it enters SSH wrapper mode. 2. On first run or when `netbox.url` is empty, the interactive setup wizard starts automatically. 3. It parses the SSH arguments to extract the destination hostname, handling all flags that consume an extra argument (`-p`, `-i`, `-J`, …). 4. It resolves the destination: first an exact cache lookup, then shortcut expansion (domain append, hyphen folding) against the cache, and finally a fresh NetBox query with the expanded name. If the cache entry is within the TTL, no network request is made. 5. Otherwise it queries NetBox (`/api/dcim/devices/` and `/api/virtualization/virtual-machines/` in parallel), runs the result through the resolver chain, and caches the IP. -6. It calls `syscall.Exec` to replace itself with `ssh`, substituting the hostname with the resolved IP. +6. It adds the configured SSH options for the host (jump host, host key checking, `-o` options) before the destination and calls `syscall.Exec` to replace itself with `ssh`, substituting the hostname with the resolved IP (or the name, with `ssh.target: name`).