Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a03cfeb177 |
10
Dockerfile
10
Dockerfile
@ -34,6 +34,7 @@ RUN pacman -S --noconfirm --needed \
|
|||||||
go \
|
go \
|
||||||
github-cli \
|
github-cli \
|
||||||
libnewt \
|
libnewt \
|
||||||
|
openssh sshfs \
|
||||||
&& pacman -Scc --noconfirm
|
&& pacman -Scc --noconfirm
|
||||||
|
|
||||||
# ── Crown Jewel #2: AUR ───────────────────────────────────────────────────────
|
# ── Crown Jewel #2: AUR ───────────────────────────────────────────────────────
|
||||||
@ -54,11 +55,14 @@ RUN sudo -u aurbuild yay -S --noconfirm --needed \
|
|||||||
tailscale \
|
tailscale \
|
||||||
&& sudo -u aurbuild yay -Scc --noconfirm
|
&& sudo -u aurbuild yay -Scc --noconfirm
|
||||||
|
|
||||||
|
# ── FUSE config: allow_other for sshfs mounts ─────────────────────────────────
|
||||||
|
RUN sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf 2>/dev/null || \
|
||||||
|
echo 'user_allow_other' >> /etc/fuse.conf
|
||||||
|
|
||||||
# ── Dev user with host-matching UID/GID ───────────────────────────────────────
|
# ── Dev user with host-matching UID/GID ───────────────────────────────────────
|
||||||
# UID/GID match host so /workspace bind mount has clean permissions both sides.
|
# UID/GID match host so /workspace bind mount has clean permissions both sides.
|
||||||
# aurbuild is parked at UID 9001 so there's no collision with host UID.
|
# aurbuild is parked at UID 9001 so there's no collision with host UID.
|
||||||
RUN set -e; \
|
RUN set -e; \
|
||||||
# Group: handle pre-existing GID gracefully (Arch base has users:1000)
|
|
||||||
if getent group ${USER_GID} >/dev/null; then \
|
if getent group ${USER_GID} >/dev/null; then \
|
||||||
groupmod -n dev "$(getent group ${USER_GID} | cut -d: -f1)"; \
|
groupmod -n dev "$(getent group ${USER_GID} | cut -d: -f1)"; \
|
||||||
else \
|
else \
|
||||||
@ -70,6 +74,10 @@ RUN set -e; \
|
|||||||
# ── Skeleton: bake dotfiles into /etc/skel-arch-dev/ ──────────────────────────
|
# ── Skeleton: bake dotfiles into /etc/skel-arch-dev/ ──────────────────────────
|
||||||
COPY --chown=dev:dev dotfiles/ /etc/skel-arch-dev/
|
COPY --chown=dev:dev dotfiles/ /etc/skel-arch-dev/
|
||||||
|
|
||||||
|
# ── Roaming toolkit: add_new_machine.sh onto PATH ─────────────────────────────
|
||||||
|
COPY scripts/add_new_machine.sh /usr/local/bin/add_new_machine.sh
|
||||||
|
RUN chmod +x /usr/local/bin/add_new_machine.sh
|
||||||
|
|
||||||
# ── Initial seed of /home/dev so plugin bake works at build time ──────────────
|
# ── Initial seed of /home/dev so plugin bake works at build time ──────────────
|
||||||
RUN cp -an /etc/skel-arch-dev/. /home/dev/ && \
|
RUN cp -an /etc/skel-arch-dev/. /home/dev/ && \
|
||||||
chown -R dev:dev /home/dev
|
chown -R dev:dev /home/dev
|
||||||
|
|||||||
132
ROAMING.md
Normal file
132
ROAMING.md
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# Roaming ~ — arch-dev v3
|
||||||
|
|
||||||
|
Turn neovim-ide into your **roaming home directory**. SSH in from any device,
|
||||||
|
and every server on your tailnet feels like home: one keyring, one shell
|
||||||
|
history, one set of dotfiles, reachable from anywhere.
|
||||||
|
|
||||||
|
This is what makes neovim-ide more than another riced nvim install — it's a
|
||||||
|
control plane for your whole fleet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Model
|
||||||
|
|
||||||
|
```
|
||||||
|
You (phone / tablet / chromebook)
|
||||||
|
↓ ssh
|
||||||
|
nvimide.tailnet.ts ← your roaming home (this container)
|
||||||
|
↓ ssh / sshfs over tailnet
|
||||||
|
┌──────────┬──────────┬──────────┐
|
||||||
|
mail proxy dev (any host)
|
||||||
|
```
|
||||||
|
|
||||||
|
You live in nvimide. Files live on remote hosts (mounted via sshfs when you
|
||||||
|
want them). Commands run on remote hosts via short per-host functions.
|
||||||
|
|
||||||
|
Keys never leave nvimide — agent auth handles remote login, remote `sudo`
|
||||||
|
still prompts (the security boundary stays intact).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `~/.config/roaming/hosts` | Registry — one line per host (source of truth) |
|
||||||
|
| `~/.config/roaming/roaming.zsh` | Engine — generates host functions, mount helpers |
|
||||||
|
| `add_new_machine.sh` | Onboard a new host (key + register + verify) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Run commands on a host
|
||||||
|
|
||||||
|
Each registry entry becomes a command. Dumb by design — pipes and redirects
|
||||||
|
run **locally**, exactly like plain ssh (predictable, no surprises).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mail # interactive shell on mail
|
||||||
|
mail tail -f /var/log/mail.log # TTY allocated — -f works
|
||||||
|
mail sudo systemctl restart postfix # remote sudo prompts for password
|
||||||
|
proxy sudo systemctl reload caddy
|
||||||
|
dev docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mount a host's filesystem
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mount_host mail # sshfs the registered path → /workspace/mail
|
||||||
|
ls /workspace/mail/etc/postfix/
|
||||||
|
unmount_host mail # clean unmount (handles stale mounts)
|
||||||
|
```
|
||||||
|
|
||||||
|
Mounts use `reconnect,ServerAliveInterval=15,ServerAliveCountMax=3` so a
|
||||||
|
network blip errors out in ~45s instead of freezing your shell forever.
|
||||||
|
|
||||||
|
### Status & reload
|
||||||
|
|
||||||
|
```bash
|
||||||
|
roaming-status # list hosts + which are mounted
|
||||||
|
roaming-reload # re-read registry after editing hosts by hand
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add a new machine
|
||||||
|
|
||||||
|
```bash
|
||||||
|
add_new_machine.sh mail mail.tailnet.ts root /
|
||||||
|
# 1. ssh-copy-id (one password prompt)
|
||||||
|
# 2. appends to ~/.config/roaming/hosts
|
||||||
|
# 3. verifies key-based login
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, `mail` works in the next shell (or after `roaming-reload`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Registry Format
|
||||||
|
|
||||||
|
```
|
||||||
|
# name fqdn user mount
|
||||||
|
mail mail.tailnet.ts root /
|
||||||
|
proxy proxy.tailnet.ts admin /etc/caddy
|
||||||
|
dev dev-server.tailnet.ts wayne
|
||||||
|
```
|
||||||
|
|
||||||
|
- `user` optional — defaults to `$ROAMING_DEFAULT_USER` or current user
|
||||||
|
- `mount` optional — omit if you only run commands, never mount
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Onboarding a Fresh Client
|
||||||
|
|
||||||
|
The whole point: a new device needs almost nothing.
|
||||||
|
|
||||||
|
1. Install your SSH key to reach nvimide.tailnet.ts
|
||||||
|
2. SSH in — you're home. Dotfiles, history (atuin), keys, the works.
|
||||||
|
|
||||||
|
No per-device key sprawl. nvimide holds the fleet keyring; clients only need
|
||||||
|
to reach nvimide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gotchas (learned, baked in)
|
||||||
|
|
||||||
|
- **Pipes are local.** `mail tail -f log | grep err` greps locally. Want it
|
||||||
|
remote? Quote the whole pipeline: `mail 'tail -f log | grep err'`.
|
||||||
|
- **Stale mount frozen?** `fusermount3 -uz /workspace/<name>` force-clears it.
|
||||||
|
- **`tail -f` / `htop` / sudo need a TTY** — handled (`ssh -t` always).
|
||||||
|
- **Agent forwarding (`-A`) is intentionally OFF** — a compromised remote
|
||||||
|
could abuse a forwarded agent. Add per-host later only if you need to hop.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Roadmap (future hardening)
|
||||||
|
|
||||||
|
- **SSH CA + OIDC (Authelia)** — replace `authorized_keys` with short-lived
|
||||||
|
certs issued after Authelia auth. Collapses key management fleet-wide.
|
||||||
|
Deferred: keys stay as the stable fallback until the cert flow is proven.
|
||||||
|
- **autofs** — system-level lazy mount/unmount (60s idle auto-unmount) as an
|
||||||
|
alternative to manual `mount_host`.
|
||||||
|
- **atuin** — single shell-history brain in nvimide, DB in the postgres
|
||||||
|
cluster; retire atuin-server.
|
||||||
@ -3,9 +3,6 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
args:
|
args:
|
||||||
# Match host UID/GID for clean /workspace permissions
|
|
||||||
# Set via: UID=$(id -u) GID=$(id -g) docker compose build
|
|
||||||
# Or .env file in repo root
|
|
||||||
USER_UID: ${UID:-1000}
|
USER_UID: ${UID:-1000}
|
||||||
USER_GID: ${GID:-1000}
|
USER_GID: ${GID:-1000}
|
||||||
image: arch-dev:latest
|
image: arch-dev:latest
|
||||||
@ -15,11 +12,7 @@ services:
|
|||||||
tty: true
|
tty: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
# Project files — bind mount, host-visible, host-UID-owned
|
|
||||||
- ./workspace:/workspace
|
- ./workspace:/workspace
|
||||||
|
|
||||||
# Stateful home — named volume, survives --rm
|
|
||||||
# Reset with: docker volume rm <project>_arch-dev-home
|
|
||||||
- arch-dev-home:/home/dev
|
- arch-dev-home:/home/dev
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
@ -30,8 +23,7 @@ services:
|
|||||||
- GIT_COMMITTER_NAME=${GIT_NAME:-dev}
|
- GIT_COMMITTER_NAME=${GIT_NAME:-dev}
|
||||||
- GIT_COMMITTER_EMAIL=${GIT_EMAIL:-dev@localhost}
|
- GIT_COMMITTER_EMAIL=${GIT_EMAIL:-dev@localhost}
|
||||||
|
|
||||||
# Capability set built up through testing —
|
# Capability set — cap_drop ALL then re-add only what's needed
|
||||||
# cap_drop ALL then re-add only what's needed.
|
|
||||||
cap_drop:
|
cap_drop:
|
||||||
- ALL
|
- ALL
|
||||||
cap_add:
|
cap_add:
|
||||||
@ -41,13 +33,20 @@ services:
|
|||||||
- AUDIT_WRITE # sudoers_audit plugin
|
- AUDIT_WRITE # sudoers_audit plugin
|
||||||
- NET_ADMIN # tailscale
|
- NET_ADMIN # tailscale
|
||||||
- NET_RAW # tailscale
|
- NET_RAW # tailscale
|
||||||
- CHOWN # pacman temp dir ownership
|
- CHOWN # pacman
|
||||||
- DAC_OVERRIDE # pacman lock files
|
- DAC_OVERRIDE # pacman
|
||||||
- FOWNER # pacman package ownership
|
- FOWNER # pacman
|
||||||
|
- SYS_ADMIN # sshfs/FUSE mounts (roaming ~)
|
||||||
|
|
||||||
# Tailscale needs tun device for kernel-mode networking
|
# Devices: tun for tailscale, fuse for sshfs
|
||||||
devices:
|
devices:
|
||||||
- /dev/net/tun:/dev/net/tun
|
- /dev/net/tun:/dev/net/tun
|
||||||
|
- /dev/fuse:/dev/fuse
|
||||||
|
|
||||||
|
# FUSE mounts need apparmor unconfined on many hosts
|
||||||
|
# (remove if your host doesn't use apparmor)
|
||||||
|
security_opt:
|
||||||
|
- apparmor:unconfined
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
arch-dev-home:
|
arch-dev-home:
|
||||||
|
|||||||
17
dotfiles/.config/roaming/hosts
Normal file
17
dotfiles/.config/roaming/hosts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# ── arch-dev roaming :: host registry ─────────────────────────────────────────
|
||||||
|
# One host per line. Lines starting with # are ignored.
|
||||||
|
#
|
||||||
|
# Fields (whitespace-separated):
|
||||||
|
# name short alias used for the shell function (e.g. `mail`)
|
||||||
|
# fqdn tailnet hostname or any reachable host (e.g. mail.tailnet.ts)
|
||||||
|
# user ssh user (optional, defaults to $ROAMING_DEFAULT_USER or current user)
|
||||||
|
# mount remote path to mount via sshfs (optional, e.g. / or /srv)
|
||||||
|
#
|
||||||
|
# Examples (delete these, add your own):
|
||||||
|
#
|
||||||
|
# name fqdn user mount
|
||||||
|
# mail mail.tailnet.ts root /
|
||||||
|
# proxy proxy.tailnet.ts admin /etc/caddy
|
||||||
|
# dev dev-server.tailnet.ts wayne /srv
|
||||||
|
|
||||||
|
# name fqdn user mount
|
||||||
153
dotfiles/.config/roaming/roaming.zsh
Normal file
153
dotfiles/.config/roaming/roaming.zsh
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# ╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
# ║ arch-dev roaming :: engine ║
|
||||||
|
# ║ Roaming ~ over the tailnet. Source this from .zshrc. ║
|
||||||
|
# ║ ║
|
||||||
|
# ║ Reads ~/.config/roaming/hosts and generates a function per ║
|
||||||
|
# ║ host so you can run: ║
|
||||||
|
# ║ mail → interactive shell on mail ║
|
||||||
|
# ║ mail tail -f /var/log/x → run with TTY (works for -f, sudo) ║
|
||||||
|
# ║ mount_host mail → sshfs the host's mount at ║
|
||||||
|
# ║ /workspace/mail ║
|
||||||
|
# ╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
export ROAMING_DIR="${ROAMING_DIR:-$HOME/.config/roaming}"
|
||||||
|
export ROAMING_HOSTS="${ROAMING_HOSTS:-$ROAMING_DIR/hosts}"
|
||||||
|
export ROAMING_MOUNT_BASE="${ROAMING_MOUNT_BASE:-/workspace}"
|
||||||
|
export ROAMING_DEFAULT_USER="${ROAMING_DEFAULT_USER:-$USER}"
|
||||||
|
|
||||||
|
# sshfs options that prevent the dreaded indefinite-freeze on network blips.
|
||||||
|
# ServerAliveInterval=15 + CountMax=3 → I/O errors out after ~45s instead of
|
||||||
|
# hanging forever. reconnect re-establishes when the host comes back.
|
||||||
|
export ROAMING_SSHFS_OPTS="reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,follow_symlinks"
|
||||||
|
|
||||||
|
# ── Internal: resolve a host name to its registry fields ──────────────────────
|
||||||
|
# Sets globals: _RH_NAME _RH_FQDN _RH_USER _RH_MOUNT
|
||||||
|
# Returns 1 if not found.
|
||||||
|
_roaming_lookup() {
|
||||||
|
local want="$1"
|
||||||
|
[[ -f "$ROAMING_HOSTS" ]] || return 1
|
||||||
|
local name fqdn user mount
|
||||||
|
while read -r name fqdn user mount; do
|
||||||
|
[[ -z "$name" || "$name" == \#* ]] && continue
|
||||||
|
if [[ "$name" == "$want" ]]; then
|
||||||
|
_RH_NAME="$name"
|
||||||
|
_RH_FQDN="$fqdn"
|
||||||
|
_RH_USER="${user:-$ROAMING_DEFAULT_USER}"
|
||||||
|
_RH_MOUNT="$mount"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done < "$ROAMING_HOSTS"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Internal: the actual remote-exec logic ────────────────────────────────────
|
||||||
|
# No args → interactive shell. Args → run with a TTY (so tail -f / sudo work).
|
||||||
|
# "Dumb" by design: pipes/redirects run LOCALLY, exactly like plain ssh.
|
||||||
|
_roaming_exec() {
|
||||||
|
local name="$1"; shift
|
||||||
|
if ! _roaming_lookup "$name"; then
|
||||||
|
echo "roaming: unknown host '$name' (check $ROAMING_HOSTS)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local target="${_RH_USER}@${_RH_FQDN}"
|
||||||
|
if [[ $# -eq 0 ]]; then
|
||||||
|
ssh -t "$target"
|
||||||
|
else
|
||||||
|
ssh -t "$target" "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── mount_host <name> ─────────────────────────────────────────────────────────
|
||||||
|
mount_host() {
|
||||||
|
local name="$1"
|
||||||
|
if [[ -z "$name" ]]; then
|
||||||
|
echo "usage: mount_host <name>" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! _roaming_lookup "$name"; then
|
||||||
|
echo "roaming: unknown host '$name'" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -z "$_RH_MOUNT" ]]; then
|
||||||
|
echo "roaming: '$name' has no mount path in registry" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local mp="$ROAMING_MOUNT_BASE/$name"
|
||||||
|
# Already mounted?
|
||||||
|
if mountpoint -q "$mp" 2>/dev/null; then
|
||||||
|
echo "roaming: $name already mounted at $mp"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
mkdir -p "$mp"
|
||||||
|
echo "roaming: mounting ${_RH_USER}@${_RH_FQDN}:${_RH_MOUNT} → $mp"
|
||||||
|
sshfs "${_RH_USER}@${_RH_FQDN}:${_RH_MOUNT}" "$mp" \
|
||||||
|
-o "$ROAMING_SSHFS_OPTS" \
|
||||||
|
&& echo "roaming: ✓ mounted" \
|
||||||
|
|| echo "roaming: ✗ mount failed" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── unmount_host <name> ───────────────────────────────────────────────────────
|
||||||
|
unmount_host() {
|
||||||
|
local name="$1"
|
||||||
|
if [[ -z "$name" ]]; then
|
||||||
|
echo "usage: unmount_host <name>" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local mp="$ROAMING_MOUNT_BASE/$name"
|
||||||
|
if ! mountpoint -q "$mp" 2>/dev/null; then
|
||||||
|
echo "roaming: $name not mounted"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# fusermount3 first; -z (lazy) as fallback for stale mounts
|
||||||
|
fusermount3 -u "$mp" 2>/dev/null \
|
||||||
|
|| fusermount3 -uz "$mp" 2>/dev/null \
|
||||||
|
|| umount "$mp" 2>/dev/null
|
||||||
|
if mountpoint -q "$mp" 2>/dev/null; then
|
||||||
|
echo "roaming: ✗ failed to unmount $name (stale? try: fusermount3 -uz $mp)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rmdir "$mp" 2>/dev/null
|
||||||
|
echo "roaming: ✓ unmounted $name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── roaming-status ────────────────────────────────────────────────────────────
|
||||||
|
roaming-status() {
|
||||||
|
echo "── registered hosts ──"
|
||||||
|
if [[ -f "$ROAMING_HOSTS" ]]; then
|
||||||
|
local name fqdn user mount
|
||||||
|
while read -r name fqdn user mount; do
|
||||||
|
[[ -z "$name" || "$name" == \#* ]] && continue
|
||||||
|
local mp="$ROAMING_MOUNT_BASE/$name"
|
||||||
|
local mflag=""
|
||||||
|
mountpoint -q "$mp" 2>/dev/null && mflag=" [mounted: $mp]"
|
||||||
|
printf " %-10s %s@%s%s\n" "$name" "${user:-$ROAMING_DEFAULT_USER}" "$fqdn" "$mflag"
|
||||||
|
done < "$ROAMING_HOSTS"
|
||||||
|
else
|
||||||
|
echo " (no registry at $ROAMING_HOSTS)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Generate a function per host ──────────────────────────────────────────────
|
||||||
|
# After this loop, each registry entry `mail` becomes a `mail` command.
|
||||||
|
_roaming_generate() {
|
||||||
|
[[ -f "$ROAMING_HOSTS" ]] || return 0
|
||||||
|
local name fqdn user mount
|
||||||
|
while read -r name fqdn user mount; do
|
||||||
|
[[ -z "$name" || "$name" == \#* ]] && continue
|
||||||
|
# Skip if it would clobber an existing command (safety)
|
||||||
|
if command -v "$name" >/dev/null 2>&1 && ! typeset -f "$name" >/dev/null 2>&1; then
|
||||||
|
echo "roaming: skipping '$name' — conflicts with existing command" >&2
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
eval "$name() { _roaming_exec '$name' \"\$@\"; }"
|
||||||
|
done < "$ROAMING_HOSTS"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reload registry + regenerate functions (call after editing hosts)
|
||||||
|
roaming-reload() {
|
||||||
|
_roaming_generate
|
||||||
|
echo "roaming: reloaded $(grep -cvE '^\s*#|^\s*$' "$ROAMING_HOSTS" 2>/dev/null || echo 0) host(s)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate on source
|
||||||
|
_roaming_generate
|
||||||
@ -201,3 +201,7 @@ if [[ -s "$NVM_DIR/nvm.sh" ]]; then
|
|||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Roaming ~ (v3) ────────────────────────────────────────────────────────────
|
||||||
|
# Fleet-wide remote control + sshfs mounts over the tailnet.
|
||||||
|
# Registry: ~/.config/roaming/hosts Add hosts: add_new_machine.sh
|
||||||
|
[[ -f "$HOME/.config/roaming/roaming.zsh" ]] && source "$HOME/.config/roaming/roaming.zsh"
|
||||||
|
|||||||
4
roaming-zshrc.snippet
Normal file
4
roaming-zshrc.snippet
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# ── Roaming ~ (v3) ────────────────────────────────────────────────────────────
|
||||||
|
# Fleet-wide remote control + sshfs mounts over the tailnet.
|
||||||
|
# Registry: ~/.config/roaming/hosts Add hosts: add_new_machine.sh
|
||||||
|
[[ -f "$HOME/.config/roaming/roaming.zsh" ]] && source "$HOME/.config/roaming/roaming.zsh"
|
||||||
76
scripts/add_new_machine.sh
Executable file
76
scripts/add_new_machine.sh
Executable file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
# ║ arch-dev roaming :: add_new_machine ║
|
||||||
|
# ║ Onboard a new host into the roaming fleet: ║
|
||||||
|
# ║ 1. push your SSH key (ssh-copy-id) ║
|
||||||
|
# ║ 2. register it in ~/.config/roaming/hosts ║
|
||||||
|
# ║ 3. optionally test the connection + mount ║
|
||||||
|
# ╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROAMING_DIR="${ROAMING_DIR:-$HOME/.config/roaming}"
|
||||||
|
ROAMING_HOSTS="${ROAMING_HOSTS:-$ROAMING_DIR/hosts}"
|
||||||
|
ROAMING_DEFAULT_USER="${ROAMING_DEFAULT_USER:-$USER}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: add_new_machine.sh <name> <fqdn> [user] [mount]
|
||||||
|
|
||||||
|
name short alias for the host (e.g. mail)
|
||||||
|
fqdn reachable hostname (e.g. mail.tailnet.ts)
|
||||||
|
user ssh user (default: $ROAMING_DEFAULT_USER)
|
||||||
|
mount remote path to expose via sshfs (optional, e.g. / or /srv)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
add_new_machine.sh mail mail.tailnet.ts root /
|
||||||
|
add_new_machine.sh proxy proxy.tailnet.ts admin /etc/caddy
|
||||||
|
add_new_machine.sh dev dev-server.tailnet.ts wayne
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ $# -lt 2 ]] && usage
|
||||||
|
|
||||||
|
NAME="$1"
|
||||||
|
FQDN="$2"
|
||||||
|
USER_ARG="${3:-$ROAMING_DEFAULT_USER}"
|
||||||
|
MOUNT="${4:-}"
|
||||||
|
|
||||||
|
mkdir -p "$ROAMING_DIR"
|
||||||
|
touch "$ROAMING_HOSTS"
|
||||||
|
|
||||||
|
# ── Guard: already registered? ────────────────────────────────────────────────
|
||||||
|
if grep -qE "^\s*${NAME}\s" "$ROAMING_HOSTS" 2>/dev/null; then
|
||||||
|
echo "✗ '$NAME' is already in $ROAMING_HOSTS"
|
||||||
|
echo " edit the file directly to change it"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Step 1: SSH key ───────────────────────────────────────────────────────────
|
||||||
|
echo "── Step 1: pushing SSH key to ${USER_ARG}@${FQDN} ──"
|
||||||
|
echo " (you'll be prompted for the remote password once)"
|
||||||
|
if ssh-copy-id "${USER_ARG}@${FQDN}"; then
|
||||||
|
echo " ✓ key installed"
|
||||||
|
else
|
||||||
|
echo " ✗ ssh-copy-id failed — aborting, nothing registered"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Step 2: register ──────────────────────────────────────────────────────────
|
||||||
|
echo "── Step 2: registering in $ROAMING_HOSTS ──"
|
||||||
|
printf "%-10s %-26s %-8s %s\n" "$NAME" "$FQDN" "$USER_ARG" "$MOUNT" >> "$ROAMING_HOSTS"
|
||||||
|
echo " ✓ registered"
|
||||||
|
|
||||||
|
# ── Step 3: verify ────────────────────────────────────────────────────────────
|
||||||
|
echo "── Step 3: verifying connection ──"
|
||||||
|
if ssh -o BatchMode=yes -o ConnectTimeout=8 "${USER_ARG}@${FQDN}" 'echo ok' >/dev/null 2>&1; then
|
||||||
|
echo " ✓ key-based SSH works"
|
||||||
|
else
|
||||||
|
echo " ⚠ key-based SSH test failed (host may still be fine — check manually)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Done. In a new shell (or after 'roaming-reload'):"
|
||||||
|
echo " $NAME # interactive shell"
|
||||||
|
echo " $NAME sudo systemctl status # run a command"
|
||||||
|
[[ -n "$MOUNT" ]] && echo " mount_host $NAME # sshfs ${MOUNT} → /workspace/$NAME"
|
||||||
Loading…
Reference in New Issue
Block a user