frugal-iot-server 2.0.11 → 2.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/INSTALLATION.md +128 -31
- package/docs/OPENWRT.md +2 -2
- package/package.json +5 -3
- package/public/service-worker.js +1 -1
- package/scripts/addbridge-prod.zsh +15 -19
- package/scripts/addorganization.zsh +9 -3
- package/scripts/backup.zsh +361 -0
- package/scripts/broker-config.zsh +185 -0
- package/scripts/diagnostic.zsh +122 -83
- package/scripts/init.zsh +161 -84
- package/scripts/not-published.txt +1 -0
- package/scripts/openwrt-preflight.sh +14 -2
- package/scripts/restore.zsh +375 -0
package/scripts/diagnostic.zsh
CHANGED
|
@@ -133,36 +133,59 @@ if [[ -f frugal-iot.db ]] && have sqlite3; then
|
|
|
133
133
|
fi
|
|
134
134
|
|
|
135
135
|
section "Mosquitto configuration"
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
# Which files the broker reads, asked of its own configuration - production's is several hand-made
|
|
137
|
+
# files in conf.d, a Pi's is one frugal-iot.conf. scripts/broker-config.zsh explains the sudo retry,
|
|
138
|
+
# which is per file: a root-only bridge file used to be skipped silently among readable ones.
|
|
139
|
+
# docs/INSTALLATION.md has this script copied on its own to a machine not yet installed, so the
|
|
140
|
+
# helper may not be beside it - look in the installed package too before giving up.
|
|
141
|
+
BROKER_HELPER=""
|
|
142
|
+
for f in "${0:A:h}/broker-config.zsh" node_modules/frugal-iot-server/scripts/broker-config.zsh; do
|
|
143
|
+
[[ -r "$f" ]] && { BROKER_HELPER="$f"; break; }
|
|
144
|
+
done
|
|
145
|
+
if [[ -z "$BROKER_HELPER" ]]; then
|
|
146
|
+
print -u3 "diagnostic.zsh needs broker-config.zsh beside it - copy that from the same scripts/ directory"
|
|
147
|
+
exit 2
|
|
148
|
+
fi
|
|
149
|
+
source "$BROKER_HELPER"
|
|
150
|
+
broker_conf_read
|
|
151
|
+
if (( ! ${#BROKER_CONF_FILES} )); then
|
|
152
|
+
item "$(broker_conf_main): missing - is mosquitto installed?"
|
|
138
153
|
else
|
|
139
|
-
item "
|
|
140
|
-
|
|
141
|
-
item "
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
154
|
+
item "files the broker reads, in order:"
|
|
155
|
+
for f in $BROKER_CONF_FILES; do
|
|
156
|
+
item " ${f}: $(fileinfo $f)"
|
|
157
|
+
# Read through the helper rather than grep, so a root-only file is shown as well
|
|
158
|
+
print -r -- "$(broker_conf_cat $f 2>/dev/null)" | grep -nE '^[[:space:]]*(listener|protocol|password_file|allow_anonymous)' | sed 's/^/ /'
|
|
159
|
+
done
|
|
160
|
+
(( ${#BROKER_CONF_FILES} == 1 )) && problem "$BROKER_CONF_FILES reads no other files - the Frugal IoT config was never copied into conf.d (step 5)"
|
|
161
|
+
fi
|
|
162
|
+
if (( ${#BROKER_CONF_UNREADABLE} )); then
|
|
163
|
+
item "NOT READABLE as $(id -un), even with sudo -n: ${(j:, :)BROKER_CONF_UNREADABLE}"
|
|
164
|
+
item " everything below about the broker is missing whatever those files say - run with sudo"
|
|
165
|
+
problem "Could not read ${(j:, :)BROKER_CONF_UNREADABLE} - re-run with sudo for a complete answer about the broker"
|
|
166
|
+
fi
|
|
167
|
+
item "files the configuration names, and what a backup does with each (the files above are taken too):"
|
|
168
|
+
# NAMED_FILE, not "path": zsh ties $path to $PATH, so reading into it wipes out every command after
|
|
169
|
+
broker_named_files | while read -r setting NAMED_FILE role; do
|
|
170
|
+
case $role in
|
|
171
|
+
backup) role="taken" ;;
|
|
172
|
+
tls) role="taken (TLS)" ;;
|
|
173
|
+
rebuildable) role="taken - its admin account matches secrets.yaml; the rest is rebuilt from the db" ;;
|
|
174
|
+
package) role="not taken - part of mosquitto" ;;
|
|
175
|
+
live) role="not taken - running state" ;;
|
|
176
|
+
esac
|
|
177
|
+
# A certificate under /etc/letsencrypt/live sits in a root-only directory, so it is invisible to
|
|
178
|
+
# an ordinary user - which is not the same thing as missing.
|
|
179
|
+
if [[ ! -e "$NAMED_FILE" ]] && have sudo && sudo -n test -e "$NAMED_FILE" 2>/dev/null; then
|
|
180
|
+
item " ${(r:22:)setting} ${NAMED_FILE}: present, needs root to see"
|
|
181
|
+
else
|
|
182
|
+
item " ${(r:22:)setting} ${NAMED_FILE}: $(fileinfo $NAMED_FILE)"
|
|
183
|
+
fi
|
|
184
|
+
item " -> ${role}"
|
|
145
185
|
done
|
|
146
|
-
[[ -z "$(print -r -- /etc/mosquitto/conf.d/*(N))" ]] && problem "Nothing in /etc/mosquitto/conf.d - the Frugal IoT config was never copied there (step 5)"
|
|
147
|
-
|
|
148
|
-
# The files the broker actually reads. mosquitto's include_dir only loads names ending in .conf,
|
|
149
|
-
# so look at exactly those - and the last setting wins, hence "tail -1" everywhere below.
|
|
150
|
-
BROKER_CONF_FILES=(/etc/mosquitto/mosquitto.conf(N) /etc/mosquitto/conf.d/*.conf(N))
|
|
151
186
|
|
|
152
|
-
# Everything the broker reads, as one stream
|
|
153
|
-
|
|
154
|
-
# an error, which is indistinguishable from "that setting is not configured". Left unguarded, the
|
|
155
|
-
# comparison below reported a broker as missing EVERY setting this release ships, which as a
|
|
156
|
-
# periodic check is worse than no check at all.
|
|
157
|
-
broker_conf_text() {
|
|
158
|
-
local t
|
|
159
|
-
(( ${#BROKER_CONF_FILES} )) || return 0
|
|
160
|
-
t=$(cat "${BROKER_CONF_FILES[@]}" 2>/dev/null) || true
|
|
161
|
-
if [[ -z "$t" ]] && have sudo; then
|
|
162
|
-
t=$(sudo -n cat "${BROKER_CONF_FILES[@]}" 2>/dev/null) || true
|
|
163
|
-
fi
|
|
164
|
-
print -r -- "$t"
|
|
165
|
-
}
|
|
187
|
+
# Everything the broker reads, as one stream, comments removed
|
|
188
|
+
broker_conf_text() { print -r -- "$BROKER_CONF_TEXT"; }
|
|
166
189
|
|
|
167
190
|
# Where the packaged copy of a shipped file is. What is in ./extras is NOT it: frugal-iot-init
|
|
168
191
|
# copies extras/ in and then never overwrites it, so once a release changes one of these files the
|
|
@@ -170,8 +193,38 @@ broker_conf_text() {
|
|
|
170
193
|
PKG_EXTRAS="node_modules/frugal-iot-server/extras"
|
|
171
194
|
[[ -d "$PKG_EXTRAS" ]] || PKG_EXTRAS="${0:A:h:h}/extras"
|
|
172
195
|
|
|
196
|
+
# What this release ships, against what the broker is actually running.
|
|
197
|
+
#
|
|
198
|
+
# This used to be "diff ./extras/mosquitto.conf /etc/mosquitto/conf.d/frugal-iot.conf", which
|
|
199
|
+
# reported a problem on every machine there has ever been, for three separate reasons: ./extras is
|
|
200
|
+
# a fossil frugal-iot-init will not overwrite, so it differs from the release for ever; the
|
|
201
|
+
# installed copy has the dynamic-security plugin's path substituted, so it differs from the shipped
|
|
202
|
+
# one by design; and "diff -q" against a frugal-iot.conf that does not exist fails too, which is
|
|
203
|
+
# the normal state of a broker whose configuration was assembled by hand as several small files in
|
|
204
|
+
# conf.d. A check that is always wrong is one you stop reading, so this asks the question that
|
|
205
|
+
# actually matters instead: is the broker missing a setting this release ships?
|
|
206
|
+
if [[ -f "${PKG_EXTRAS}/mosquitto.conf" ]] && (( ${#BROKER_CONF_FILES} )); then
|
|
207
|
+
# Setting by setting against what the broker reads, wherever it reads it from - see
|
|
208
|
+
# broker_conf_compare. A file kept at another path is information, not a fault.
|
|
209
|
+
CONF_FINDINGS=$(broker_conf_compare "${PKG_EXTRAS}/mosquitto.conf")
|
|
210
|
+
if [[ -z "$CONF_FINDINGS" ]]; then
|
|
211
|
+
item "the broker has every setting this release ships"
|
|
212
|
+
else
|
|
213
|
+
item "compared with this release's mosquitto.conf:"
|
|
214
|
+
CONF_REAL=0
|
|
215
|
+
for l in ${(f)CONF_FINDINGS}; do
|
|
216
|
+
case ${l%%$'\t'*} in
|
|
217
|
+
missing) item " missing: ${l#*$'\t'}"; (( CONF_REAL++ )) || true ;;
|
|
218
|
+
differs) item " different: ${l#*$'\t'}"; (( CONF_REAL++ )) || true ;;
|
|
219
|
+
elsewhere) item " (fine) ${l#*$'\t'}" ;;
|
|
220
|
+
esac
|
|
221
|
+
done
|
|
222
|
+
(( CONF_REAL )) && problem "The broker's configuration differs from this release's in ${CONF_REAL} setting(s) - see \"Mosquitto configuration\" above. Add them to a file in /etc/mosquitto/conf.d/ (or re-run install-pi.sh on a Pi it set up), then: sudo systemctl restart mosquitto"
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
173
226
|
# Which password file does the running configuration actually name?
|
|
174
|
-
|
|
227
|
+
broker_setting password_file; CONFIGURED_PWFILE="$BROKER_SETTING"
|
|
175
228
|
section "Mosquitto password file"
|
|
176
229
|
if [[ -n "$CONFIGURED_PWFILE" ]]; then
|
|
177
230
|
item "configuration names: $CONFIGURED_PWFILE"
|
|
@@ -180,44 +233,15 @@ if [[ -n "$CONFIGURED_PWFILE" ]]; then
|
|
|
180
233
|
problem "Mosquitto is configured to use $CONFIGURED_PWFILE but that file does not exist - it will refuse to start"
|
|
181
234
|
else
|
|
182
235
|
# Account names only, never the hashes
|
|
183
|
-
|
|
236
|
+
# Read through the helper: the file is mosquitto:mosquitto 600, so a plain cut as the service
|
|
237
|
+
# user returns nothing, which looks exactly like a file with no accounts in it
|
|
238
|
+
item " accounts in it: $(broker_conf_cat "$CONFIGURED_PWFILE" | cut -d: -f1 | tr '\n' ' ')"
|
|
184
239
|
[[ ! -s "$CONFIGURED_PWFILE" ]] && item " (the file is empty - no accounts yet)"
|
|
185
240
|
fi
|
|
186
241
|
else
|
|
187
242
|
item "no password_file line found in any mosquitto configuration"
|
|
188
243
|
problem "No password_file configured - the broker would allow anonymous access or reject everything"
|
|
189
244
|
fi
|
|
190
|
-
# What this release ships, against what the broker is actually running.
|
|
191
|
-
#
|
|
192
|
-
# This used to be "diff ./extras/mosquitto.conf /etc/mosquitto/conf.d/frugal-iot.conf", which
|
|
193
|
-
# reported a problem on every machine there has ever been, for three separate reasons: ./extras is
|
|
194
|
-
# a fossil frugal-iot-init will not overwrite, so it differs from the release for ever; the
|
|
195
|
-
# installed copy has the dynamic-security plugin's path substituted, so it differs from the shipped
|
|
196
|
-
# one by design; and "diff -q" against a frugal-iot.conf that does not exist fails too, which is
|
|
197
|
-
# the normal state of a broker whose configuration was assembled by hand as several small files in
|
|
198
|
-
# conf.d. A check that is always wrong is one you stop reading, so this asks the question that
|
|
199
|
-
# actually matters instead: is the broker missing a setting this release ships?
|
|
200
|
-
if [[ -f "${PKG_EXTRAS}/mosquitto.conf" ]]; then
|
|
201
|
-
item "this release's mosquitto.conf names: $(grep -hE '^[[:space:]]*password_file' "${PKG_EXTRAS}/mosquitto.conf" 2>/dev/null | awk '{print $2}')"
|
|
202
|
-
if (( ${#BROKER_CONF_FILES} )); then
|
|
203
|
-
# The plugin line is normalised: the installer substitutes a path that varies by distribution.
|
|
204
|
-
# Comparing settings rather than files means this works whether the configuration arrived as one
|
|
205
|
-
# frugal-iot.conf or as several hand-written pieces.
|
|
206
|
-
MISSING_CONF=$(comm -23 \
|
|
207
|
-
<(sed -E 's#^plugin .*#plugin -#' "${PKG_EXTRAS}/mosquitto.conf" | grep -vE '^[[:space:]]*(#|$)' | sort -u) \
|
|
208
|
-
<(broker_conf_text | sed -E 's#^plugin .*#plugin -#' | grep -vE '^[[:space:]]*(#|$)' | sort -u) 2>/dev/null)
|
|
209
|
-
# password_file and the listeners are allowed to be sited differently on an older installation,
|
|
210
|
-
# so a difference in those is reported without being called a fault.
|
|
211
|
-
MISSING_REAL=$(print -r -- "$MISSING_CONF" | grep -vE '^[[:space:]]*(password_file|listener|protocol)[[:space:]]' || true)
|
|
212
|
-
if [[ -n "$MISSING_CONF" ]]; then
|
|
213
|
-
item "settings this release ships that the broker does not have:"
|
|
214
|
-
print -r -- "$MISSING_CONF" | while IFS= read -r l; do [[ -n "$l" ]] && item " $l"; done
|
|
215
|
-
else
|
|
216
|
-
item "the broker has every setting this release ships"
|
|
217
|
-
fi
|
|
218
|
-
[[ -n "$MISSING_REAL" ]] && problem "The broker is missing $(print -r -- "$MISSING_REAL" | grep -c .) setting(s) this release ships - see \"Mosquitto password file\" above. Re-run install-pi.sh, or add them by hand if this broker's config.d was written by hand."
|
|
219
|
-
fi
|
|
220
|
-
fi
|
|
221
245
|
item "candidate locations, whether or not configured:"
|
|
222
246
|
for f in /var/lib/mosquitto/passwords /etc/mosquitto/mosquitto_passwords; do
|
|
223
247
|
item " ${f}: $(fileinfo $f)"
|
|
@@ -238,7 +262,7 @@ else
|
|
|
238
262
|
item "systemctl not available on this machine"
|
|
239
263
|
fi
|
|
240
264
|
# Debian's mosquitto logs to a file rather than the journal, so this is where the real reason is.
|
|
241
|
-
MOSQUITTO_LOG=$(
|
|
265
|
+
MOSQUITTO_LOG=$(broker_conf_lines log_dest | awk '$2 == "file" {print $3}' | tail -1)
|
|
242
266
|
[[ -z "$MOSQUITTO_LOG" ]] && MOSQUITTO_LOG=/var/log/mosquitto/mosquitto.log
|
|
243
267
|
item "its own log file: ${MOSQUITTO_LOG} ($(fileinfo $MOSQUITTO_LOG))"
|
|
244
268
|
if [[ -r "$MOSQUITTO_LOG" ]]; then
|
|
@@ -347,11 +371,6 @@ section "Broker access control"
|
|
|
347
371
|
# An acl_file confines each account to its own organization's topics; without one, every account
|
|
348
372
|
# that can log in reaches every topic on the broker. Reported here because a broker installed before
|
|
349
373
|
# the ACL shipped will not have one, and nothing else says so.
|
|
350
|
-
BROKER_SETTING=""
|
|
351
|
-
broker_setting() { # name -> value from the broker's own configuration, sudo only if needed
|
|
352
|
-
# broker_conf_text carries the sudo fallback this used to spell out for itself
|
|
353
|
-
BROKER_SETTING=$(broker_conf_text | grep -hE "^[[:space:]]*$1[[:space:]]" | tail -1 | awk '{print $2}') || true
|
|
354
|
-
}
|
|
355
374
|
broker_setting acl_file; ACLFILE="$BROKER_SETTING"
|
|
356
375
|
if [[ -z "$ACLFILE" ]]; then
|
|
357
376
|
item "acl_file: NOT SET - every account that can log in can reach every topic, across all organizations"
|
|
@@ -495,24 +514,13 @@ section "Bridge to a production server (optional)"
|
|
|
495
514
|
# A bridge relays this Pi's readings to a production server - see
|
|
496
515
|
# extras/mosquitto-bridge.conf.example. It is configured by a "connection" line in one of
|
|
497
516
|
# mosquitto's own config files, so most installations have none, which is not a problem.
|
|
498
|
-
BRIDGECONFS=(/etc/mosquitto/mosquitto.conf(N) /etc/mosquitto/conf.d/*.conf(N))
|
|
499
517
|
# The bridge's own file is 600 root, because it holds the bridge's password - so read as an ordinary
|
|
500
518
|
# user it yields nothing, which looked exactly like "no bridge configured" on a Pi whose bridge was
|
|
501
|
-
# configured and failing.
|
|
502
|
-
#
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
BRIDGETEXT+="$(<$f)"$'\n'
|
|
507
|
-
elif have sudo && t=$(sudo -n cat $f 2>/dev/null); then
|
|
508
|
-
BRIDGETEXT+="$t"$'\n'
|
|
509
|
-
else
|
|
510
|
-
BRIDGEUNREAD+=($f)
|
|
511
|
-
fi
|
|
512
|
-
done
|
|
513
|
-
BRIDGENAMES=$(print -r -- "$BRIDGETEXT" | grep -E '^[[:space:]]*connection[[:space:]]' | awk '{print $2}')
|
|
514
|
-
if [[ -z "$BRIDGENAMES" ]] && (( ${#BRIDGEUNREAD} )); then
|
|
515
|
-
item "cannot tell whether a bridge is configured: not readable as $(id -un) - ${(j:, :)BRIDGEUNREAD}"
|
|
519
|
+
# configured and failing. broker_conf_read retried it with sudo; say so when even that failed rather
|
|
520
|
+
# than report an answer that was never looked for.
|
|
521
|
+
BRIDGENAMES=$(broker_conf_lines connection | awk '{print $2}')
|
|
522
|
+
if [[ -z "$BRIDGENAMES" ]] && (( ${#BROKER_CONF_UNREADABLE} )); then
|
|
523
|
+
item "cannot tell whether a bridge is configured: not readable as $(id -un) - ${(j:, :)BROKER_CONF_UNREADABLE}"
|
|
516
524
|
item " sudo npx --no frugal-iot-diagnostic reads them"
|
|
517
525
|
elif [[ -z "$BRIDGENAMES" ]]; then
|
|
518
526
|
item "no bridge configured - readings stay on this Pi"
|
|
@@ -520,7 +528,7 @@ else
|
|
|
520
528
|
for b in ${(f)BRIDGENAMES}; do item "configured bridge: $b"; done
|
|
521
529
|
# What it is set to relay and where. remote_password is deliberately not among these.
|
|
522
530
|
item "its settings:"
|
|
523
|
-
|
|
531
|
+
broker_conf_lines 'address|remote_username|remote_clientid|topic|cleansession|restart_timeout|notifications' | sed 's/^/ /'
|
|
524
532
|
# Whether it is actually up, rather than what the configuration hoped for. With "notifications"
|
|
525
533
|
# left on, the bridge publishes a retained 1 or 0 here each time it connects or drops.
|
|
526
534
|
if have mosquitto_sub; then
|
|
@@ -583,6 +591,37 @@ if [[ -d data ]]; then
|
|
|
583
591
|
fi
|
|
584
592
|
fi
|
|
585
593
|
|
|
594
|
+
section "Backups"
|
|
595
|
+
# A nightly timer that has quietly stopped looks exactly like one that works, so say how old the
|
|
596
|
+
# newest snapshot is. Only a problem once the timer is installed - before that, nothing promised one.
|
|
597
|
+
BACKUP_DIR=/var/backups/frugal-iot
|
|
598
|
+
BACKUP_TIMER=""
|
|
599
|
+
have systemctl && { BACKUP_TIMER=$(systemctl is-enabled frugal-iot-backup.timer 2>/dev/null) || true; }
|
|
600
|
+
# is-enabled answers "not-found" for a unit that does not exist, rather than nothing
|
|
601
|
+
[[ "$BACKUP_TIMER" == not-found ]] && BACKUP_TIMER=""
|
|
602
|
+
item "timer: ${BACKUP_TIMER:-not installed - npx --no frugal-iot-backup install}"
|
|
603
|
+
# The directory is 700, owned by the account the server runs as - so sudo for anyone else
|
|
604
|
+
NEWEST_BACKUP=$( (ls -1t "$BACKUP_DIR" 2>/dev/null || { have sudo && sudo -n ls -1t "$BACKUP_DIR" 2>/dev/null; }) |
|
|
605
|
+
grep -m1 '^frugal-iot-state-.*\.tar\.gz$') || true
|
|
606
|
+
if [[ -n "$NEWEST_BACKUP" ]]; then
|
|
607
|
+
# The time is in the name, which saves a stat that might need sudo too
|
|
608
|
+
BACKUP_STAMP=${${NEWEST_BACKUP%.tar.gz}[-15,-1]} # YYYYmmdd-HHMMSS
|
|
609
|
+
zmodload zsh/datetime 2>/dev/null
|
|
610
|
+
if strftime -r -s BACKUP_SECS '%Y%m%d-%H%M%S' "$BACKUP_STAMP" 2>/dev/null; then
|
|
611
|
+
BACKUP_HOURS=$(( (EPOCHSECONDS - BACKUP_SECS) / 3600 ))
|
|
612
|
+
item "newest: ${BACKUP_DIR}/${NEWEST_BACKUP} (${BACKUP_HOURS} hours old)"
|
|
613
|
+
[[ "$BACKUP_TIMER" == enabled ]] && (( BACKUP_HOURS > 48 )) &&
|
|
614
|
+
problem "The newest backup is ${BACKUP_HOURS} hours old although the nightly timer is enabled - see: journalctl -u frugal-iot-backup"
|
|
615
|
+
else
|
|
616
|
+
item "newest: ${BACKUP_DIR}/${NEWEST_BACKUP}"
|
|
617
|
+
fi
|
|
618
|
+
elif [[ "$BACKUP_TIMER" == enabled ]]; then
|
|
619
|
+
item "newest: none in ${BACKUP_DIR} (or not readable as $(id -un))"
|
|
620
|
+
problem "The backup timer is enabled but there is no snapshot in ${BACKUP_DIR} - see: journalctl -u frugal-iot-backup"
|
|
621
|
+
else
|
|
622
|
+
item "newest: none"
|
|
623
|
+
fi
|
|
624
|
+
|
|
586
625
|
section "Wear on the SD card"
|
|
587
626
|
# An SD card wears out from being written to, and a server logging sensor readings writes constantly
|
|
588
627
|
# unless told not to. This reports how much is actually being written and whether the settings that
|
package/scripts/init.zsh
CHANGED
|
@@ -31,18 +31,33 @@ fi
|
|
|
31
31
|
COPIED=() # which files this run actually created, so existing ones are never rewritten below
|
|
32
32
|
DIFFER_TO=() # files kept that no longer match what this release ships, and where to compare them
|
|
33
33
|
DIFFER_FROM=()
|
|
34
|
+
DIFFER_OURS=() # ...and the lines in each that this release does not have
|
|
34
35
|
# copy_if_missing <packaged file> <local file> [compare]
|
|
35
|
-
# "compare"
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
36
|
+
# "compare" is for a file a release extends - the schema - and that someone may also have added to
|
|
37
|
+
# here. When it differs, the question is whether this copy has anything the release does not:
|
|
38
|
+
# - nothing: every line here is still in the release's version, so all the release did was add
|
|
39
|
+
# (a new sensor type, a new field). Installing it loses nothing, so it is installed. Asking
|
|
40
|
+
# instead was a question with only one answer, put to someone who then had to read a diff of a
|
|
41
|
+
# file they never touched to find that out.
|
|
42
|
+
# - something: either it was added here, or the release changed or removed it - and only a person
|
|
43
|
+
# can tell which. So it is kept, and those lines, and only those, are shown.
|
|
44
|
+
# Only pass "compare" for files like that - config.yaml, mqtt.yaml and server.yaml are this
|
|
45
|
+
# installation's own settings, so of course they differ, and warning about them every time would
|
|
46
|
+
# train you to ignore the warning that matters.
|
|
39
47
|
copy_if_missing() {
|
|
40
|
-
local from=$1 to=$2 compare=${3:-}
|
|
48
|
+
local from=$1 to=$2 compare=${3:-} ours
|
|
41
49
|
if [[ -e "$to" ]]; then
|
|
42
50
|
if [[ "$compare" == compare && -e "$from" ]] && ! cmp -s "$from" "$to"; then
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
ours=$(diff "$to" "$from" | sed -n 's/^< //p') || true
|
|
52
|
+
if [[ -z "$ours" ]]; then
|
|
53
|
+
cp "$from" "$to"
|
|
54
|
+
echo " updated ${to} (this release only adds to it)"
|
|
55
|
+
else
|
|
56
|
+
DIFFER_TO+=("$to")
|
|
57
|
+
DIFFER_FROM+=("$from")
|
|
58
|
+
DIFFER_OURS+=("$ours")
|
|
59
|
+
echo " kept ${to} (has lines this release does not - see below)"
|
|
60
|
+
fi
|
|
46
61
|
else
|
|
47
62
|
echo " kept ${to} (already present)"
|
|
48
63
|
fi
|
|
@@ -192,115 +207,177 @@ else
|
|
|
192
207
|
echo " kept ${DB} permissions.project (already present)"
|
|
193
208
|
fi
|
|
194
209
|
|
|
210
|
+
# ---- 3b. Dependency overrides, which npm reads only from THIS directory's package.json ----
|
|
211
|
+
# frugal-iot-server's package.json carries "overrides" that fix vulnerable transitive dependencies
|
|
212
|
+
# (uuid, below firebase-admin in the logger). npm honours overrides only in the top-level project,
|
|
213
|
+
# and here that is this directory's package.json, which just lists frugal-iot-server - so the
|
|
214
|
+
# package's own overrides are ignored, and "npm audit" on an install reports what it reports clean
|
|
215
|
+
# in a checkout. Copied here so that they apply. A dependency needing an override has to have it
|
|
216
|
+
# repeated in the server's package.json, since the logger's own is ignored for the same reason.
|
|
217
|
+
#
|
|
218
|
+
# The release's value replaces a different one here: these exist to raise a floor, and a stale one
|
|
219
|
+
# would bring the vulnerability back. One a release drops is left in place - it is harmless.
|
|
220
|
+
if [[ "$PKG" == */node_modules/frugal-iot-server && -f ./package.json ]]; then
|
|
221
|
+
echo "Dependencies:"
|
|
222
|
+
OVR_SET=$(node -e '
|
|
223
|
+
const fs = require("fs");
|
|
224
|
+
const want = JSON.parse(fs.readFileSync(process.argv[1])).overrides || {};
|
|
225
|
+
const local = JSON.parse(fs.readFileSync("./package.json"));
|
|
226
|
+
const have = local.overrides || {};
|
|
227
|
+
const changed = Object.keys(want).filter(k => JSON.stringify(have[k]) !== JSON.stringify(want[k]));
|
|
228
|
+
if (changed.length) {
|
|
229
|
+
local.overrides = { ...have, ...want };
|
|
230
|
+
fs.writeFileSync("./package.json", JSON.stringify(local, null, 2) + "\n");
|
|
231
|
+
}
|
|
232
|
+
console.log(changed.map(k => k + " " + JSON.stringify(want[k])).join("\n"));
|
|
233
|
+
' "${PKG}/package.json") || OVR_SET="ERROR"
|
|
234
|
+
if [[ "$OVR_SET" == ERROR ]]; then
|
|
235
|
+
echo " ERROR: could not read or update ./package.json - its overrides are unchanged" >&2
|
|
236
|
+
elif [[ -z "$OVR_SET" ]]; then
|
|
237
|
+
echo " kept ./package.json overrides (already present)"
|
|
238
|
+
else
|
|
239
|
+
for l in ${(f)OVR_SET}; do echo " set ./package.json override ${l}"; done
|
|
240
|
+
# The override changes nothing in node_modules until npm installs again
|
|
241
|
+
if npm install --no-fund --no-audit; then
|
|
242
|
+
echo " updated ./node_modules to match"
|
|
243
|
+
else
|
|
244
|
+
echo " ERROR: npm install failed - run it again in $(pwd) before restarting the server" >&2
|
|
245
|
+
fi
|
|
246
|
+
fi
|
|
247
|
+
fi
|
|
248
|
+
|
|
195
249
|
# ---- 4. Is what was installed FROM extras/ still current? ----
|
|
196
250
|
# The files in extras/ are templates for files that live somewhere else - the broker's
|
|
197
|
-
# configuration, its ACL, the systemd
|
|
198
|
-
# nothing about any of them, so the question is asked
|
|
251
|
+
# configuration, its ACL, the systemd units. Whether this directory's copy matches the release says
|
|
252
|
+
# nothing about any of them, so the question is asked of the file actually in use - found by asking
|
|
253
|
+
# the broker and systemd where that is, never by assuming a Pi's layout. Production's broker
|
|
254
|
+
# configuration is several hand-written files in conf.d, its unit is in /usr/lib/systemd/system, and
|
|
255
|
+
# checking "the usual place" there reported everything as absent, which was true and no help.
|
|
199
256
|
#
|
|
200
|
-
# Each
|
|
201
|
-
#
|
|
202
|
-
# -
|
|
203
|
-
#
|
|
204
|
-
# - aclfile
|
|
205
|
-
#
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
257
|
+
# Each is compared in the way it is allowed to differ, and what is reported is the lines that
|
|
258
|
+
# differ, not an instruction to go and diff it:
|
|
259
|
+
# - the broker's configuration, setting by setting across every file it reads
|
|
260
|
+
# (broker_conf_compare in scripts/broker-config.zsh)
|
|
261
|
+
# - aclfile as a subset: addbridge-prod appends a stanza per bridge, so only a line this release
|
|
262
|
+
# ships and the installed file lacks is a finding
|
|
263
|
+
# - frugaliot.service with User, WorkingDirectory and ExecStart normalised, because the installer
|
|
264
|
+
# rewrites those for whoever runs it and wherever the server went
|
|
265
|
+
STALE=() # "installed|packaged" - and STALE_LINES, what differs, in the same order
|
|
266
|
+
STALE_LINES=()
|
|
267
|
+
UNREADABLE=() # there, but not readable even with sudo -n
|
|
268
|
+
|
|
269
|
+
# norm_text <how>: settings only - comments and blank lines gone - with what an installer
|
|
270
|
+
# legitimately rewrites reduced to its name. Reads its standard input.
|
|
271
|
+
norm_text() {
|
|
272
|
+
local expr='s#^##'
|
|
273
|
+
[[ $1 == service ]] && expr='s#^(User|WorkingDirectory|ExecStart)=.*#\1=(set by the installer)#'
|
|
274
|
+
sed -E "$expr" | grep -vE '^[[:space:]]*(#|$)' || true
|
|
275
|
+
}
|
|
209
276
|
|
|
210
|
-
# check_installed <packaged file> <installed path> <exact|
|
|
277
|
+
# check_installed <packaged file> <installed path> <exact|service|subset>
|
|
278
|
+
# The installed file is read with broker_conf_cat, which retries with sudo -n: the aclfile is
|
|
279
|
+
# mosquitto:mosquitto 600, so reading it plainly as the service user always fails.
|
|
211
280
|
check_installed() {
|
|
212
|
-
local from=$1 to=$2 how=$3
|
|
213
|
-
[[ -e "$from" ]] || return 0
|
|
214
|
-
if
|
|
215
|
-
UNINSTALLED+=("${to}|${from}")
|
|
216
|
-
return 0
|
|
217
|
-
fi
|
|
218
|
-
if [[ ! -r "$to" ]]; then
|
|
281
|
+
local from=$1 to=$2 how=$3 here lines
|
|
282
|
+
[[ -e "$from" && -e "$to" ]] || return 0
|
|
283
|
+
if ! here=$(broker_conf_cat "$to"); then
|
|
219
284
|
UNREADABLE+=("$to")
|
|
220
285
|
return 0
|
|
221
286
|
fi
|
|
222
|
-
# Both sides through the same filter, so only a real difference survives. Note the "#"
|
|
223
|
-
# delimiter: an alternation inside an "s|...|" would end the expression at its first branch, and
|
|
224
|
-
# the "|| true" below would then hide the error and report the two files as identical.
|
|
225
|
-
local norm
|
|
226
287
|
case "$how" in
|
|
227
|
-
plugin) norm='s#^plugin .*#plugin -#' ;;
|
|
228
|
-
# The installer rewrites User, WorkingDirectory and ExecStart for whoever runs it and wherever
|
|
229
|
-
# the server went, so those three are normalised - comparing them exactly would report every
|
|
230
|
-
# installation that is not user "pi" in /home/pi/frugal-iot as out of date at every upgrade.
|
|
231
|
-
service) norm='s#^(User|WorkingDirectory|ExecStart)=.*#\1=-#' ;;
|
|
232
|
-
*) norm='' ;;
|
|
233
|
-
esac
|
|
234
|
-
|
|
235
|
-
case "$how" in
|
|
236
|
-
plugin|service)
|
|
237
|
-
a=$(sed -E "$norm" "$from" | grep -vE '^[[:space:]]*(#|$)' | sort || true)
|
|
238
|
-
b=$(sed -E "$norm" "$to" | grep -vE '^[[:space:]]*(#|$)' | sort || true)
|
|
239
|
-
# Neither of these files is ever empty once the comments are gone. Both blank means the
|
|
240
|
-
# filter failed rather than that they agree, and reporting "current" on that would be the
|
|
241
|
-
# worst answer of the three.
|
|
242
|
-
if [[ -z "$a" || -z "$b" ]]; then
|
|
243
|
-
UNREADABLE+=("$to")
|
|
244
|
-
elif [[ "$a" != "$b" ]]; then
|
|
245
|
-
STALE+=("${to}|${from}")
|
|
246
|
-
fi
|
|
247
|
-
;;
|
|
248
288
|
subset)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
[[ -z "$missing" ]] || STALE+=("${to}|${from}")
|
|
289
|
+
lines=$(comm -23 <(norm_text exact < "$from" | sort -u) <(print -r -- "$here" | norm_text exact | sort -u) |
|
|
290
|
+
sed 's/^/release only: /') || true
|
|
252
291
|
;;
|
|
253
292
|
*)
|
|
254
|
-
|
|
293
|
+
# "here" and "release", from diff's < and >; its hunk headers say nothing to a reader
|
|
294
|
+
lines=$(diff <(print -r -- "$here" | norm_text "$how") <(norm_text "$how" < "$from") |
|
|
295
|
+
sed -n -e 's/^< /here only: /p' -e 's/^> /release only: /p') || true
|
|
255
296
|
;;
|
|
256
297
|
esac
|
|
298
|
+
if [[ -n "$lines" ]]; then
|
|
299
|
+
STALE+=("${to}|${from}")
|
|
300
|
+
STALE_LINES+=("$lines")
|
|
301
|
+
fi
|
|
257
302
|
}
|
|
258
303
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
304
|
+
# Where systemd keeps a unit, if it has one - /etc/systemd/system on a Pi, /usr/lib/systemd/system
|
|
305
|
+
# on production
|
|
306
|
+
unit_path() {
|
|
307
|
+
command -v systemctl >/dev/null 2>&1 || return 0
|
|
308
|
+
systemctl show -p FragmentPath --value "$1" 2>/dev/null || true
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# The broker: what it reads, asked of its own configuration
|
|
312
|
+
source "${PKG}/scripts/broker-config.zsh"
|
|
313
|
+
broker_conf_read
|
|
314
|
+
BROKER_FINDINGS=""
|
|
315
|
+
if (( ${#BROKER_CONF_FILES} )); then
|
|
316
|
+
BROKER_FINDINGS=$(broker_conf_compare "${PKG}/extras/mosquitto.conf")
|
|
317
|
+
broker_setting acl_file
|
|
318
|
+
[[ -n "$BROKER_SETTING" ]] && check_installed "${PKG}/extras/aclfile" "$BROKER_SETTING" subset
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
SERVICE_UNIT=$(unit_path frugaliot.service)
|
|
322
|
+
[[ -n "$SERVICE_UNIT" ]] && check_installed "${PKG}/extras/frugaliot.service" "$SERVICE_UNIT" service
|
|
323
|
+
# The setup network's watchdog exists only on a Pi with WiFi that install-pi.sh gave one. Anywhere
|
|
324
|
+
# else it is simply not there, and saying so at every upgrade was noise.
|
|
325
|
+
WATCHDOG_UNIT=$(unit_path frugaliot-ap-watchdog.service)
|
|
326
|
+
if [[ -n "$WATCHDOG_UNIT" ]]; then
|
|
327
|
+
check_installed "${PKG}/extras/frugaliot-ap-watchdog.service" "$WATCHDOG_UNIT" exact
|
|
328
|
+
check_installed "${PKG}/extras/frugaliot-ap-watchdog.timer" "$(unit_path frugaliot-ap-watchdog.timer)" exact
|
|
329
|
+
check_installed "${PKG}/extras/frugaliot-ap-watchdog" /usr/local/sbin/frugaliot-ap-watchdog exact
|
|
330
|
+
fi
|
|
267
331
|
|
|
268
332
|
if (( ${#DIFFER_TO} )); then
|
|
269
333
|
echo
|
|
270
|
-
echo "
|
|
334
|
+
echo "Kept as you have them, because they have lines this release's version does not:"
|
|
271
335
|
for i in {1..${#DIFFER_TO}}; do
|
|
272
336
|
echo " ${DIFFER_TO[$i]}"
|
|
273
|
-
|
|
337
|
+
print -r -- "${DIFFER_OURS[$i]}" | head -10 | sed 's/^/ /'
|
|
338
|
+
(( $(print -r -- "${DIFFER_OURS[$i]}" | wc -l) > 10 )) && echo " ..."
|
|
274
339
|
done
|
|
275
|
-
echo "
|
|
276
|
-
echo "
|
|
277
|
-
echo "
|
|
340
|
+
echo "If you added those, nothing needs doing. If you did not, this release changed or removed"
|
|
341
|
+
echo "them, and taking its version is safe:"
|
|
342
|
+
for i in {1..${#DIFFER_TO}}; do echo " cp ${DIFFER_FROM[$i]} ${DIFFER_TO[$i]}"; done
|
|
278
343
|
fi
|
|
279
344
|
|
|
280
|
-
if
|
|
345
|
+
if [[ -n "$BROKER_FINDINGS" ]]; then
|
|
281
346
|
echo
|
|
282
|
-
echo "
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
347
|
+
echo "The broker's configuration, compared setting by setting with this release's"
|
|
348
|
+
echo "extras/mosquitto.conf, across the ${#BROKER_CONF_FILES} file(s) it reads:"
|
|
349
|
+
BROKER_REAL=0
|
|
350
|
+
for l in ${(f)BROKER_FINDINGS}; do
|
|
351
|
+
case ${l%%$'\t'*} in
|
|
352
|
+
missing) echo " missing: ${l#*$'\t'}"; (( BROKER_REAL++ )) || true ;;
|
|
353
|
+
differs) echo " different: ${l#*$'\t'}"; (( BROKER_REAL++ )) || true ;;
|
|
354
|
+
elsewhere) echo " (fine) ${l#*$'\t'}" ;;
|
|
355
|
+
esac
|
|
286
356
|
done
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
357
|
+
if (( BROKER_REAL )); then
|
|
358
|
+
echo "To bring it in line, put the settings above in any file in /etc/mosquitto/conf.d/ - on a Pi"
|
|
359
|
+
echo "set up by install-pi.sh, re-running it does that - then: sudo systemctl restart mosquitto"
|
|
360
|
+
fi
|
|
290
361
|
fi
|
|
362
|
+
(( ${#BROKER_CONF_UNREADABLE} )) &&
|
|
363
|
+
{ echo; echo "Broker configuration not readable as $(id -un), so the comparison above is incomplete: ${(j:, :)BROKER_CONF_UNREADABLE}"; }
|
|
291
364
|
|
|
292
|
-
if (( ${#
|
|
365
|
+
if (( ${#STALE} )); then
|
|
293
366
|
echo
|
|
294
|
-
echo "
|
|
295
|
-
for
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
367
|
+
echo "Installed elsewhere from extras/, and different from this release:"
|
|
368
|
+
for i in {1..${#STALE}}; do
|
|
369
|
+
echo " ${STALE[$i]%%|*}"
|
|
370
|
+
print -r -- "${STALE_LINES[$i]}" | sed 's/^/ /'
|
|
371
|
+
done
|
|
372
|
+
echo "Bring them in line by hand - change the \"here only\" lines to the \"release only\" ones - or on"
|
|
373
|
+
echo "a Pi set up by it, by re-running install-pi.sh. The whole difference, if you want it:"
|
|
374
|
+
for e in $STALE; do echo " sudo diff ${e%%|*} ${e#*|}"; done
|
|
299
375
|
fi
|
|
300
376
|
|
|
301
377
|
if (( ${#UNREADABLE} )); then
|
|
302
378
|
echo
|
|
303
|
-
echo "There, but not readable as $(id -un), so not compared: ${(j:, :)UNREADABLE}"
|
|
379
|
+
echo "There, but not readable as $(id -un) without a sudo password, so not compared: ${(j:, :)UNREADABLE}"
|
|
380
|
+
echo "To compare them too: sudo -v, then run this again"
|
|
304
381
|
fi
|
|
305
382
|
|
|
306
383
|
echo
|
|
@@ -156,8 +156,20 @@ info "install directory ${DIR} is on ${DMNT} (${DFS}), ${DFREE} MB free"
|
|
|
156
156
|
case "$DFS" in
|
|
157
157
|
vfat|exfat|msdos|ntfs|fuseblk) fail "${DIR} is on ${DFS}, which cannot hold the install. Put it on an ext4 drive: --dir /mnt/<drive>/frugal-iot" ;;
|
|
158
158
|
*) if [ "$DFREE" -lt 400 ]; then
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
# A drive that is already mounted is the usual answer: the --dir may simply name the wrong mount
|
|
160
|
+
# point (/mnt/sda1 when the drive is at /mnt/sda). Suggest any real filesystem with room.
|
|
161
|
+
BIGMNT=$(awk '$3 ~ /^(ext2|ext3|ext4|f2fs|btrfs|xfs)$/ && $2 != "/" && $2 !~ /^\/(overlay|rom)/ {print $2}' /proc/mounts | sort -u | while read -r m; do
|
|
162
|
+
[ "$(free_mb "$m")" -ge 400 ] 2>/dev/null && { echo "$m"; break; }; done)
|
|
163
|
+
if [ -n "$BIGMNT" ]; then
|
|
164
|
+
fail "only ${DFREE} MB free for ${DIR}, which is not on the drive. ${BIGMNT} has room: try --dir ${BIGMNT}/frugal-iot"
|
|
165
|
+
else
|
|
166
|
+
fail "only ${DFREE} MB free for ${DIR}. Attach a USB/SSD drive (extroot, or mount it) and use --dir"
|
|
167
|
+
fi
|
|
168
|
+
# (ls with several patterns fails if ANY has no match, so test each separately)
|
|
169
|
+
SEEN_DRIVE=0
|
|
170
|
+
for d in /dev/sd[a-z]* /dev/mmcblk[1-9]* /dev/nvme*; do [ -e "$d" ] && SEEN_DRIVE=1; done
|
|
171
|
+
if [ -n "$BIGMNT" ]; then :
|
|
172
|
+
elif [ "$SEEN_DRIVE" = 0 ]; then
|
|
161
173
|
# Seen on a real router: the drive was plugged in and formatted ext4, and invisible, because
|
|
162
174
|
# the image carried no USB storage driver.
|
|
163
175
|
info "No USB, SD or NVMe drive is visible at all. If one is plugged in, this OpenWrt image has no"
|