-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathstack
More file actions
executable file
·355 lines (333 loc) · 14.1 KB
/
Copy pathstack
File metadata and controls
executable file
·355 lines (333 loc) · 14.1 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
#!/usr/bin/env bash
# Single entry point for operating the stack. Run ./stack help for usage.
set -euo pipefail
cd "$(dirname "$0")"
usage() {
cat <<'EOF'
Usage: ./stack <command> [args]
up | down | restart [svc] Start / stop / restart the stack (or one service)
init Interactive setup wizard (optional; up uses defaults)
logs [svc] Follow logs (all services, or one)
apply Re-render .env from config.json and apply changes
upgrade Fetch the latest release and apply it (backs up first)
upgrade-agent Watch for dashboard Upgrade-button requests (auto-managed)
status Container + health summary; warns on anything down
doctor Read-only health report (deps, disk, sync, tor)
backup Save config, credentials, and onion keys to backups/
restore [-y] <archive> Restore a backup archive (prompts before overwrite)
help This text
EOF
}
env_get() { sed -n "s/^$1=//p" .env 2>/dev/null; }
# Interactive setup: every prompt has a sensible default — press Enter to skip.
# Writes config.json (only the values you change), then offers to start. Plain
# `./stack up` needs none of this; it just uses the defaults.
init() {
for tool in jq openssl; do
command -v "$tool" >/dev/null || {
echo "$tool is required: sudo apt install $tool"
exit 1
}
done
if [ -f config.json ]; then
printf "config.json already exists — overwrite it? [y/N] "
read -r reply
case "$reply" in y | Y) ;; *)
echo "Kept existing config.json. (Edit it and ./stack apply, or ./stack up.)"
return 0
;;
esac
fi
echo "Setup — press Enter to accept the [default]. Nothing here is required."
echo
read -r -p "RPC username [bitcoin]: " w_user
read -rsp "RPC password [Enter = strong random]: " w_pass; echo
read -rsp "Dashboard web password [Enter = strong random]: " w_dashpass; echo
if [ -z "$w_dashpass" ]; then
w_dashpass=$(openssl rand -hex 16)
echo " generated dashboard password: $w_dashpass (log in with any username — save it)"
fi
read -r -p "Publish the dashboard as a Tor onion service (reach it from anywhere)? [y/N] " w_dashonion
read -r -p "One-click upgrades from the dashboard (Upgrade button)? [y/N] " w_control
read -r -p "Accept inbound connections over Tor (helps the network, no IP exposure)? [Y/n] " w_inbound
read -r -p "Sync the initial download over clearnet first? Faster, but EXPOSES YOUR IP. [y/N] " w_clearnet
read -r -p "Prune the chain to save disk — size in GB, or Enter for a full node: " w_prune_gb
yes() { case "$1" in y | Y | yes | YES) return 0 ;; *) return 1 ;; esac; }
cfg=$(jq -n '{}')
[ -n "$w_user" ] && [ "$w_user" != "bitcoin" ] && cfg=$(jq --arg v "$w_user" '.bitcoin.node_username=$v' <<<"$cfg")
[ -n "$w_pass" ] && cfg=$(jq --arg v "$w_pass" '.bitcoin.node_password=$v' <<<"$cfg")
[ -n "$w_dashpass" ] && cfg=$(jq --arg v "$w_dashpass" '.dashboard.password=$v' <<<"$cfg")
yes "$w_dashonion" && cfg=$(jq '.dashboard.onion=true' <<<"$cfg")
yes "$w_control" && cfg=$(jq '.dashboard.control=true' <<<"$cfg")
# inbound is default-on; only record a deviation when the user declines
case "$w_inbound" in n | N | no | NO) cfg=$(jq '.bitcoin.inbound_onion=false' <<<"$cfg") ;; esac
yes "$w_clearnet" && cfg=$(jq '.bitcoin.sync_over_clearnet=true' <<<"$cfg")
if [ -n "$w_prune_gb" ]; then
case "$w_prune_gb" in
*[!0-9]*) echo " (prune must be a whole number of GB — ignoring)" ;;
*) cfg=$(jq --argjson v "$((w_prune_gb * 1000))" '.bitcoin.prune_mb=$v' <<<"$cfg") ;;
esac
fi
echo "$cfg" | jq . >config.json
echo
echo "Wrote config.json:"
sed 's/^/ /' config.json
echo
printf "Start the stack now? [Y/n] "
read -r reply
case "$reply" in n | N) echo "Later: ./stack up" ;; *)
./configure.sh && docker compose up -d && ensure_agent
;;
esac
}
status() {
docker compose ps
bad=0
for svc in tor bitcoin dashboard; do
st=$(docker inspect "$svc" --format '{{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}' 2>/dev/null || echo missing)
case "$st" in
"running healthy") echo "OK $svc" ;;
"running health: starting" | "running starting") echo "WAIT $svc (health check warming up)" ;;
*)
echo "WARN $svc: $st"
bad=1
;;
esac
done
return "$bad"
}
doctor() {
echo "== Dependencies"
for tool in docker jq openssl; do
if command -v "$tool" >/dev/null; then echo "OK $tool"; else echo "MISS $tool"; fi
done
echo "== Config"
if [ ! -f config.json ]; then
echo "MISS config.json (cp config.example.json config.json)"
elif [ ! -f .env ]; then
echo "MISS .env (run ./stack apply)"
elif [ config.json -nt .env ]; then
echo "WARN config.json is newer than .env — run ./stack apply"
else
echo "OK config.json rendered into .env"
fi
if [ "$(env_get DASHBOARD_CONTROL)" = "1" ]; then
if pgrep -f "stack upgrade-agent" >/dev/null 2>&1; then
echo "OK upgrade-agent (dashboard Upgrade button)"
else
echo "WARN upgrade-agent not running — ./stack up starts it"
fi
fi
echo "== Disk"
data_dir=$(env_get BITCOIN_DATA_DIR)
df -h "${data_dir:-.}" 2>/dev/null | tail -1 | awk '{printf " %s free of %s (%s used) at %s\n", $4, $2, $5, $6}'
echo "== Containers"
status || true
echo "== Node"
if docker exec bitcoin bitcoin-cli -datadir=/data getblockchaininfo 2>/dev/null |
jq -r '" blocks \(.blocks)/\(.headers) progress \((.verificationprogress*100*100|round)/100)% ibd \(.initialblockdownload) pruned \(.pruned)"'; then :; else
echo " RPC unreachable (normal during startup; if it's been up a while, check ./stack logs bitcoin)"
fi
echo "== Tor"
docker logs tor 2>&1 | grep "Bootstrapped" | tail -1 | sed 's/^/ /' || echo " no bootstrap log yet"
if [ "$(env_get DASHBOARD_ONION)" = "1" ]; then
echo " dashboard onion: $(docker exec tor cat /var/lib/tor/dashboard_onion/hostname 2>/dev/null || echo 'not provisioned yet')"
fi
if [ "$(env_get BITCOIN_INBOUND_ONION)" = "1" ]; then
echo " node onion: $(docker exec bitcoin bitcoin-cli -datadir=/data getnetworkinfo 2>/dev/null | jq -r '.localaddresses[0].address // "not registered yet"')"
fi
}
do_backup() {
mkdir -p backups
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
[ -f config.json ] && cp config.json "$tmp/"
[ -f .env ] && cp .env "$tmp/"
# bitcoind stores its inbound-onion key in the datadir
data_dir=$(env_get BITCOIN_DATA_DIR)
if [ -n "$data_dir" ] && [ -f "$data_dir/onion_v3_private_key" ]; then
cp "$data_dir/onion_v3_private_key" "$tmp/" 2>/dev/null ||
echo "WARN: node onion key not captured (unreadable — run backup as the node's user, uid 1000)"
fi
# the dashboard onion keys live in the tor volume
docker compose cp tor:/var/lib/tor/dashboard_onion "$tmp/dashboard_onion" >/dev/null 2>&1 ||
echo "WARN: dashboard onion keys not captured (is the tor container up?)"
# the watch-only roster (wallets.json) + balance history live in the dashboard
# state volume — UI-managed and not otherwise re-derivable
docker compose cp dashboard:/state "$tmp/dashboard_state" >/dev/null 2>&1 ||
echo "WARN: watch-only roster not captured (is the dashboard container up?)"
out="backups/stack-backup-$(date +%Y%m%d-%H%M%S).tar.gz"
tar czf "$out" -C "$tmp" .
chmod 600 "$out"
echo "Wrote $out (config, credentials, onion keys, watch-only roster — chain data excluded, it's re-derivable)"
}
do_restore() {
yes=0
if [ "${1:-}" = "-y" ]; then
yes=1
shift
fi
if [ -z "${1:-}" ]; then
echo "usage: ./stack restore [-y] <archive>"
exit 1
fi
archive=$1
if [ "$yes" != "1" ]; then
printf "This overwrites config.json, .env, and onion keys from %s. Continue? [y/N] " "$archive"
read -r reply
case "$reply" in y | Y) ;; *)
echo "Aborted."
exit 1
;;
esac
fi
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
tar xzf "$archive" -C "$tmp"
[ -f "$tmp/config.json" ] && cp "$tmp/config.json" config.json && echo "Restored config.json"
if [ -f "$tmp/.env" ]; then
cp "$tmp/.env" .env && chmod 600 .env && echo "Restored .env"
fi
data_dir=$(env_get BITCOIN_DATA_DIR)
if [ -f "$tmp/onion_v3_private_key" ] && [ -n "$data_dir" ]; then
cp "$tmp/onion_v3_private_key" "$data_dir/" && echo "Restored node onion key"
fi
if [ -d "$tmp/dashboard_onion" ]; then
if docker compose cp "$tmp/dashboard_onion" tor:/var/lib/tor/ >/dev/null 2>&1 &&
docker compose exec -u root tor chown -R tor:tor /var/lib/tor/dashboard_onion >/dev/null 2>&1; then
docker compose restart tor >/dev/null
echo "Restored dashboard onion keys (tor restarted)"
else
echo "WARN: could not restore dashboard onion keys (is the tor container up?)"
fi
fi
if [ -d "$tmp/dashboard_state" ]; then
# restore the roster into the volume; chown to the dashboard's uid (1000) in
# case the backup came from an older root-owned /state
if docker compose cp "$tmp/dashboard_state/." dashboard:/state >/dev/null 2>&1; then
docker compose exec -u root dashboard chown -R 1000:1000 /state >/dev/null 2>&1 || true
echo "Restored watch-only roster"
else
echo "WARN: could not restore watch-only roster (is the dashboard container up?)"
fi
fi
echo "Done. Apply with: ./stack apply"
}
# One-command upgrade: fetch the latest release tag and apply it. Backs up
# first, only ever moves forward (never downgrades), and prints a rollback line.
do_upgrade() {
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Not a git checkout — upgrade by unpacking the latest release tarball:"
echo " https://github.com/VijitSingh97/bitcoin-starter-stack/releases/latest"
exit 1
fi
current=$(cat VERSION 2>/dev/null || echo 0)
echo "Current: v$current — fetching release tags..."
git fetch --tags --quiet 2>/dev/null || {
echo "git fetch failed — check your network, then retry."
exit 1
}
latest=$(git tag --list --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
latest=${latest#v}
[ -n "$latest" ] || {
echo "No release tags found on the remote."
exit 1
}
if [ "$latest" = "$current" ]; then
echo "Already on the latest release (v$current). Nothing to do."
return 0
fi
if [ "$(printf '%s\n%s\n' "$current" "$latest" | sort -V | tail -1)" != "$latest" ]; then
echo "Installed v$current is newer than the latest release v$latest — nothing to do."
return 0
fi
echo "Upgrading v$current -> v$latest"
echo "== Backing up first (safety net)"
do_backup
echo "== Checking out v$latest"
git checkout --quiet "v$latest" || {
echo "checkout failed — commit or stash local changes to tracked files, then retry."
exit 1
}
echo "== Applying (pull new images, recreate changed containers)"
./configure.sh
docker compose up -d
echo
echo "Upgraded to v$latest. Verify with: ./stack status"
echo "Roll back if needed: git checkout \"v$current\" && ./stack apply"
}
# Host-side agent for the opt-in dashboard Upgrade button. The dashboard (which
# has no host/docker access) only drops a marker file in its state volume; this
# runs on the host, sees the marker, and performs the upgrade with the right
# privileges. up/apply manage it automatically via ensure_agent below; running
# it by hand just watches in the foreground.
upgrade_agent() {
echo "upgrade-agent: watching for dashboard upgrade requests (Ctrl-C to stop)."
echo " The button appears when dashboard.control=true; requests run ./stack upgrade here."
while true; do
if docker exec dashboard test -f /state/upgrade.request 2>/dev/null; then
docker exec -u 1000 dashboard rm -f /state/upgrade.request 2>/dev/null || true
echo "$(date '+%Y-%m-%d %H:%M:%S') upgrade-agent: request received — upgrading"
# subshell so do_upgrade's exit on a bad fetch/checkout doesn't kill the agent
(do_upgrade) || echo "upgrade-agent: upgrade did not complete (see above); still watching"
fi
sleep 10
done
}
# Keep the agent in sync with dashboard.control: when on, start it detached and
# install a @reboot cron entry (no sudo needed); when off, remove both. Skips
# entirely if the operator runs the agent under systemd (docs/operations.md).
# ponytail: cron can't restart a crashed agent until reboot — use the systemd
# unit if that matters to you.
ensure_agent() {
if systemctl is-enabled bitcoin-stack-upgrade-agent >/dev/null 2>&1; then
return 0 # managed by systemd — stay out of its way
fi
if [ "$(env_get DASHBOARD_CONTROL)" = "1" ]; then
line="@reboot sleep 60 && cd $PWD && ./stack upgrade-agent >> \$HOME/upgrade-agent.log 2>&1"
(crontab -l 2>/dev/null | grep -vF "stack upgrade-agent"; echo "$line") | crontab - 2>/dev/null ||
echo "WARN could not install the upgrade-agent @reboot cron entry"
if ! pgrep -f "stack upgrade-agent" >/dev/null 2>&1; then
(setsid ./stack upgrade-agent >>"$HOME/upgrade-agent.log" 2>&1 </dev/null &)
echo "upgrade-agent: started (log: ~/upgrade-agent.log; restarts on reboot via cron)"
fi
else
if crontab -l 2>/dev/null | grep -qF "stack upgrade-agent"; then
# grep -v exits 1 when ours is the only line — that's fine, write it empty
crontab -l 2>/dev/null | { grep -vF "stack upgrade-agent" || true; } | crontab -
fi
if pkill -f "stack upgrade-agen[t]" 2>/dev/null; then
echo "upgrade-agent: stopped (dashboard.control is off)"
fi
fi
}
cmd=${1:-help}
[ $# -gt 0 ] && shift
case "$cmd" in
up)
[ -f .env ] || ./configure.sh # first run: generate .env (auto-config)
docker compose up -d "$@"
ensure_agent
;;
init) init ;;
down) docker compose down "$@" ;;
restart) docker compose restart "$@" ;;
logs) docker compose logs -f --tail=100 "$@" ;;
apply)
./configure.sh
docker compose up -d
ensure_agent
;;
upgrade) do_upgrade ;;
upgrade-agent) upgrade_agent ;;
status) status ;;
doctor) doctor ;;
backup) do_backup ;;
restore) do_restore "$@" ;;
help | --help | -h) usage ;;
*)
echo "Unknown command: $cmd"
usage
exit 1
;;
esac