sync: auto-sync from HOWARD-HOME at 2026-06-24 17:37:00
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-06-24 17:37:00
This commit is contained in:
79
.claude/skills/synology/scripts/syno-ssh.sh
Normal file
79
.claude/skills/synology/scripts/syno-ssh.sh
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# syno-ssh.sh — SSH backend for a Synology NAS: the `syno*` CLI surface the DSM Web
|
||||
# API does NOT expose (filesystem ACLs, low-level share/user/group internals, package
|
||||
# CLI). Pairs with scripts/syno_client.py (the Web API surface). Read recipes run
|
||||
# freely; arbitrary `run "<cmd>"` is gated behind --confirm.
|
||||
#
|
||||
# REQUIRES: SSH enabled on the NAS (DSM > Terminal & SNMP) + L3 reach. Cascades NAS is
|
||||
# 192.168.0.120 — bring up the site VPN first. Privileged recipes use `sudo -S` with the
|
||||
# vaulted admin password.
|
||||
# AUTH (password): sshpass if installed, else OpenSSH SSH_ASKPASS fallback. On Windows the
|
||||
# askpass fallback needs MSYS/Git-bash ssh on PATH (system OpenSSH can't exec a shell askpass).
|
||||
#
|
||||
# Recipes (read): info | shares | users | groups | acl <share> | df | packages | services
|
||||
# reboot | shutdown [--confirm] power via synoshutdown -r|-s (Web-API 103 fallback)
|
||||
# run "<cmd>" [--confirm] arbitrary command (gated; privileged -> prepend `sudo -S`)
|
||||
#
|
||||
# Usage: bash .claude/skills/synology/scripts/syno-ssh.sh <recipe> [args] [--confirm]
|
||||
# [--vault clients/<x>/synology-...sops.yaml]
|
||||
set -uo pipefail
|
||||
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
|
||||
VAULT="$REPO/.claude/scripts/vault.sh"
|
||||
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "synology/ssh" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
|
||||
|
||||
VP="clients/cascades-tucson/synology-cascadesds.sops.yaml"; CONFIRM=0; POS=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--vault) VP="${2:?--vault needs a path}"; shift 2;;
|
||||
--confirm) CONFIRM=1; shift;;
|
||||
*) POS+=("$1"); shift;;
|
||||
esac
|
||||
done
|
||||
RECIPE="${POS[0]:-}"; [ -n "$RECIPE" ] || { echo "usage: syno-ssh.sh <info|shares|users|groups|acl <share>|df|packages|services|reboot|shutdown|run \"<cmd>\"> [--confirm]"; exit 2; }
|
||||
|
||||
H="$(bash "$VAULT" get-field "$VP" host 2>/dev/null)"
|
||||
U="$(bash "$VAULT" get-field "$VP" credentials.username 2>/dev/null)"
|
||||
P="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null)"
|
||||
[ -n "$H" ] && [ -n "$U" ] && [ -n "$P" ] || { echo "[BLOCKED] no Synology cred at vault:$VP (need host/credentials.username/credentials.password)"; exit 2; }
|
||||
|
||||
SSH_OPTS=(-o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
|
||||
-o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1)
|
||||
if command -v sshpass >/dev/null 2>&1; then
|
||||
run_ssh() { local rc; SSHPASS="$P" sshpass -e ssh "${SSH_OPTS[@]}" "$@" || { rc=$?; [ "$rc" = 255 ] && logerr "syno SSH connect/auth failed (rc=255)" "host=$H vp=$VP"; return $rc; }; }
|
||||
else
|
||||
ASKPASS="$(mktemp)"; printf '#!/bin/sh\nprintf "%%s\\n" "$SYNO_SSH_PW"\n' > "$ASKPASS"; chmod +x "$ASKPASS"
|
||||
trap 'rm -f "$ASKPASS"' EXIT
|
||||
run_ssh() { local rc; SYNO_SSH_PW="$P" SSH_ASKPASS="$ASKPASS" SSH_ASKPASS_REQUIRE=force DISPLAY="${DISPLAY:-:0}" ssh "${SSH_OPTS[@]}" "$@" || { rc=$?; [ "$rc" = 255 ] && logerr "syno SSH connect/auth failed (rc=255)" "host=$H vp=$VP"; return $rc; }; }
|
||||
fi
|
||||
|
||||
# privileged remote command: feed the admin password to `sudo -S`
|
||||
priv() { run_ssh "$U@$H" "echo '$P' | sudo -S -p '' $1" 2>&1 | grep -v '^Password:' ; }
|
||||
plain() { run_ssh "$U@$H" "$1" 2>&1 | grep -viE 'Permanently added'; }
|
||||
|
||||
case "$RECIPE" in
|
||||
info) plain 'uname -a; echo; cat /etc/synoinfo.conf 2>/dev/null | grep -iE "^(productversion|buildnumber|unique|upnpmodelname)" ; echo; cat /proc/meminfo | head -1';;
|
||||
df) plain 'df -h | grep -E "Filesystem|/volume"';;
|
||||
shares) priv 'synoshare --enum ALL';;
|
||||
users) priv 'synouser --list local || synouser --enum local';;
|
||||
groups) priv 'synogroup --list || synogroup --enum local';;
|
||||
packages) plain 'synopkg list 2>/dev/null || ls /var/packages';;
|
||||
services) priv 'synoservice --list 2>/dev/null | head -80';;
|
||||
acl)
|
||||
SHARE="${POS[1]:?acl needs a share name, e.g. acl Server}"
|
||||
priv "synoacltool -get /volume1/$SHARE";;
|
||||
reboot)
|
||||
[ "$CONFIRM" = "1" ] || { echo "[BLOCKED] reboot the NAS — re-run with --confirm"; exit 2; }
|
||||
echo "[INFO] rebooting $H via synoshutdown -r (Web-API 103 fallback)"; priv 'synoshutdown -r';;
|
||||
shutdown)
|
||||
[ "$CONFIRM" = "1" ] || { echo "[BLOCKED] shut down the NAS — re-run with --confirm"; exit 2; }
|
||||
echo "[INFO] shutting down $H via synoshutdown -s"; priv 'synoshutdown -s';;
|
||||
run)
|
||||
CMD="${POS[1]:?run needs a quoted command}"
|
||||
[ "$CONFIRM" = "1" ] || { echo "[BLOCKED] 'run' executes an arbitrary command on the NAS — re-run with --confirm"; echo " would run: $CMD"; exit 2; }
|
||||
echo "[INFO] running on $U@$H: $CMD"
|
||||
plain "$CMD";;
|
||||
*) echo "[ERROR] unknown recipe: $RECIPE"; exit 2;;
|
||||
esac
|
||||
rc=$?
|
||||
[ "$rc" -ne 0 ] && logerr "syno-ssh recipe '$RECIPE' failed (rc=$rc)" "host=$H"
|
||||
exit $rc
|
||||
Reference in New Issue
Block a user