-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathinstall.sh
More file actions
575 lines (521 loc) · 23.7 KB
/
Copy pathinstall.sh
File metadata and controls
575 lines (521 loc) · 23.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
#!/bin/sh
# =============================================================================
# NetPulse — one-liner installer (Linux server)
#
# Read-only PWA dashboard for monitoring OpenWrt/GL.iNet home networks.
# Installs the single static Go binary (frontend embedded) as a sandboxed
# systemd service.
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/gnacho/netpulse/main/install.sh | sh
# sh install.sh --version=1.0.0 --unattended
# sh install.sh --dry-run # describe every step, touches nothing
# sh install.sh --uninstall # keeps /var/lib/netpulse (data + .env)
# sh install.sh --uninstall --purge
#
# Requirements: Linux with systemd (Debian/Ubuntu/Fedora/Arch/...),
# amd64 / arm64 / armv7. Verifies sha256 of every download (checksums.txt).
#
# Layout (capistrano-less: single binary + state dir):
# /usr/local/bin/netpulse binary
# /var/lib/netpulse working dir: .env, data/ (SQLite), .ssh/
# =============================================================================
set -eu
APP_NAME="netpulse"
GH_REPO="gnacho/netpulse"
BIN_NAME="netpulse"
DEFAULT_PORT="3000"
INSTALL_DIR="/usr/local/bin"
STATE_DIR="/var/lib/$APP_NAME"
SERVICE_NAME="$APP_NAME"
# Versión de ESTE instalador (bumpear en cada release junto a httpapi.Version,
# para poder saber qué install.sh se está ejecutando; ver CHANGELOG).
INSTALLER_VERSION="2.28.11"
NETPULSE_VERSION=""; UNATTENDED=0; DRY_RUN=0; UNINSTALL=0; PURGE=0; DEMO=0
# ---------------------------------------------------------------- logging ---
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
C_G=$(printf '\033[32m'); C_R=$(printf '\033[31m'); C_Y=$(printf '\033[33m'); C_B=$(printf '\033[1m'); C_0=$(printf '\033[0m')
else C_G=""; C_R=""; C_Y=""; C_B=""; C_0=""; fi
info() { printf '%s--%s %s\n' "$C_B" "$C_0" "$*"; }
ok() { printf '%s✓%s %s\n' "$C_G" "$C_0" "$*"; }
warn() { printf '%s!%s %s\n' "$C_Y" "$C_0" "$*" >&2; }
err() { printf '%s✗ %s%s\n' "$C_R" "$*" "$C_0" >&2; }
fatal() { _c=$1; shift; err "$*"; exit "$_c"; }
run() { if [ "$DRY_RUN" -eq 1 ]; then info "[dry-run] $*"; else "$@"; fi; }
usage() {
cat <<EOF
NetPulse — installer (network monitoring dashboard for OpenWrt/GL.iNet)
Usage: sh install.sh [options]
--version=X.Y.Z version to install (default: latest stable release)
--demo install in demo mode with sample data (DEMO_MODE=1)
--unattended no questions (automatic when there's no TTY)
--dry-run describe each step without touching the system
--uninstall remove service, binary and user (keeps $STATE_DIR)
--purge with --uninstall: also remove data and configuration
-h, --help this help
Update = re-run this script (your data and .env are preserved).
Repo: https://github.com/$GH_REPO
EOF
exit 0
}
for arg in "$@"; do
case "$arg" in
--version=*) NETPULSE_VERSION="${arg#*=}" ;;
--demo) DEMO=1 ;;
--unattended) UNATTENDED=1 ;;
--dry-run) DRY_RUN=1 ;;
--uninstall) UNINSTALL=1 ;;
--purge) PURGE=1 ;;
-h|--help) usage ;;
*) fatal 10 "unknown option: $arg (try --help)" ;;
esac
done
[ -t 0 ] || UNATTENDED=1 # pipe-to-shell: never prompt
# ------------------------------------------------------- interactive helpers -
tty_ok() { (exec 3<>/dev/tty) 2>/dev/null; }
# ask_yes_no "question" [default: 0=no, 1=yes] — prompts on /dev/tty; returns 0/1.
# Non-interactive (--unattended or no TTY): returns the default silently.
ask_yes_no() {
_q=$1; _def=${2:-0}
[ "$UNATTENDED" -eq 1 ] && return "$_def"
tty_ok || return "$_def"
if [ "$_def" -eq 1 ]; then _hint="Y/n"; else _hint="y/N"; fi
while :; do
printf '%s [%s] ' "$_q" "$_hint" > /dev/tty
IFS= read -r _r < /dev/tty || _r=""
case "$(printf '%s' "$_r" | tr '[:upper:]' '[:lower:]')" in
"") return "$_def" ;;
y|yes|s|si) return 0 ;;
n|no) return 1 ;;
*) printf 'Please answer y or n.\n' > /dev/tty ;;
esac
done
}
# -------------------------------------------------------------- elevation ---
if [ "$(id -u)" -eq 0 ]; then SUDO=""
elif command -v sudo >/dev/null 2>&1; then SUDO="sudo -E"
elif command -v doas >/dev/null 2>&1; then SUDO="doas"
else fatal 22 "I need root (or sudo/doas). Download the script and run it as root: su -c 'sh install.sh'"
fi
# --------------------------------------------------------------- uninstall --
if [ "$UNINSTALL" -eq 1 ]; then
info "uninstalling $APP_NAME"
if [ -f "/etc/systemd/system/$SERVICE_NAME.service" ]; then
run $SUDO systemctl stop "$SERVICE_NAME" 2>/dev/null || true
run $SUDO systemctl disable "$SERVICE_NAME" 2>/dev/null || true
run $SUDO rm -f "/etc/systemd/system/$SERVICE_NAME.service"
# Units de reinicio bajo demanda (issue #4): si existen, quitarlas
if [ -f "/etc/systemd/system/$SERVICE_NAME-restart.path" ]; then
run $SUDO systemctl disable --now "$SERVICE_NAME-restart.path" 2>/dev/null
run $SUDO rm -f "/etc/systemd/system/$SERVICE_NAME-restart.path" \
"/etc/systemd/system/$SERVICE_NAME-restart.service"
fi
# Auto-update estable (#480): unidades + helper + marcadores residuales
if [ -f "/etc/systemd/system/$SERVICE_NAME-stable-update.path" ]; then
run $SUDO systemctl disable --now "$SERVICE_NAME-stable-update.path" 2>/dev/null
run $SUDO rm -f "/etc/systemd/system/$SERVICE_NAME-stable-update.path" \
"/etc/systemd/system/$SERVICE_NAME-stable-update.service" \
"$INSTALL_DIR/$APP_NAME-stable-apply"
run $SUDO rm -f "$STATE_DIR/data/.stable-update" \
"$STATE_DIR/data/.update-applied" "$STATE_DIR/data/.stable-update.error"
fi
run $SUDO systemctl daemon-reload
ok "systemd unit removed"
fi
run $SUDO rm -f "$INSTALL_DIR/$BIN_NAME" "$INSTALL_DIR/$BIN_NAME.bak"
ok "binary removed from $INSTALL_DIR"
if id "$APP_NAME" >/dev/null 2>&1; then
run $SUDO userdel "$APP_NAME" 2>/dev/null || warn "could not delete user $APP_NAME"
ok "system user removed"
fi
# userdel leaves the login group behind whenever it has other members
# (the collector user joins it) or the distro keeps it; the leftover
# group breaks the next install (#467).
if getent group "$APP_NAME" >/dev/null 2>&1; then
run $SUDO groupdel "$APP_NAME" 2>/dev/null || warn "could not delete group $APP_NAME (still in use?)"
ok "system group removed"
fi
if [ "$PURGE" -eq 1 ]; then
run $SUDO rm -rf "$STATE_DIR"
ok "data and configuration removed (--purge)"
else
info "data kept in $STATE_DIR (remove with --purge)"
fi
ok "$APP_NAME uninstalled"
exit 0
fi
# --------------------------------------------------------------- detection --
. /etc/os-release 2>/dev/null || true
OS_PRETTY="${PRETTY_NAME:-$(uname -s)}"
ARCH=$(uname -m)
case "$ARCH" in
x86_64|amd64) GOARCH=amd64 ;;
aarch64|arm64) GOARCH=arm64 ;;
armv7l|armv7) GOARCH=armv7 ;;
*) fatal 20 "unsupported architecture: $ARCH (released: amd64, arm64, armv7)"
esac
if [ ! -d /run/systemd/system ] || ! command -v systemctl >/dev/null 2>&1; then
fatal 23 "NetPulse needs systemd (this machine doesn't run it). See https://github.com/$GH_REPO for manual setup"
fi
info "detected: $OS_PRETTY · linux/$GOARCH · systemd"
info "install.sh version: $INSTALLER_VERSION"
if command -v curl >/dev/null 2>&1; then FETCH="curl -fsSL --retry 3 --connect-timeout 10"
elif command -v wget >/dev/null 2>&1; then FETCH="wget -q -O-"
else fatal 21 "I need curl or wget (install it with your package manager)"
fi
fetch_to() { $FETCH "$1" > "$2"; }
command -v sha256sum >/dev/null 2>&1 || fatal 21 "missing sha256sum (coreutils package)"
# ------------------------------------------------- disk / memory pre-flight --
AVAIL_MB=$(df -Pm / 2>/dev/null | awk 'NR==2 {print $4}' || true)
if [ -n "${AVAIL_MB:-}" ]; then
[ "$AVAIL_MB" -lt 150 ] && fatal 24 "not enough disk space: ${AVAIL_MB} MB free (minimum 150 MB)"
[ "$AVAIL_MB" -lt 300 ] && warn "low disk space: ${AVAIL_MB} MB free (recommended: 300+ MB)"
ok "disk space: ${AVAIL_MB} MB free"
fi
MEM_MB=$(awk '/^MemAvailable:/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || true)
if [ -n "${MEM_MB:-}" ] && [ "$MEM_MB" -lt 128 ]; then
warn "low memory: ${MEM_MB} MB available — NetPulse needs very little, but 128+ MB is recommended"
fi
# ------------------------------------------------------ port pre-flight -----
port_in_use() {
if command -v ss >/dev/null 2>&1; then
ss -tln 2>/dev/null | awk '{print $4}' | grep -qE "[:.]${1}\$"
elif command -v netstat >/dev/null 2>&1; then
netstat -tln 2>/dev/null | awk '{print $4}' | grep -qE "[:.]${1}\$"
else
return 1 # can't check: assume free
fi
}
pick_port() {
_p=$1; _end=$((_p + 20))
while [ "$_p" -le "$_end" ]; do
if ! port_in_use "$_p"; then printf '%s' "$_p"; return 0; fi
_p=$((_p + 1))
done
return 1
}
# choose_port WANT — interactive (TTY): asks which port to use, suggesting the
# next free one and rejecting busy/invalid answers. Non-interactive: prints the
# next free port. Prints the chosen port on stdout; fails if none is free.
choose_port() {
_want=$1
_next=$(pick_port "$((_want + 1))") || _next=""
if [ "$UNATTENDED" -eq 0 ] && tty_ok; then
while :; do
printf 'Port %s is already in use.\nWhich port should %s listen on? [%s] ' \
"$_want" "$APP_NAME" "${_next:-none free}" > /dev/tty
IFS= read -r _r < /dev/tty || _r=""
_r="${_r:-$_next}"
case "$_r" in
''|*[!0-9]*) printf 'Please enter a port number.\n' > /dev/tty; continue ;;
esac
if [ "$_r" -lt 1 ] || [ "$_r" -gt 65535 ]; then
printf 'Out of range (1-65535).\n' > /dev/tty; continue
fi
if port_in_use "$_r"; then
printf 'Port %s is also in use.\n' "$_r" > /dev/tty; continue
fi
printf '%s' "$_r"; return 0
done
fi
[ -n "$_next" ] || return 1
printf '%s' "$_next"
}
# Fresh install only: an upgrade keeps the port from the existing .env file.
PORT="$DEFAULT_PORT"
if [ ! -f "$STATE_DIR/.env" ]; then
if port_in_use "$DEFAULT_PORT"; then
PORT=$(choose_port "$DEFAULT_PORT") \
|| fatal 25 "port $DEFAULT_PORT is busy and no free port found in $((DEFAULT_PORT + 1))-$((DEFAULT_PORT + 21)) — set one manually in $STATE_DIR/.env after install"
warn "port $DEFAULT_PORT is already in use — NetPulse will listen on $PORT instead"
else
ok "port $DEFAULT_PORT is free"
fi
fi
# Demo mode (issue #4): solo con --demo explícito. El default es BD limpia;
# la demo se activa después desde Ajustes (el botón llama a
# POST /api/demo/enable y reinicia el servicio vía .restart-me).
# --------------------------------------------------------- resolve version --
if [ -z "$NETPULSE_VERSION" ]; then
info "resolving latest stable version"
NETPULSE_VERSION=$($FETCH "https://api.github.com/repos/$GH_REPO/releases/latest" \
| grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | cut -d'"' -f4) \
|| fatal 31 "could not resolve the latest version (GitHub rate-limit?). Use --version=X.Y.Z"
[ -n "$NETPULSE_VERSION" ] || fatal 31 "no stable release found yet. Use --version=X.Y.Z"
fi
VERSION_NORM=$(echo "$NETPULSE_VERSION" | sed 's/^v//')
case "$NETPULSE_VERSION" in
v*) NETPULSE_TAG="$NETPULSE_VERSION" ;;
*) NETPULSE_TAG="v$NETPULSE_VERSION" ;;
esac
ASSET="${BIN_NAME}_${VERSION_NORM}_linux_${GOARCH}.tar.gz"
BASE_URL="https://github.com/$GH_REPO/releases/download/$NETPULSE_TAG"
info "version: $NETPULSE_VERSION"
# connectivity pre-flight BEFORE touching the system
$FETCH "https://github.com/$GH_REPO" >/dev/null \
|| fatal 30 "no access to github.com (proxy? DNS? firewall?)"
UPGRADING=0
if [ -x "$INSTALL_DIR/$BIN_NAME" ]; then
UPGRADING=1
info "previous install detected: upgrade mode (data and .env are preserved)"
fi
# ---------------------------------------------------------- download+verify --
TMP=$(mktemp -d) || fatal 34 "mktemp failed"
cleanup() { rm -rf "$TMP"; return 0; }
trap cleanup EXIT INT TERM
info "downloading $ASSET"
fetch_to "$BASE_URL/$ASSET" "$TMP/$ASSET" || fatal 32 "download failed: $BASE_URL/$ASSET"
fetch_to "$BASE_URL/checksums.txt" "$TMP/checksums.txt" || fatal 32 "checksums.txt not found in release $NETPULSE_VERSION"
[ -s "$TMP/$ASSET" ] || fatal 32 "downloaded asset is empty"
SUM_FILE=$(sha256sum "$TMP/$ASSET" | awk '{print $1}')
SUM_REF=$(grep " $ASSET\$" "$TMP/checksums.txt" | awk '{print $1}')
[ -n "$SUM_REF" ] || fatal 33 "$ASSET not listed in checksums.txt"
[ "$SUM_FILE" = "$SUM_REF" ] || fatal 33 "sha256 MISMATCH for $ASSET — corrupt or tampered download"
ok "sha256 verified"
tar -tzf "$TMP/$ASSET" >/dev/null 2>&1 || fatal 34 "tarball is corrupt"
tar -xzf "$TMP/$ASSET" -C "$TMP"
[ -s "$TMP/$BIN_NAME" ] || fatal 34 "tarball does not contain $BIN_NAME"
# ------------------------------------------------------------------ install --
run $SUDO install -d -m 0755 "$INSTALL_DIR"
if [ "$UPGRADING" -eq 1 ]; then
run $SUDO cp -a "$INSTALL_DIR/$BIN_NAME" "$INSTALL_DIR/$BIN_NAME.bak"
info "previous binary backed up: $INSTALL_DIR/$BIN_NAME.bak"
fi
run $SUDO install -T -m 0755 "$TMP/$BIN_NAME" "$INSTALL_DIR/$BIN_NAME"
ok "binary installed at $INSTALL_DIR/$BIN_NAME"
if ! id "$APP_NAME" >/dev/null 2>&1; then
# Reinstall on a box where the group survived a previous uninstall
# (#467): reuse the existing group instead of dying with
# "useradd: group netpulse exists".
if getent group "$APP_NAME" >/dev/null 2>&1; then
run $SUDO useradd --system -g "$APP_NAME" --home-dir "$STATE_DIR" --shell /usr/sbin/nologin "$APP_NAME"
else
run $SUDO useradd --system --home-dir "$STATE_DIR" --shell /usr/sbin/nologin "$APP_NAME"
fi
ok "system user $APP_NAME created"
fi
run $SUDO install -d -m 0750 -o "$APP_NAME" -g "$APP_NAME" "$STATE_DIR"
# SELinux: context fix, degrade to warning
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" = "Enforcing" ]; then
run $SUDO chcon -t bin_t "$INSTALL_DIR/$BIN_NAME" 2>/dev/null \
|| warn "SELinux Enforcing: if the service fails, check 'ausearch -m avc -ts recent'"
fi
# Initial config ONLY on fresh install (upgrades never touch .env or data)
if [ ! -f "$STATE_DIR/.env" ]; then
# #895: alfabeto sin caracteres ambiguos (sin 0/O ni 1/l/I) para que la
# password inicial no se confunda en fuentes de terminal. 57 símbolos ×
# 16 caracteres ≈ 94 bits de entropía.
ADMIN_PASS=$(LC_ALL=C tr -dc 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789' < /dev/urandom | head -c 16)
if [ "$DRY_RUN" -eq 1 ]; then info "[dry-run] would generate $STATE_DIR/.env (0600, random admin password, PORT=$PORT, DEMO_MODE=$DEMO)"; else
$SUDO tee "$STATE_DIR/.env" >/dev/null <<EOF
PORT=$PORT
DATA_DIR=./data
AUTH_USER=admin
AUTH_PASS=$ADMIN_PASS
DEMO_MODE=$DEMO
SSH_KEY_PATH=$STATE_DIR/.ssh/id_ed25519
EOF
$SUDO chmod 0600 "$STATE_DIR/.env"
$SUDO chown "$APP_NAME:$APP_NAME" "$STATE_DIR/.env"
fi
FRESH_CREDENTIALS=1
else
FRESH_CREDENTIALS=0
PORT=$(sed -n 's/^PORT=//p' "$STATE_DIR/.env" | head -1)
PORT="${PORT:-$DEFAULT_PORT}"
DEMO=$(sed -n 's/^DEMO_MODE=//p' "$STATE_DIR/.env" | head -1)
DEMO="${DEMO:-0}"
info "existing config kept ($STATE_DIR/.env)"
fi
# ----------------------------------------------------------------- service --
if [ "$DRY_RUN" -eq 1 ]; then info "[dry-run] would write systemd unit + enable --now"; else
$SUDO tee "/etc/systemd/system/$SERVICE_NAME.service" >/dev/null <<EOF
[Unit]
Description=NetPulse — network monitoring dashboard (OpenWrt/GL.iNet)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=$APP_NAME
Group=$APP_NAME
WorkingDirectory=$STATE_DIR
ExecStart=$INSTALL_DIR/$BIN_NAME
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
LockPersonality=true
RemoveIPC=true
StateDirectory=$APP_NAME
UMask=007
[Install]
WantedBy=multi-user.target
EOF
# Unidad de reinicio bajo demanda (issue #4 + updater): una unit .path
# vigila $STATE_DIR/data/.restart-me; cuando el servidor lo toca (p.ej.
# POST /api/demo/enable) este oneshot lo borra y reinicia el servicio.
$SUDO tee "/etc/systemd/system/$SERVICE_NAME-restart.path" >/dev/null <<EOF
[Unit]
Description=Reinicia $SERVICE_NAME cuando el servidor toca .restart-me
[Path]
PathChanged=$STATE_DIR/data/.restart-me
[Install]
WantedBy=multi-user.target
EOF
$SUDO tee "/etc/systemd/system/$SERVICE_NAME-restart.service" >/dev/null <<EOF
[Unit]
Description=Reinicio de $SERVICE_NAME solicitado por el servidor
[Service]
Type=oneshot
ExecStart=/bin/sh -c "rm -f $STATE_DIR/data/.restart-me; /bin/systemctl restart $SERVICE_NAME.service"
EOF
# Auto-update estable (#480): el servidor deja el binario nuevo en
# escena + marcador .stable-update; esta unit .path dispara el helper
# root que verifica, hace swap y reinicia con rollback.
$SUDO tee "/etc/systemd/system/$SERVICE_NAME-stable-update.path" >/dev/null <<EOF
[Unit]
Description=Vigila el marcador de auto-update estable de $SERVICE_NAME
[Path]
PathChanged=$STATE_DIR/data/.stable-update
[Install]
WantedBy=multi-user.target
EOF
$SUDO tee "/etc/systemd/system/$SERVICE_NAME-stable-update.service" >/dev/null <<EOF
[Unit]
Description=Aplica el update estable de $SERVICE_NAME (swap+restart)
[Service]
Type=oneshot
Environment=STATE_DIR=$STATE_DIR
Environment=SERVER_BIN=$INSTALL_DIR/$BIN_NAME
Environment=SERVICE=$SERVICE_NAME
ExecStart=$INSTALL_DIR/$APP_NAME-stable-apply
EOF
$SUDO tee "$INSTALL_DIR/$APP_NAME-stable-apply" >/dev/null <<'EOF_HELPER'
#!/bin/sh
# netpulse-stable-apply — helper root del auto-update estable (#480).
# Lo dispara netpulse-stable-update.path cuando el servidor deja el binario
# nuevo en escena con el marcador .stable-update (target/sha256/staged).
# Reverifica el sha256, swap atómico, reinicio y healthcheck; rollback si
# el servicio no vuelve a levantar. Sin curl, acepta sin verificar salud.
set -u
STATE_DIR="${STATE_DIR:-/var/lib/netpulse}"
SERVER_BIN="${SERVER_BIN:-/usr/local/bin/netpulse}"
SERVICE="${SERVICE:-netpulse}"
DATA_DIR="$STATE_DIR/data"
MARKER="$DATA_DIR/.stable-update"
APPLIED="$DATA_DIR/.update-applied"
ERRFILE="$DATA_DIR/.stable-update.error"
BACKUP="$SERVER_BIN.bak-selfupdate"
LOG_TAG="netpulse-stable-apply"
log() { logger -t "$LOG_TAG" "$*" 2>/dev/null || echo "$LOG_TAG: $*"; }
fail() {
log "ERROR: $*"
rm -f "$MARKER"
echo "$*" > "$ERRFILE" 2>/dev/null || true
exit 1
}
[ -f "$MARKER" ] || exit 0
target=""; sha=""; staged=""
while IFS='=' read -r k v; do
case "$k" in
target) target="$v" ;;
sha256) sha="$v" ;;
staged) staged="$v" ;;
esac
done < "$MARKER"
[ -n "$target" ] && [ -n "$sha" ] && [ -n "$staged" ] || fail "stable_marker_invalid"
case "$target" in v[0-9]*) ;; *) fail "stable_marker_invalid" ;; esac
[ -f "$staged" ] || fail "stable_marker_invalid"
echo "$sha $staged" | sha256sum -c - >/dev/null 2>&1 || fail "stable_checksum_mismatch"
cp -a "$SERVER_BIN" "$BACKUP" 2>/dev/null || true
install -T -m 0755 "$staged" "$SERVER_BIN" || fail "stable_install_failed"
# Marcador de éxito ANTES del reinicio (patrón #444): el arranque nuevo lo
# usa para confirmar el apply en el historial.
printf '%s\n' "$target" > "$APPLIED"
rm -f "$MARKER" "$staged"
log "swap a $target hecho; reiniciando $SERVICE"
systemctl restart "$SERVICE.service" || fail "stable_install_failed"
if ! command -v curl >/dev/null 2>&1; then
log "curl no disponible; healthcheck omitido"
rm -f "$ERRFILE"
exit 0
fi
PORT=""
[ -f "$STATE_DIR/.env" ] && PORT=$(sed -n 's/^PORT=//p' "$STATE_DIR/.env" | head -1)
PORT="${PORT:-3000}"
# Budget generoso: el stop del servicio puede tardar hasta TimeoutStopSec
# (90 s) si un tick del poller queda atascado en SNMP lentos (#481), más el
# arranque con primer poll en curso.
i=0
while [ "$i" -lt 70 ]; do
if curl -fsS --max-time 3 "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; then
log "healthcheck OK tras actualizar a $target"
rm -f "$ERRFILE"
exit 0
fi
i=$((i + 1))
sleep 3
done
log "healthcheck falló tras 210s; rollback a $BACKUP"
cp -a "$BACKUP" "$SERVER_BIN" 2>/dev/null || true
systemctl restart "$SERVICE.service" 2>/dev/null || true
rm -f "$APPLIED"
fail "stable_healthcheck_failed"
EOF_HELPER
$SUDO chmod 0755 "$INSTALL_DIR/$APP_NAME-stable-apply"
fi
run $SUDO systemctl daemon-reload
if [ "$UPGRADING" -eq 1 ]; then run $SUDO systemctl restart "$SERVICE_NAME"
else run $SUDO systemctl enable --now "$SERVICE_NAME"; fi
run $SUDO systemctl enable --now "$SERVICE_NAME-restart.path" 2>/dev/null \
|| warn "could not enable $SERVICE_NAME-restart.path (demo/update auto-restart)"
run $SUDO systemctl enable --now "$SERVICE_NAME-stable-update.path" 2>/dev/null \
|| warn "could not enable $SERVICE_NAME-stable-update.path (stable self-update)"
if [ "$DRY_RUN" -eq 0 ]; then
sleep 3
if ! $SUDO systemctl is-active --quiet "$SERVICE_NAME"; then
err "service failed to start; diagnostics:"
$SUDO systemctl status "$SERVICE_NAME" --no-pager || true
$SUDO journalctl -u "$SERVICE_NAME" -n 50 --no-pager || true
exit 40
fi
ok "service $SERVICE_NAME active"
if command -v curl >/dev/null 2>&1; then
curl -fsS --max-time 5 "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1 \
&& ok "HTTP health check OK on :$PORT" \
|| warn "service is up but http://127.0.0.1:$PORT didn't answer yet (give it a few seconds)"
fi
fi
# ------------------------------------------------------------------ summary --
printf '\n%s================ %s installed ================%s\n' "$C_G" "$APP_NAME" "$C_0"
printf 'Version: %s%s\n' "$NETPULSE_VERSION" "$( [ "$UPGRADING" -eq 1 ] && echo " (upgrade — previous binary at $INSTALL_DIR/$BIN_NAME.bak)" || true)"
printf 'Binary: %s\n' "$INSTALL_DIR/$BIN_NAME"
printf 'Data: %s (SQLite, .env, SSH keypair)\n' "$STATE_DIR"
printf 'Access: http://<this-machine-ip>:%s\n' "$PORT"
if [ "${FRESH_CREDENTIALS:-0}" -eq 1 ] && [ "$DRY_RUN" -eq 0 ]; then
printf '\nInitial credentials (shown ONCE — change them after logging in):\n'
printf ' user: admin\n password: %s\n' "$ADMIN_PASS"
fi
printf '\nUseful commands:\n'
printf ' systemctl status %s\n journalctl -u %s -f\n' "$SERVICE_NAME" "$SERVICE_NAME"
printf ' sh install.sh # update to the latest stable version\n'
printf ' sh install.sh --uninstall\n'
printf '\nNotes:\n'
if [ "${DEMO:-0}" = "1" ]; then
printf ' - DEMO MODE: sample network with 60+ devices; your routers are untouched.\n'
printf ' To go live: set DEMO_MODE=0 in %s/.env and systemctl restart %s\n' "$STATE_DIR" "$SERVICE_NAME"
else
printf ' - To explore with sample data first: set DEMO_MODE=1 in %s/.env\n' "$STATE_DIR"
printf ' and systemctl restart %s (demo mode never touches your routers).\n' "$SERVICE_NAME"
fi
printf ' - No firewall port was opened. If you need one:\n'
printf ' firewall-cmd --permanent --add-port=%s/tcp && firewall-cmd --reload\n' "$PORT"
printf ' ufw allow %s/tcp\n' "$PORT"
printf ' - Live mode: authorize the server SSH public key on each router\n'
printf ' (Settings shows it; append to /etc/dropbear/authorized_keys).\n'
printf ' - Auto-update: the app downloads, verifies (sha256) and applies new\n'
printf ' stable versions by itself (root helper %s-stable-apply; rollback\n' "$APP_NAME"
printf ' if the health check fails). Re-run this script for manual updates.\n'
printf '%s================================================%s\n\n' "$C_G" "$C_0"