deploy-remote.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
# BoardRipper NAS Remote Deploy Script
# This script runs ON the NAS via SSH. It is SCP'd by NASdeploy.sh.
# Arguments: $1 = sudo password
set -e
DOCKER="/usr/local/bin/docker"
PW="$1"
IMAGE_NAME="boardripper"
IMAGE_TAG="latest"
CONFIG_FILE="/volume1/docker/boardripper/container-config.json"
# Fallback config for first deploy (no existing container AND no saved config)
DEFAULT_MOUNTS='-v /volume1/docker/boardripper/data:/data -v /volume1/AL ZEUG/LogiCloud/Schematics-BV-EFI:/library:ro -v /var/run/docker.sock:/var/run/docker.sock'
DEFAULT_PORTS='-p 8090:8080'
DEFAULT_ENV='-e PORT=8080 -e LIBRARY_DIR=/library'
# Helper: run docker with sudo, filtering prompt noise
sdocker() {
echo "${PW}" | sudo -S ${DOCKER} "$@" 2>&1 | grep -v '^\[sudo\]' | grep -v '^Password:' || true
}
echo "[NAS] Decompressing image..."
gunzip -f /tmp/${IMAGE_NAME}.tar.gz
echo "[NAS] Loading Docker image..."
sdocker load -i /tmp/${IMAGE_NAME}.tar
# Image now ships USER 65532 (non-root). Existing /volume1/docker/boardripper/data
# was owned by root from prior root-running deploys; chown so the container can
# still write databank.db / .update-secret. Idempotent — no-op if already 65532.
echo "[NAS] Ensuring /volume1/docker/boardripper/data is owned by 65532:65532..."
echo "${PW}" | sudo -S chown -R 65532:65532 /volume1/docker/boardripper/data 2>&1 \
| grep -v '^\[sudo\]' | grep -v '^Password:' || true
# ── Capture existing container config and persist to file ──
EXISTING=$(echo "${PW}" | sudo -S ${DOCKER} ps -a --filter "name=^${IMAGE_NAME}$" --format '{{.Names}}' 2>/dev/null | grep -v '^\[sudo\]' | grep -v '^Password:' || true)
if [ -n "${EXISTING}" ]; then
echo "[NAS] Saving container config to ${CONFIG_FILE}..."
echo "${PW}" | sudo -S ${DOCKER} inspect "${IMAGE_NAME}" 2>/dev/null \
| grep -v '^\[sudo\]' | grep -v '^Password:' \
> "${CONFIG_FILE}" 2>/dev/null || true
fi
# ── Read config: from saved file, or fall back to defaults ──
MOUNT_ARGS=""
PORT_ARGS=""
ENV_ARGS=""
RESTART_POLICY="unless-stopped"
if [ -f "${CONFIG_FILE}" ] && python3 -c "import sys,json; json.load(open(sys.argv[1]))" "${CONFIG_FILE}" 2>/dev/null; then
echo "[NAS] Reading config from ${CONFIG_FILE}..."
MOUNT_ARGS=$(python3 -c "
import sys, json
data = json.load(open(sys.argv[1]))
if isinstance(data, list): data = data[0]
for m in data.get('Mounts', []):
if m.get('Type') == 'bind':
s = '-v ' + m['Source'] + ':' + m['Destination']
if not m.get('RW', True): s += ':ro'
print(s, end=' ')
" "${CONFIG_FILE}" 2>/dev/null)
PORT_ARGS=$(python3 -c "
import sys, json
data = json.load(open(sys.argv[1]))
if isinstance(data, list): data = data[0]
for cp, bindings in (data.get('HostConfig', {}).get('PortBindings') or {}).items():
port = cp.replace('/tcp', '')
for b in (bindings or []):
hp = b.get('HostPort', '')
hip = b.get('HostIp', '')
if hp:
prefix = (hip + ':') if hip and hip != '0.0.0.0' else ''
print(f'-p {prefix}{hp}:{port}', end=' ')
" "${CONFIG_FILE}" 2>/dev/null)
ENV_ARGS=$(python3 -c "
import sys, json
data = json.load(open(sys.argv[1]))
if isinstance(data, list): data = data[0]
skip = {'PATH', 'HOME', 'HOSTNAME'}
for e in data.get('Config', {}).get('Env', []):
key = e.split('=', 1)[0]
if key not in skip:
print(f'-e {e}', end=' ')
" "${CONFIG_FILE}" 2>/dev/null)
RP=$(python3 -c "
import sys, json
data = json.load(open(sys.argv[1]))
if isinstance(data, list): data = data[0]
print(data.get('HostConfig', {}).get('RestartPolicy', {}).get('Name', ''))
" "${CONFIG_FILE}" 2>/dev/null)
if [ -n "${RP}" ] && [ "${RP}" != "no" ]; then
RESTART_POLICY="${RP}"
fi
else
echo "[NAS] No saved config — using defaults"
fi
# Fall back to defaults for any empty fields
[ -z "${PORT_ARGS}" ] && PORT_ARGS="${DEFAULT_PORTS}" && echo "[NAS] ports: using defaults"
[ -z "${MOUNT_ARGS}" ] && MOUNT_ARGS="${DEFAULT_MOUNTS}" && echo "[NAS] mounts: using defaults"
[ -z "${ENV_ARGS}" ] && ENV_ARGS="${DEFAULT_ENV}" && echo "[NAS] env: using defaults"
# Ensure Docker socket is always mounted (required for self-update)
if ! echo "${MOUNT_ARGS}" | grep -q 'docker.sock'; then
MOUNT_ARGS="${MOUNT_ARGS} -v /var/run/docker.sock:/var/run/docker.sock"
fi
# Strip any legacy GITHUB_TOKEN that an old deploy may have baked into the
# saved container config — the offline-signed update path doesn't use it,
# and a stale token in the env can confuse external tools that key off it.
# Trailing-space-OR-end-of-string on the value match handles the case where
# the token is the last item in ENV_ARGS — earlier `[^ ]* `-with-mandatory-
# trailing-space pattern silently kept tokens at end-of-string. Add a
# trailing space first so a single regex form catches every position.
ENV_ARGS=$(echo "${ENV_ARGS} " | sed -E 's/-e GITHUB_TOKEN=[^ ]* //g; s/[[:space:]]+$//')
echo "[NAS] mounts: ${MOUNT_ARGS}"
echo "[NAS] ports: ${PORT_ARGS}"
echo "[NAS] env: ${ENV_ARGS}"
echo "[NAS] restart: ${RESTART_POLICY}"
echo "[NAS] Stopping old container..."
sdocker stop ${IMAGE_NAME} 2>/dev/null || true
sdocker rm ${IMAGE_NAME} 2>/dev/null || true
echo "[NAS] Starting new container..."
# USER override: the in-image default is UID 65532 (good for fresh OSS
# deployments), but this NAS's library mounts contain subdirs owned by
# many different users (inwerp, sc-syncthing, etc.) with restrictive
# perms — only root can read across all of them. Override the image's
# USER for this specific install. The permission audit's H9 finding
# stays open as a follow-up: support non-root with mixed-ownership
# library mounts (likely via --group-add detection or a per-mount UID
# probe in the deploy script).
RUN_CMD=$(python3 -c "
import shlex
args = ['run', '-d', '--name', '${IMAGE_NAME}', '--restart', '${RESTART_POLICY}', '--user', '0:0']
for raw in '''${PORT_ARGS}
${MOUNT_ARGS}
${ENV_ARGS}'''.strip().splitlines():
raw = raw.strip()
if not raw: continue
parts = raw.split()
i = 0
while i < len(parts):
if parts[i] in ('-v', '-p', '-e') and i + 1 < len(parts):
val_parts = [parts[i + 1]]
j = i + 2
while j < len(parts) and parts[j] not in ('-v', '-p', '-e'):
val_parts.append(parts[j])
j += 1
args.extend([parts[i], ' '.join(val_parts)])
i = j
else:
args.append(parts[i])
i += 1
args.append('${IMAGE_NAME}:${IMAGE_TAG}')
print(' '.join(shlex.quote(a) for a in args))
" 2>/dev/null)
echo "[NAS] cmd: docker ${RUN_CMD}"
echo "${DOCKER} ${RUN_CMD}" > /tmp/docker_run_cmd.sh
echo "${PW}" | sudo -S bash /tmp/docker_run_cmd.sh 2>&1 | grep -v '^\[sudo\]' | grep -v '^Password:' || true
rm -f /tmp/docker_run_cmd.sh
echo "[NAS] Pruning old images..."
sdocker image prune -f 2>/dev/null || true
echo "[NAS] Container status:"
sdocker ps --filter "name=${IMAGE_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
echo "[NAS] Mounts:"
echo "${PW}" | sudo -S ${DOCKER} inspect ${IMAGE_NAME} 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
if isinstance(data, list): data = data[0]
for m in data.get('Mounts', []):
mode = 'ro' if not m.get('RW', True) else 'rw'
print(f' {m[\"Source\"]} -> {m[\"Destination\"]} ({mode})')
" 2>/dev/null || echo " (could not read mounts)"
echo "[NAS] Cleaning up..."
rm -f /tmp/${IMAGE_NAME}.tar /tmp/deploy-remote.sh