Initial fleet inventory + automation
inventory.yml: 6-host source of truth (quantum, ghost, smith, architect, tank, morpheus) fleet.sh: status / update / restart / exec / deploy / pair / sync-usage / list / ssh configs/tmux.conf: canonical mobile-tuned config pushed by 'fleet deploy tmux' All multi-host ops run in parallel with grouped output; offline hosts are reported but don't halt the sweep. macOS uses brew_services + brew upgrade; linux uses systemd_user + getmoshi.app install.sh.
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
# Never commit pairing tokens.
|
||||||
|
*.token
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
secrets/
|
||||||
|
|
||||||
|
# Local-only scratch
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Canonical tmux.conf pushed to every fleet host by `fleet deploy tmux`.
|
||||||
|
# Mobile-tuned: larger scrollback, mouse, finer wheel scroll for phone use.
|
||||||
|
|
||||||
|
set -g history-limit 100000
|
||||||
|
set -g mouse on
|
||||||
|
set -g base-index 1
|
||||||
|
setw -g pane-base-index 1
|
||||||
|
set -g renumber-windows on
|
||||||
|
set -g set-titles on
|
||||||
|
set -g set-titles-string "#I: #T"
|
||||||
|
|
||||||
|
# finer mobile wheel scrolling (default 5 lines is jumpy on a phone)
|
||||||
|
bind -T root WheelUpPane send-keys -X -N 3 scroll-up
|
||||||
|
bind -T root WheelDownPane send-keys -X -N 3 scroll-down
|
||||||
|
bind -T copy-mode-vi WheelUpPane send-keys -X -N 3 scroll-up
|
||||||
|
bind -T copy-mode-vi WheelDownPane send-keys -X -N 3 scroll-down
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# fleet.sh — Moshi fleet inventory + automation.
|
||||||
|
#
|
||||||
|
# fleet status Per-host: reachable, moshi-hook version, daemon, paired.
|
||||||
|
# fleet update [host...] Upgrade moshi-hook (brew on macOS, install.sh on linux) + restart daemon.
|
||||||
|
# fleet restart [host...] Restart moshi-hook daemon.
|
||||||
|
# fleet exec '<cmd>' [host...] Run a shell command on each host; output grouped per-host.
|
||||||
|
# fleet deploy tmux [host...] Push configs/tmux.conf to ~/.tmux.conf and reload running tmux servers.
|
||||||
|
# fleet pair <token> <host> Pair a host using its secret_store from inventory.
|
||||||
|
# fleet sync-usage [host...] Force `moshi-hook usage --sync` on each host (Usage tab refresh).
|
||||||
|
# fleet list Print inventory as a table.
|
||||||
|
# fleet ssh <host> Open an interactive ssh to a host.
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# - Without [host...] filter, runs on ALL hosts in inventory.
|
||||||
|
# - All multi-host operations run in parallel; output is grouped per-host.
|
||||||
|
# - Offline hosts are reported as such; they don't halt the sweep.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
INVENTORY="$SCRIPT_DIR/inventory.yml"
|
||||||
|
CONFIGS="$SCRIPT_DIR/configs"
|
||||||
|
SSH_OPTS=(-o ConnectTimeout=8 -o BatchMode=yes -o StrictHostKeyChecking=accept-new)
|
||||||
|
|
||||||
|
# ---------- helpers ----------
|
||||||
|
|
||||||
|
die() { printf 'fleet: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
require() { command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"; }
|
||||||
|
require yq
|
||||||
|
require ssh
|
||||||
|
|
||||||
|
all_hosts() { yq -r '.hosts[].name' "$INVENTORY"; }
|
||||||
|
|
||||||
|
field() {
|
||||||
|
local host=$1 key=$2
|
||||||
|
yq -r ".hosts[] | select(.name == \"$host\") | .${key}" "$INVENTORY"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve host list: if any args, use them; else all.
|
||||||
|
resolve_hosts() {
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
all_hosts
|
||||||
|
else
|
||||||
|
for h in "$@"; do
|
||||||
|
if ! yq -e ".hosts[] | select(.name == \"$h\")" "$INVENTORY" >/dev/null 2>&1; then
|
||||||
|
die "unknown host: $h"
|
||||||
|
fi
|
||||||
|
echo "$h"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build env-prefix and binary path appropriate for the host's OS.
|
||||||
|
remote_env_prefix() {
|
||||||
|
local host=$1 os
|
||||||
|
os=$(field "$host" os)
|
||||||
|
if [ "$os" = "linux" ]; then
|
||||||
|
# systemctl --user over SSH needs XDG_RUNTIME_DIR exported.
|
||||||
|
echo 'export XDG_RUNTIME_DIR=/run/user/$(id -u); '
|
||||||
|
else
|
||||||
|
# On macOS, source ~/.zshenv so brew's PATH is set under non-interactive SSH.
|
||||||
|
echo 'source ~/.zshenv 2>/dev/null; '
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run a command on a host. Honours is_local (skips ssh).
|
||||||
|
ssh_on() {
|
||||||
|
local host=$1; shift
|
||||||
|
local cmd=$1
|
||||||
|
local user ip is_local prefix
|
||||||
|
user=$(field "$host" user)
|
||||||
|
ip=$(field "$host" ts_ip)
|
||||||
|
is_local=$(field "$host" is_local)
|
||||||
|
prefix=$(remote_env_prefix "$host")
|
||||||
|
if [ "$is_local" = "true" ]; then
|
||||||
|
bash -c "${prefix}${cmd}"
|
||||||
|
else
|
||||||
|
ssh "${SSH_OPTS[@]}" "$user@$ip" "${prefix}${cmd}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parallel-run a function-of-one-host against many hosts; print grouped output.
|
||||||
|
parallel_run() {
|
||||||
|
local fn=$1; shift
|
||||||
|
local hosts=("$@")
|
||||||
|
local tmpdir; tmpdir=$(mktemp -d)
|
||||||
|
local pids=()
|
||||||
|
for h in "${hosts[@]}"; do
|
||||||
|
( "$fn" "$h" >"$tmpdir/$h.out" 2>&1; echo $? >"$tmpdir/$h.rc" ) &
|
||||||
|
pids+=($!)
|
||||||
|
done
|
||||||
|
for p in "${pids[@]}"; do wait "$p" 2>/dev/null || true; done
|
||||||
|
for h in "${hosts[@]}"; do
|
||||||
|
printf '\n\033[1;36m=== %s ===\033[0m\n' "$h"
|
||||||
|
cat "$tmpdir/$h.out"
|
||||||
|
done
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- subcommands ----------
|
||||||
|
|
||||||
|
cmd_list() {
|
||||||
|
printf '%-12s %-10s %-18s %-8s %-9s %-15s %-13s\n' \
|
||||||
|
NAME USER TS_IP OS ARCH DAEMON STORE
|
||||||
|
printf '%-12s %-10s %-18s %-8s %-9s %-15s %-13s\n' \
|
||||||
|
------------ ---------- ------------------ -------- --------- --------------- -------------
|
||||||
|
for h in $(all_hosts); do
|
||||||
|
printf '%-12s %-10s %-18s %-8s %-9s %-15s %-13s\n' \
|
||||||
|
"$h" \
|
||||||
|
"$(field "$h" user)" \
|
||||||
|
"$(field "$h" ts_ip)" \
|
||||||
|
"$(field "$h" os)" \
|
||||||
|
"$(field "$h" arch)" \
|
||||||
|
"$(field "$h" daemon)" \
|
||||||
|
"$(field "$h" secret_store)"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
status_one() {
|
||||||
|
local host=$1
|
||||||
|
local hook; hook=$(field "$host" hook_bin)
|
||||||
|
local daemon; daemon=$(field "$host" daemon)
|
||||||
|
local daemon_check
|
||||||
|
if [ "$daemon" = "brew_services" ]; then
|
||||||
|
daemon_check='brew services list 2>/dev/null | awk "\$1==\"moshi-hook\"{print \$2}"'
|
||||||
|
else
|
||||||
|
daemon_check='systemctl --user is-active moshi-hook 2>/dev/null'
|
||||||
|
fi
|
||||||
|
ssh_on "$host" "
|
||||||
|
if ! command -v ssh >/dev/null; then :; fi
|
||||||
|
if [ ! -x '$hook' ]; then echo 'moshi-hook: NOT INSTALLED at $hook'; exit 0; fi
|
||||||
|
ver=\$('$hook' --version 2>/dev/null || echo 'pre-0.2 (no --version flag)')
|
||||||
|
paired=\$('$hook' status 2>/dev/null | awk '/^status:/{print \$2}')
|
||||||
|
display=\$('$hook' status 2>/dev/null | awk -F': *' '/^display name:/{print \$2}')
|
||||||
|
host_id=\$('$hook' status 2>/dev/null | awk '/^host id:/{print \$3}')
|
||||||
|
daemon=\$($daemon_check)
|
||||||
|
printf 'version: %s\n' \"\$ver\"
|
||||||
|
printf 'daemon: %s\n' \"\${daemon:-unknown}\"
|
||||||
|
printf 'paired: %s\n' \"\${paired:-unknown}\"
|
||||||
|
printf 'display: %s\n' \"\${display:-?}\"
|
||||||
|
printf 'host_id: %s\n' \"\${host_id:-?}\"
|
||||||
|
" || echo "UNREACHABLE (offline / ssh refused / timeout)"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
local hosts=(); while IFS= read -r h; do hosts+=("$h"); done < <(resolve_hosts "$@")
|
||||||
|
parallel_run status_one "${hosts[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
update_one() {
|
||||||
|
local host=$1
|
||||||
|
local os; os=$(field "$host" os)
|
||||||
|
local daemon; daemon=$(field "$host" daemon)
|
||||||
|
local hook; hook=$(field "$host" hook_bin)
|
||||||
|
if [ "$os" = "macos" ]; then
|
||||||
|
ssh_on "$host" "
|
||||||
|
brew update >/dev/null 2>&1
|
||||||
|
brew upgrade moshi-hook 2>&1 | tail -4
|
||||||
|
brew services restart moshi-hook 2>&1 | tail -1
|
||||||
|
sleep 2
|
||||||
|
'$hook' --version 2>&1
|
||||||
|
" || echo "UNREACHABLE"
|
||||||
|
else
|
||||||
|
ssh_on "$host" "
|
||||||
|
curl -fsSL https://getmoshi.app/install.sh | sh 2>&1 | tail -3
|
||||||
|
systemctl --user restart moshi-hook 2>&1
|
||||||
|
sleep 2
|
||||||
|
'$hook' --version 2>&1
|
||||||
|
systemctl --user is-active moshi-hook
|
||||||
|
" || echo "UNREACHABLE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_update() {
|
||||||
|
local hosts=(); while IFS= read -r h; do hosts+=("$h"); done < <(resolve_hosts "$@")
|
||||||
|
parallel_run update_one "${hosts[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_one() {
|
||||||
|
local host=$1
|
||||||
|
local daemon; daemon=$(field "$host" daemon)
|
||||||
|
if [ "$daemon" = "brew_services" ]; then
|
||||||
|
ssh_on "$host" "brew services restart moshi-hook 2>&1 | tail -1" || echo "UNREACHABLE"
|
||||||
|
else
|
||||||
|
ssh_on "$host" "systemctl --user restart moshi-hook && systemctl --user is-active moshi-hook" || echo "UNREACHABLE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_restart() {
|
||||||
|
local hosts=(); while IFS= read -r h; do hosts+=("$h"); done < <(resolve_hosts "$@")
|
||||||
|
parallel_run restart_one "${hosts[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_one() {
|
||||||
|
local host=$1
|
||||||
|
local hook; hook=$(field "$host" hook_bin)
|
||||||
|
ssh_on "$host" "'$hook' usage --sync 2>&1 | tail -2" || echo "UNREACHABLE"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_sync_usage() {
|
||||||
|
local hosts=(); while IFS= read -r h; do hosts+=("$h"); done < <(resolve_hosts "$@")
|
||||||
|
parallel_run sync_one "${hosts[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ad-hoc exec: first arg is the command, the rest are host filters
|
||||||
|
EXEC_CMD=""
|
||||||
|
exec_one() {
|
||||||
|
local host=$1
|
||||||
|
ssh_on "$host" "$EXEC_CMD" || echo "UNREACHABLE"
|
||||||
|
}
|
||||||
|
cmd_exec() {
|
||||||
|
[ $# -ge 1 ] || die "usage: fleet exec '<command>' [host...]"
|
||||||
|
EXEC_CMD=$1; shift
|
||||||
|
local hosts=(); while IFS= read -r h; do hosts+=("$h"); done < <(resolve_hosts "$@")
|
||||||
|
parallel_run exec_one "${hosts[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# deploy: only "tmux" supported for now
|
||||||
|
deploy_tmux_one() {
|
||||||
|
local host=$1
|
||||||
|
local user ip is_local
|
||||||
|
user=$(field "$host" user)
|
||||||
|
ip=$(field "$host" ts_ip)
|
||||||
|
is_local=$(field "$host" is_local)
|
||||||
|
if [ "$is_local" = "true" ]; then
|
||||||
|
cp "$CONFIGS/tmux.conf" ~/.tmux.conf
|
||||||
|
else
|
||||||
|
scp "${SSH_OPTS[@]}" -q "$CONFIGS/tmux.conf" "$user@$ip:~/.tmux.conf" || { echo "UNREACHABLE"; return; }
|
||||||
|
fi
|
||||||
|
# reload any running tmux server (no-op if none)
|
||||||
|
ssh_on "$host" "tmux ls >/dev/null 2>&1 && tmux source-file ~/.tmux.conf 2>&1 && echo 'reloaded running tmux' || echo 'no running tmux server (config still installed)'"
|
||||||
|
}
|
||||||
|
cmd_deploy() {
|
||||||
|
case "${1:-}" in
|
||||||
|
tmux)
|
||||||
|
shift
|
||||||
|
local hosts=(); while IFS= read -r h; do hosts+=("$h"); done < <(resolve_hosts "$@")
|
||||||
|
parallel_run deploy_tmux_one "${hosts[@]}"
|
||||||
|
;;
|
||||||
|
*) die "usage: fleet deploy tmux [host...]" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_pair() {
|
||||||
|
[ $# -eq 2 ] || die "usage: fleet pair <token> <host>"
|
||||||
|
local token=$1 host=$2
|
||||||
|
local hook store
|
||||||
|
hook=$(field "$host" hook_bin)
|
||||||
|
store=$(field "$host" secret_store)
|
||||||
|
printf '\n\033[1;36m=== %s ===\033[0m\n' "$host"
|
||||||
|
ssh_on "$host" "'$hook' pair --token '$token' --store '$store' 2>&1; '$hook' install 2>&1 | tail -5"
|
||||||
|
printf 'restarting daemon...\n'
|
||||||
|
restart_one "$host"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_ssh() {
|
||||||
|
[ $# -eq 1 ] || die "usage: fleet ssh <host>"
|
||||||
|
local host=$1
|
||||||
|
local user ip; user=$(field "$host" user); ip=$(field "$host" ts_ip)
|
||||||
|
exec ssh "$user@$ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- dispatch ----------
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-status}" in
|
||||||
|
status) shift; cmd_status "$@" ;;
|
||||||
|
update) shift; cmd_update "$@" ;;
|
||||||
|
restart) shift; cmd_restart "$@" ;;
|
||||||
|
exec) shift; cmd_exec "$@" ;;
|
||||||
|
deploy) shift; cmd_deploy "$@" ;;
|
||||||
|
pair) shift; cmd_pair "$@" ;;
|
||||||
|
sync-usage) shift; cmd_sync_usage "$@" ;;
|
||||||
|
list) shift; cmd_list ;;
|
||||||
|
ssh) shift; cmd_ssh "$@" ;;
|
||||||
|
-h|--help|help) usage ;;
|
||||||
|
*) usage; exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Moshi remote-agent fleet inventory.
|
||||||
|
#
|
||||||
|
# Source of truth for the fleet.sh tooling. Edit this when adding/removing
|
||||||
|
# hosts or changing their bootstrap details. Live state (online/offline,
|
||||||
|
# moshi-hook version, daemon up) is queried at runtime — do NOT cache it here.
|
||||||
|
#
|
||||||
|
# Fields:
|
||||||
|
# name short name (used as fleet.sh argument and ssh display)
|
||||||
|
# user ssh username on the host
|
||||||
|
# ts_ip Tailscale IP (preferred over MagicDNS — survives DNS hiccups)
|
||||||
|
# ts_name Tailscale MagicDNS short name (used by Easy Pair QR)
|
||||||
|
# os macos | linux
|
||||||
|
# arch arm64 | x86_64
|
||||||
|
# brew_prefix /opt/homebrew | /usr/local | "" (linux)
|
||||||
|
# hook_bin absolute path to moshi-hook on that host
|
||||||
|
# daemon brew_services | systemd_user
|
||||||
|
# secret_store keychain | file (moshi-hook --store)
|
||||||
|
# is_local true on the host running fleet.sh; skips ssh
|
||||||
|
# notes free-form
|
||||||
|
|
||||||
|
hosts:
|
||||||
|
- name: quantum
|
||||||
|
user: quantum
|
||||||
|
ts_ip: 100.90.165.125
|
||||||
|
ts_name: quantum
|
||||||
|
os: macos
|
||||||
|
arch: arm64
|
||||||
|
brew_prefix: /opt/homebrew
|
||||||
|
hook_bin: /opt/homebrew/bin/moshi-hook
|
||||||
|
daemon: brew_services
|
||||||
|
secret_store: keychain
|
||||||
|
is_local: true
|
||||||
|
notes: "M5 MacBook Pro — control plane host"
|
||||||
|
|
||||||
|
- name: ghost
|
||||||
|
user: osobh
|
||||||
|
ts_ip: 100.79.33.78
|
||||||
|
ts_name: ghost
|
||||||
|
os: macos
|
||||||
|
arch: arm64
|
||||||
|
brew_prefix: /opt/homebrew
|
||||||
|
hook_bin: /opt/homebrew/bin/moshi-hook
|
||||||
|
daemon: brew_services
|
||||||
|
secret_store: file
|
||||||
|
is_local: false
|
||||||
|
notes: "MBP arm64 — pair with --store file (keychain locks over SSH)"
|
||||||
|
|
||||||
|
- name: smith
|
||||||
|
user: builder
|
||||||
|
ts_ip: 100.127.18.60
|
||||||
|
ts_name: smith
|
||||||
|
os: macos
|
||||||
|
arch: x86_64
|
||||||
|
brew_prefix: /usr/local
|
||||||
|
hook_bin: /usr/local/bin/moshi-hook
|
||||||
|
daemon: brew_services
|
||||||
|
secret_store: file
|
||||||
|
is_local: false
|
||||||
|
notes: "Intel MBP — pair with --store file"
|
||||||
|
|
||||||
|
- name: architect
|
||||||
|
user: osobh
|
||||||
|
ts_ip: 100.104.171.32
|
||||||
|
ts_name: architect
|
||||||
|
os: linux
|
||||||
|
arch: x86_64
|
||||||
|
brew_prefix: ""
|
||||||
|
hook_bin: /home/osobh/.local/bin/moshi-hook
|
||||||
|
daemon: systemd_user
|
||||||
|
secret_store: file
|
||||||
|
is_local: false
|
||||||
|
notes: "Linux — needs XDG_RUNTIME_DIR=/run/user/$(id -u) over SSH"
|
||||||
|
|
||||||
|
- name: tank
|
||||||
|
user: osobh
|
||||||
|
ts_ip: 100.108.129.81
|
||||||
|
ts_name: tank
|
||||||
|
os: linux
|
||||||
|
arch: x86_64
|
||||||
|
brew_prefix: ""
|
||||||
|
hook_bin: /home/osobh/.local/bin/moshi-hook
|
||||||
|
daemon: systemd_user
|
||||||
|
secret_store: file
|
||||||
|
is_local: false
|
||||||
|
notes: ""
|
||||||
|
|
||||||
|
- name: morpheus
|
||||||
|
user: osobh
|
||||||
|
ts_ip: 100.123.224.84
|
||||||
|
ts_name: morpheus
|
||||||
|
os: linux
|
||||||
|
arch: x86_64
|
||||||
|
brew_prefix: ""
|
||||||
|
hook_bin: /home/osobh/.local/bin/moshi-hook
|
||||||
|
daemon: systemd_user
|
||||||
|
secret_store: file
|
||||||
|
is_local: false
|
||||||
|
notes: ""
|
||||||
Reference in New Issue
Block a user