net-tools/bin/host-apply
Natalie 03e47fc4df feat(@tools/net-tools): add mesh/lan tooling with host renderers
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-09 19:53:08 -07:00

151 lines
5.9 KiB
Bash
Executable file

#!/bin/sh
# host-apply — render THIS device's view of the fleet from data/mesh-hosts.json.
#
# Unlike the other renderers (which emit a uniform artifact), host-apply detects
# which host it runs on and computes addresses from THAT device's vantage point:
#
# ssh HostName for self→target =
# target.public if the target has a public IP (robust, always up)
# target.lan elif target has a LAN IP AND self can reach the LAN
# (self has a LAN IP, or self is the roaming laptop —
# the wg tunnel routes the LAN /24, and the
# smart-lan-router daemon makes it direct when home)
# target.wg else (mesh-only)
#
# It writes a single managed block (Host <name> <aliases> → HostName/User) to the
# invoking user's ~/.ssh/config, placed at the TOP so it wins first-match over
# any hand-maintained stanzas. Old names are kept as Host aliases (alias-first).
#
# Self is identified by matching the box's hostname/short-name or any local IPv4
# (incl. the wg IP) against hosts[].{name,aliases,lan,wg}.
#
# Usage:
# host-apply # --ssh-print : print this device's ssh block (default)
# host-apply --ssh-diff # diff against current ~/.ssh/config
# host-apply --ssh-apply # splice/replace the managed block (backs up first)
# host-apply --whoami # just print which host this device resolves to
#
# Companion (run separately, needs root): `mesh-hosts-render --install` writes
# this device's /etc/hosts view (the .wg/.lan names). Together they cover a
# device's ssh + hosts views from the one source of truth.
set -eu
mode=ssh-print
case "${1:-}" in
""|--ssh-print) mode=ssh-print ;;
--ssh-diff) mode=ssh-diff ;;
--ssh-apply) mode=ssh-apply ;;
--whoami) mode=whoami ;;
*) echo "host-apply: unknown arg '$1'" >&2; exit 1 ;;
esac
BEGIN='# >>> net-tools fleet (managed by host-apply) — do not edit by hand'
END='# <<< net-tools fleet'
SSH_CONFIG="$HOME/.ssh/config"
# --- locate data file (symlink-resolving walk) ---------------------------------
self_path=$0
while [ -L "$self_path" ]; do
link=$(readlink "$self_path")
case $link in /*) self_path=$link ;; *) self_path=$(dirname "$self_path")/$link ;; esac
done
root=$(cd "$(dirname "$self_path")" && pwd)
while [ "$root" != "/" ] && [ ! -f "$root/data/mesh-hosts.json" ]; do root=$(dirname "$root"); done
data_file="$root/data/mesh-hosts.json"
[ -f "$data_file" ] || { echo "host-apply: cannot locate data/mesh-hosts.json" >&2; exit 1; }
command -v jq >/dev/null || { echo "host-apply: jq not installed" >&2; exit 1; }
# Overlay: current LAN IPs discovered by the daemon (data/lan-state.json, a
# {name: ip} map) override the static `lan` seed, so ssh tracks DHCP drift.
overlay='{}'
state_file="$root/data/lan-state.json"
if [ -f "$state_file" ] && jq -e . "$state_file" >/dev/null 2>&1; then
overlay=$(cat "$state_file")
fi
# --- identify self -------------------------------------------------------------
short=$(hostname 2>/dev/null | cut -d. -f1)
[ -n "$short" ] || short=$(uname -n | cut -d. -f1)
if command -v ip >/dev/null 2>&1; then
local_ips=$(ip -o -4 addr show 2>/dev/null | awk '{print $4}' | cut -d/ -f1)
else
local_ips=$(ifconfig 2>/dev/null | awk '/inet /{print $2}')
fi
ips_json=$(printf '%s\n' $local_ips | jq -R . | jq -s .)
self=$(jq -r --arg h "$short" --argjson ips "$ips_json" '
[ .hosts[]
| . as $x
| select( ($x.name == $h)
or ($x.aliases | index($h))
or ($x.lan != null and ($ips | index($x.lan)))
or ($ips | index($x.wg)) )
| $x.name ] | first // empty
' "$data_file")
[ -n "$self" ] || { echo "host-apply: could not identify this host (short=$short, ips=$local_ips) in mesh-hosts.json" >&2; exit 1; }
if [ "$mode" = "whoami" ]; then
echo "$self"
exit 0
fi
# self_reaches_lan: a host with its own LAN IP, or the roaming laptop (tunnel
# routes 10.0.0.0/24; the daemon makes it direct when home).
reachlan=$(jq -r --arg s "$self" '
.hosts[] | select(.name == $s)
| ((.lan != null) or (.class == "laptop"))
' "$data_file")
# --- render this device's ssh block --------------------------------------------
render_block() {
printf '%s\n' "$BEGIN"
printf '# rendered for: %s (vantage: %s)\n' "$self" \
"$( [ "$reachlan" = "true" ] && echo 'LAN-capable → prefer .lan' || echo 'mesh-only → prefer .wg' )"
jq -r --arg s "$self" --argjson reachlan "$reachlan" --argjson ov "$overlay" '
.hosts[]
| select(.name != $s)
| . as $h
| (($ov[$h.name]) // $h.lan) as $lan
| ( $h.public
// (if $reachlan and $lan != null then $lan else null end)
// $h.wg ) as $addr
| "\nHost \(([$h.name] + $h.aliases) | join(" "))\n HostName \($addr)\n User \($h.ssh_user // "lilith")"
' "$data_file"
printf '\n%s\n' "$END"
}
block=$(render_block)
if [ "$mode" = "ssh-print" ]; then
printf '%s\n' "$block"
exit 0
fi
# Strip any existing managed block, then prepend the fresh one (top = wins).
current=""
[ -f "$SSH_CONFIG" ] && current=$(cat "$SSH_CONFIG")
stripped=$(printf '%s\n' "$current" | awk -v b="$BEGIN" -v e="$END" '
$0 == b { skip = 1 } skip != 1 { print } $0 == e { skip = 0 }')
new=$(printf '%s\n\n%s\n' "$block" "$stripped")
if [ "$mode" = "ssh-diff" ]; then
if command -v diff >/dev/null 2>&1; then
printf '%s\n' "$new" | diff -u "${SSH_CONFIG:-/dev/null}" - || true
else
printf '%s\n' "$new"
fi
exit 0
fi
# --ssh-apply
if [ -f "$SSH_CONFIG" ] && printf '%s\n' "$new" | cmp -s - "$SSH_CONFIG"; then
echo "host-apply: $SSH_CONFIG already up to date for $self"
exit 0
fi
mkdir -p "$HOME/.ssh"; chmod 700 "$HOME/.ssh"
[ -f "$SSH_CONFIG" ] && cp "$SSH_CONFIG" "$SSH_CONFIG.netbak"
printf '%s\n' "$new" > "$SSH_CONFIG"
chmod 600 "$SSH_CONFIG"
echo "host-apply: wrote $self's fleet block to $SSH_CONFIG (backup: $SSH_CONFIG.netbak)"