-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.sh
More file actions
407 lines (373 loc) · 17.3 KB
/
Copy pathinstall.sh
File metadata and controls
407 lines (373 loc) · 17.3 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
#!/bin/sh
# =============================================================================
# Keynest — one-liner installer (Linux server)
#
# Self-hosted property management for Airbnb hosts: reservations (iCal),
# cleanings, smart locks and profitability (Node 22 + SQLite, frontend built).
# Installs the versioned Node runtime + the app release (node_modules
# pre-built) as a sandboxed systemd service.
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/gnacho/keynest/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/keynest (data) and /etc/keynest
# sh install.sh --uninstall --purge
#
# Requirements: Linux with systemd (Debian/Ubuntu/Fedora/Arch/...),
# amd64 / arm64. Verifies sha256 of every download (checksums.txt).
#
# Layout (capistrano-style):
# /opt/keynest/node-v<ver>-linux-<arch>/ versioned Node runtime
# /opt/keynest/node -> node-v<ver>... runtime symlink
# /opt/keynest/releases/v<ver>/ app release (server/ + dist/)
# /opt/keynest/current -> releases/v<ver> live symlink (update = flip, rollback = flip back)
# /var/lib/keynest SQLite + uploads (StateDirectory)
# /etc/keynest/env config (0600)
# =============================================================================
set -eu
APP_NAME="keynest"
GH_REPO="gnacho/keynest"
DESCRIPTION="Self-hosted property management for Airbnb hosts"
DEFAULT_PORT="8081"
NODE_VERSION="22.23.2"
OPT_DIR="/opt/$APP_NAME"
STATE_DIR="/var/lib/$APP_NAME"
CONF_DIR="/etc/$APP_NAME"
ENV_FILE="$CONF_DIR/env"
SERVICE_NAME="$APP_NAME"
KEYNEST_VERSION=""; UNATTENDED=0; DRY_RUN=0; UNINSTALL=0; PURGE=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
Keynest — installer ($DESCRIPTION)
Usage: sh install.sh [options]
--version=X.Y.Z version to install (default: latest stable release)
--unattended no questions (automatic when there's no TTY)
--dry-run describe each step without touching the system
--uninstall remove app and user (keeps $STATE_DIR and $CONF_DIR)
--purge with --uninstall: also remove data and configuration
-h, --help this help
Update = re-run this script (data and config are preserved).
Rollback: ln -sfn $OPT_DIR/releases/<previous-version> $OPT_DIR/current && systemctl restart $APP_NAME
Repo: https://github.com/$GH_REPO
EOF
exit 0
}
for arg in "$@"; do
case "$arg" in
--version=*) KEYNEST_VERSION="${arg#*=}" ;;
--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
# -------------------------------------------------------------- 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"
run $SUDO systemctl daemon-reload
ok "systemd unit removed"
fi
run $SUDO rm -rf "$OPT_DIR"
ok "$OPT_DIR removed"
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
if [ "$PURGE" -eq 1 ]; then
run $SUDO rm -rf "$STATE_DIR" "$CONF_DIR"
ok "data and configuration removed (--purge)"
else
info "data kept: $STATE_DIR · config: $CONF_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) REL_ARCH=amd64; NODE_ARCH=x64 ;;
aarch64|arm64) REL_ARCH=arm64; NODE_ARCH=arm64 ;;
*) fatal 20 "unsupported architecture: $ARCH (released: amd64, arm64)"
esac
if [ ! -d /run/systemd/system ] || ! command -v systemctl >/dev/null 2>&1; then
fatal 23 "Keynest needs systemd (this machine doesn't run it). See https://github.com/$GH_REPO for manual setup"
fi
info "detected: $OS_PRETTY · linux/$REL_ARCH · systemd"
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"; }
for tool in sha256sum tar xz; do command -v "$tool" >/dev/null 2>&1 || fatal 21 "missing tool: $tool"; done
# ------------------------------------------------- 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 300 ] && fatal 24 "not enough disk space: ${AVAIL_MB} MB free (minimum 300 MB)"
[ "$AVAIL_MB" -lt 600 ] && warn "low disk space: ${AVAIL_MB} MB free (recommended: 600+ 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 400 ]; then
warn "low memory: ${MEM_MB} MB available — Node 22 + Keynest is happier with 400+ MB"
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
}
tty_ok() { (exec 3<>/dev/tty) 2>/dev/null; }
# 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 "$ENV_FILE" ]; 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 $ENV_FILE after install"
warn "port $DEFAULT_PORT is already in use — Keynest will listen on $PORT instead"
else
ok "port $DEFAULT_PORT is free"
fi
fi
# --------------------------------------------------------- resolve version --
if [ -z "$KEYNEST_VERSION" ]; then
info "resolving latest stable version"
KEYNEST_VERSION=$($FETCH "https://api.github.com/repos/$GH_REPO/releases/latest" \
| grep '"tag_name"' | head -1 | cut -d'"' -f4) \
|| fatal 31 "could not resolve the latest version (GitHub rate-limit?). Use --version=X.Y.Z"
[ -n "$KEYNEST_VERSION" ] || fatal 31 "no stable release found yet. Use --version=X.Y.Z"
fi
VERSION_NORM=$(echo "$KEYNEST_VERSION" | sed 's/^v//')
case "$KEYNEST_VERSION" in
v*) KEYNEST_TAG="$KEYNEST_VERSION" ;;
*) KEYNEST_TAG="v$KEYNEST_VERSION" ;;
esac
ASSET="${APP_NAME}_${VERSION_NORM}_linux_${REL_ARCH}.tar.gz"
BASE_URL="https://github.com/$GH_REPO/releases/download/$KEYNEST_TAG"
info "version: $KEYNEST_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
[ -L "$OPT_DIR/current" ] && { UPGRADING=1; info "previous install detected: upgrade mode (data and config are preserved)"; }
# ---------------------------------------------------------- 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 $KEYNEST_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"
# -------------------------------------------------------------- Node runtime --
NODE_DIR="node-v${NODE_VERSION}-linux-${NODE_ARCH}"
if [ ! -x "$OPT_DIR/$NODE_DIR/bin/node" ]; then
info "installing Node runtime v$NODE_VERSION ($NODE_ARCH)"
fetch_to "https://nodejs.org/dist/v${NODE_VERSION}/${NODE_DIR}.tar.xz" "$TMP/node.tar.xz" \
|| fatal 35 "Node download failed"
fetch_to "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt" "$TMP/SHASUMS256.txt" \
|| fatal 35 "SHASUMS256.txt download failed"
NSUM=$(sha256sum "$TMP/node.tar.xz" | awk '{print $1}')
NREF=$(grep " ${NODE_DIR}.tar.xz\$" "$TMP/SHASUMS256.txt" | awk '{print $1}')
[ "$NSUM" = "$NREF" ] || fatal 35 "sha256 MISMATCH for the Node runtime"
run $SUDO install -d -m 0755 "$OPT_DIR"
run $SUDO tar -xJf "$TMP/node.tar.xz" -C "$OPT_DIR"
ok "Node v$NODE_VERSION verified and installed"
else
ok "Node v$NODE_VERSION already present"
fi
run $SUDO ln -sfn "$OPT_DIR/$NODE_DIR" "$OPT_DIR/node"
NODE_BIN="$OPT_DIR/node/bin/node"
# --------------------------------------------------------------------- user --
if ! id "$APP_NAME" >/dev/null 2>&1; then
run $SUDO useradd --system --home-dir "$STATE_DIR" --shell /usr/sbin/nologin "$APP_NAME"
ok "system user $APP_NAME created"
fi
run $SUDO install -d -m 0750 -o "$APP_NAME" -g "$APP_NAME" "$STATE_DIR"
run $SUDO install -d -m 0750 "$CONF_DIR"
# ------------------------------------------------------------ install release --
RELEASE_DIR="$OPT_DIR/releases/v$VERSION_NORM"
run $SUDO install -d -m 0755 "$RELEASE_DIR"
run $SUDO tar -xzf "$TMP/$ASSET" -C "$RELEASE_DIR"
run $SUDO chown -R "$APP_NAME:$APP_NAME" "$RELEASE_DIR"
run $SUDO ln -sfn "$RELEASE_DIR" "$OPT_DIR/current"
ok "release v$VERSION_NORM at $RELEASE_DIR (current -> it)"
# Initial config ONLY on fresh install (upgrades never touch credentials)
if [ ! -f "$ENV_FILE" ]; then
ADMIN_PASS=$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | head -c 16)
if [ "$DRY_RUN" -eq 1 ]; then info "[dry-run] would generate $ENV_FILE (0600, random admin password, PORT=$PORT)"; else
$SUDO tee "$ENV_FILE" >/dev/null <<EOF
PORT=$PORT
AUTH_USER=admin
AUTH_PASS=$ADMIN_PASS
DATA_DIR=$STATE_DIR
STATIC_DIR=$OPT_DIR/current/dist
EOF
$SUDO chmod 0600 "$ENV_FILE"
$SUDO chown "$APP_NAME:$APP_NAME" "$ENV_FILE"
fi
FRESH_CREDENTIALS=1
else
FRESH_CREDENTIALS=0
PORT=$(sed -n 's/^PORT=//p' "$ENV_FILE" | head -1)
PORT="${PORT:-$DEFAULT_PORT}"
info "existing config kept ($ENV_FILE)"
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=Keynest — $DESCRIPTION
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=$APP_NAME
Group=$APP_NAME
WorkingDirectory=$OPT_DIR/current
EnvironmentFile=$ENV_FILE
ExecStart=$NODE_BIN server/src/index.js
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
LockPersonality=true
RemoveIPC=true
StateDirectory=$APP_NAME
ReadWritePaths=$STATE_DIR
UMask=007
[Install]
WantedBy=multi-user.target
EOF
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
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/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
# ---------------------------------------------------------- auto-update timer --
# Auto-update semanal con systemd timer (regla: timer > cron siempre). El script
# despliega releases estables con sha256 verificado y escribe el marker semver.
if [ "$DRY_RUN" -eq 1 ]; then
info "[dry-run] would install auto-update timer (keynest-update.timer, weekly)"
else
if [ -f "$RELEASE_DIR/deploy/keynest-update.sh" ]; then
$SUDO install -m 0755 "$RELEASE_DIR/deploy/keynest-update.sh" "$OPT_DIR/keynest-update.sh"
$SUDO cp "$RELEASE_DIR/deploy/keynest-update.service" "/etc/systemd/system/$SERVICE_NAME-update.service"
$SUDO cp "$RELEASE_DIR/deploy/keynest-update.timer" "/etc/systemd/system/$SERVICE_NAME-update.timer"
$SUDO systemctl daemon-reload
$SUDO systemctl enable --now "$SERVICE_NAME-update.timer"
ok "auto-update timer instalado (semanal; releases estables + sha256)"
else
warn "deploy/keynest-update.sh no está en el release — auto-update no instalado"
fi
fi
# ------------------------------------------------------------------ summary --
printf '\n%s================ %s installed ================%s\n' "$C_G" "$APP_NAME" "$C_0"
printf 'Version: %s%s\n' "$KEYNEST_VERSION" "$( [ "$UPGRADING" -eq 1 ] && echo ' (upgrade)' || true)"
printf 'App: %s -> %s\n' "$OPT_DIR/current" "$RELEASE_DIR"
printf 'Node: %s\n' "$NODE_BIN"
printf 'Data: %s\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'
printf ' - Demo data (5 sample properties + reservations) is seeded alongside the\n'
printf ' real database — enable demo mode from Settings to explore safely.\n'
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 ' - Behind an HTTPS reverse proxy the session cookie is marked Secure\n'
printf ' automatically (x-forwarded-proto).\n'
printf ' - Rollback: ln -sfn %s/releases/<previous> %s/current && systemctl restart %s\n' "$OPT_DIR" "$OPT_DIR" "$SERVICE_NAME"
printf '%s================================================%s\n\n' "$C_G" "$C_0"