-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathinstall-dashboard.sh
More file actions
executable file
·277 lines (250 loc) · 12.5 KB
/
install-dashboard.sh
File metadata and controls
executable file
·277 lines (250 loc) · 12.5 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
#!/usr/bin/env bash
# install-dashboard.sh — deploy the OTLab status dashboard onto l3-mon-01.
#
# What it does:
# 1. rsyncs dashboard/ to /home/otuser/lab/dashboard/ (owned by otuser)
# 2. ensures Flask + Flask-HTTPAuth are in /home/otuser/lab/.venv-modern/
# 3. generates a self-signed TLS cert for the dashboard if missing
# 4. lays down /etc/sudoers.d/099_otuser_reboot (narrow NOPASSWD rule
# so the dashboard can self-reboot l3-mon-01 without full sudo)
# 5. generates an ed25519 SSH keypair for otuser if missing, prints the
# pubkey, and authorizes it as otadmin@<remote-pi> for every other Pi
# so remote reboots work without password
# 6. installs + enables otlab-dashboard.service
# 7. prints the URL + credentials
#
# Idempotent — safe to re-run anytime (refreshes files + service).
#
# Usage:
# ./scripts/install-dashboard.sh # default otadmin@RASPLC02.local
# ./scripts/install-dashboard.sh otadmin@192.168.120.19
#
# Pre-reqs:
# - l3-mon-01 has been through bootstrap-pi.sh (lab venv exists)
# - bootstrap-users.sh has run on l1-plc-01, l3-mon-01, l1-hp-01
# (so otadmin exists on each, NOPASSWD sudo)
set -euo pipefail
# ---------------------------------------------------------------------------
# Argument parsing — backward-compatible.
# ./install-dashboard.sh # default: deploy to l3-mon-01
# ./install-dashboard.sh otadmin@host # deploy to a specific host
# ./install-dashboard.sh otadmin@host --target-host=l3-mon-01
# # explicit target role.
# # The dashboard SSHes to all
# # other PLC Pis for reboot/
# # restart orchestration; the
# # REMOTE_PIS list below is
# # the canonical PLC inventory
# # (l3-mon-01 SSHes outward
# # to l1-plc-01 + l1-hp-01).
# ---------------------------------------------------------------------------
TARGET_HOST_ROLE="l3-mon-01" # default — back-compat
PI_HOST=""
for arg in "$@"; do
case "$arg" in
--target-host=*) TARGET_HOST_ROLE="${arg#*=}" ;;
--target-host) shift; TARGET_HOST_ROLE="$1" ;;
*) if [ -z "$PI_HOST" ]; then PI_HOST="$arg"; fi ;;
esac
done
PI_HOST="${PI_HOST:-otadmin@RASPLC02.local}"
DASH_SRC="dashboard"
DASH_DST="/home/otuser/lab/dashboard"
RUNTIME_USER="otuser"
# Remote-Pi pubkey-distribution list depends on which host we're deploying
# the dashboard ON. The dashboard SSHes to OTHER Pis for reboot/restart, so
# the list is "all PLC Pis except the one we're deploying to".
case "$TARGET_HOST_ROLE" in
l3-mon-01)
# Canonical deployment — dashboard on l3-mon-01 (L3 monitoring host).
# SSH to l1-plc-01 + l1-hp-01 + (future) l1-plc-02.
REMOTE_PIS=( "192.168.120.216" "192.168.120.48" ) # mgmt IPs (laptop-reachable)
REMOTE_LAB_IPS=( "10.20.30.47" "10.20.30.48" ) # lab IPs (dashboard-side)
# When l1-plc-02 backfills, append its IPs here.
;;
*)
echo "ERROR: --target-host must be 'l3-mon-01' (got: $TARGET_HOST_ROLE)" >&2
echo " Other hosts can run a dashboard during transition (e.g. on a PLC for testing)," >&2
echo " but the canonical role is l3-mon-01." >&2
exit 1
;;
esac
echo "==> deploying OTLab dashboard to $PI_HOST (target-host role: $TARGET_HOST_ROLE)"
# ---------------------------------------------------------------------------
# 1. Sync source tree
# ---------------------------------------------------------------------------
echo "==> rsyncing $DASH_SRC/ -> $PI_HOST:/tmp/dashboard-stage/"
rsync -a --delete "${DASH_SRC}/" "${PI_HOST}:/tmp/dashboard-stage/"
ssh "$PI_HOST" "
set -e
sudo -u ${RUNTIME_USER} mkdir -p ${DASH_DST}
sudo rsync -a --delete --chown=${RUNTIME_USER}:${RUNTIME_USER} \
/tmp/dashboard-stage/ ${DASH_DST}/
rm -rf /tmp/dashboard-stage
# Don't ship dashboard.env from the repo example over the live one.
# Test as otuser — otadmin can't traverse into /home/otuser (mode 700).
if ! sudo -u ${RUNTIME_USER} test -f ${DASH_DST}/dashboard.env; then
sudo install -m 0640 -o ${RUNTIME_USER} -g ${RUNTIME_USER} \
${DASH_DST}/dashboard.env.example ${DASH_DST}/dashboard.env
echo ' laid down ${DASH_DST}/dashboard.env from example'
else
echo ' ${DASH_DST}/dashboard.env already exists — leaving alone'
fi
"
# ---------------------------------------------------------------------------
# 2. venv deps (Flask + Flask-HTTPAuth — pymodbus already installed by
# bootstrap-pi.sh into /home/otuser/lab/.venv-modern/)
# ---------------------------------------------------------------------------
echo "==> ensuring Flask + Flask-HTTPAuth in /home/otuser/lab/.venv-modern"
ssh "$PI_HOST" "
sudo -u ${RUNTIME_USER} /home/${RUNTIME_USER}/lab/.venv-modern/bin/pip install --quiet \
flask flask-httpauth
"
# ---------------------------------------------------------------------------
# 3. Self-signed TLS cert for the dashboard
# ---------------------------------------------------------------------------
echo "==> ensuring self-signed TLS cert exists"
ssh "$PI_HOST" "
set -e
if ! sudo -u ${RUNTIME_USER} test -f ${DASH_DST}/cert.pem \
|| ! sudo -u ${RUNTIME_USER} test -f ${DASH_DST}/key.pem; then
sudo -u ${RUNTIME_USER} openssl req -x509 -newkey rsa:2048 -nodes \
-keyout ${DASH_DST}/key.pem \
-out ${DASH_DST}/cert.pem \
-days 3650 \
-subj '/CN=otlab-dashboard/O=Maple Ridge Treatment Plant/OU=ICS Village' \
-addext 'subjectAltName=DNS:RASPLC02.local,DNS:rasplc02,DNS:localhost,IP:127.0.0.1,IP:192.168.120.19,IP:10.20.30.49,IP:100.77.255.56' \
>/dev/null 2>&1
sudo chmod 600 ${DASH_DST}/key.pem
sudo chmod 644 ${DASH_DST}/cert.pem
echo ' generated cert.pem + key.pem (10 yr, self-signed)'
else
echo ' cert + key already exist — skipping'
fi
"
# ---------------------------------------------------------------------------
# 4. Sudoers drop-in: narrow NOPASSWD for self-reboot only
# ---------------------------------------------------------------------------
echo "==> sudoers rule for otuser self-reboot + tcpdump"
ssh "$PI_HOST" "
sudo tee /etc/sudoers.d/099_otuser_reboot >/dev/null <<EOF
# OTLab dashboard runtime (otuser) needs to:
# 1. self-reboot l3-mon-01 (from the dashboard's Reboot button)
# 2. run tcpdump for the dashboard's pcap-capture feature
# 3. timeout(1) wraps tcpdump for fixed-duration captures
# 4. restart specific services (granular alternative to full reboot)
otuser ALL=(ALL) NOPASSWD: /bin/systemctl reboot, /usr/bin/systemctl reboot, /usr/bin/tcpdump, /usr/bin/timeout, /bin/systemctl restart sensor-sim, /bin/systemctl restart openplc, /bin/systemctl restart otlab-dashboard, /usr/bin/systemctl restart sensor-sim, /usr/bin/systemctl restart openplc, /usr/bin/systemctl restart otlab-dashboard
EOF
sudo chmod 440 /etc/sudoers.d/099_otuser_reboot
echo ' /etc/sudoers.d/099_otuser_reboot installed'
"
echo "==> ensuring captures + ssh-cm directories exist"
ssh "$PI_HOST" "
sudo -u ${RUNTIME_USER} mkdir -p ${DASH_DST}/captures ${DASH_DST}/.ssh-cm
sudo -u ${RUNTIME_USER} chmod 750 ${DASH_DST}/captures
sudo -u ${RUNTIME_USER} chmod 700 ${DASH_DST}/.ssh-cm
echo ' ${DASH_DST}/captures + .ssh-cm ready'
"
# ---------------------------------------------------------------------------
# 5. SSH keypair for otuser, plus authorize on remote otadmin accounts
# ---------------------------------------------------------------------------
echo "==> ensuring otuser has an SSH keypair for remote reboots"
PUBKEY=$(ssh "$PI_HOST" "
set -e
# Run the existence test as otuser — otadmin can't see into otuser's
# mode-700 ~/.ssh, so a plain '[ -f ... ]' here always returns false
# and we'd re-run ssh-keygen every time, which then prompts to
# overwrite and silently fails.
if ! sudo -u ${RUNTIME_USER} test -f /home/${RUNTIME_USER}/.ssh/id_ed25519; then
sudo -u ${RUNTIME_USER} mkdir -p /home/${RUNTIME_USER}/.ssh
sudo -u ${RUNTIME_USER} chmod 700 /home/${RUNTIME_USER}/.ssh
sudo -u ${RUNTIME_USER} ssh-keygen -t ed25519 -N '' \
-C 'otuser@l3-mon-01 dashboard reboot key' \
-f /home/${RUNTIME_USER}/.ssh/id_ed25519 >/dev/null
fi
sudo cat /home/${RUNTIME_USER}/.ssh/id_ed25519.pub
")
echo " pubkey: $PUBKEY"
# Stage the pubkey on the local host so we can ssh-copy-id it to remotes.
TMP_PUB="$(mktemp)"
echo "$PUBKEY" > "$TMP_PUB"
# For each remote Pi, append the pubkey to otadmin's authorized_keys (idempotent).
for remote in "${REMOTE_PIS[@]}"; do
echo "==> authorizing otuser@l3-mon-01 -> otadmin@${remote}"
if ssh -o BatchMode=yes -o ConnectTimeout=5 "otadmin@${remote}" true 2>/dev/null; then
ssh "otadmin@${remote}" "
mkdir -p ~/.ssh && chmod 700 ~/.ssh
grep -qF '$PUBKEY' ~/.ssh/authorized_keys 2>/dev/null \
|| echo '$PUBKEY' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
"
echo " ${remote} authorized"
else
echo " WARN: cannot reach otadmin@${remote} — skipping (reboot from dashboard for that host will fail until you authorize manually)"
fi
done
# Pre-warm known_hosts for both remotes from l3-mon-01's otuser side, so
# ssh from the dashboard doesn't trip on host-key prompts. Scan both the
# mgmt and the lab IPs since the dashboard reboot path uses the lab IPs
# (most reliable from l3-mon-01).
ssh "$PI_HOST" "
sudo -u ${RUNTIME_USER} bash -c '
for h in ${REMOTE_PIS[@]} ${REMOTE_LAB_IPS[@]}; do
ssh-keyscan -t ed25519 -T 5 \"\$h\" 2>/dev/null >> /home/${RUNTIME_USER}/.ssh/known_hosts || true
done
sort -u /home/${RUNTIME_USER}/.ssh/known_hosts -o /home/${RUNTIME_USER}/.ssh/known_hosts
'
"
rm -f "$TMP_PUB"
# ---------------------------------------------------------------------------
# 6. systemd unit
# ---------------------------------------------------------------------------
echo "==> installing systemd unit + (re)starting service"
ssh "$PI_HOST" "
sudo install -m 0644 ${DASH_DST}/otlab-dashboard.service \
/etc/systemd/system/otlab-dashboard.service
sudo systemctl daemon-reload
sudo systemctl enable --quiet otlab-dashboard
sudo systemctl restart otlab-dashboard
"
sleep 2
echo "==> status"
ssh "$PI_HOST" 'sudo systemctl status otlab-dashboard --no-pager 2>&1 | head -12'
echo
echo "==> recent journal"
ssh "$PI_HOST" 'sudo journalctl -u otlab-dashboard -n 5 --no-pager 2>&1' || true
# ---------------------------------------------------------------------------
# 7a. Stamp /etc/otlab-bootstrap-info for the dashboard's last-bootstrap card.
# ---------------------------------------------------------------------------
COMMIT="$(git -C "$(dirname "$0")/.." rev-parse --short HEAD 2>/dev/null || echo unknown)"
TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
SCRIPT="$(basename "$0")"
ssh "$PI_HOST" "
sudo tee /etc/otlab-bootstrap-info >/dev/null <<EOF
ts=$TS
commit=$COMMIT
script=$SCRIPT
EOF
sudo chmod 644 /etc/otlab-bootstrap-info
"
# ---------------------------------------------------------------------------
# 7. Summary
# ---------------------------------------------------------------------------
HOST_BARE="${PI_HOST##*@}"
# Use sudo grep — dashboard.env is mode 0640 owned by otuser, otadmin can't
# read it directly across the mode-700 home dir.
DASH_USER_VAL=$(ssh "$PI_HOST" "sudo grep '^DASH_USER=' ${DASH_DST}/dashboard.env | cut -d= -f2")
DASH_PASS_VAL=$(ssh "$PI_HOST" "sudo grep '^DASH_PASS=' ${DASH_DST}/dashboard.env | cut -d= -f2")
cat <<EOF
==============================================================================
OTLab Dashboard deployed.
URL: https://${HOST_BARE}:8000/ (also try https://192.168.120.19:8000/)
user: ${DASH_USER_VAL}
pass: ${DASH_PASS_VAL}
The cert is self-signed — your browser will warn. Click through.
Logs: ssh ${PI_HOST} 'sudo journalctl -u otlab-dashboard -f'
Restart: ssh ${PI_HOST} 'sudo systemctl restart otlab-dashboard'
Edit ${DASH_DST}/dashboard.env on the Pi to rotate the password,
then restart the service.
==============================================================================
EOF