frugal-iot-server 0.3.4 → 0.3.6

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.
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Warn about things in a sensor schema that are easy to get wrong and expensive to get wrong.
4
+ *
5
+ * It only warns - it never changes anything. Each of these needs somebody to decide what the right
6
+ * value is for that particular sensor, and a script guessing would be worse than a script asking.
7
+ *
8
+ * What it looks for:
9
+ *
10
+ * - A topic that does not say whether it is logged. With no "log" setting, any float, int or bool
11
+ * with "rw: r" is recorded and anything else is not, which is easy to be surprised by in either
12
+ * direction - a sensor silently not recorded, or one recorded that nobody wanted.
13
+ *
14
+ * - A logged topic with no "duplicates" rule. Without one, every reading that arrives is written
15
+ * to disk as it arrives. On a server running from an SD card that wears the card out, and it
16
+ * fills the disk with readings that mostly repeat the one before.
17
+ *
18
+ * - A "duplicates" rule that cannot do anything, because it sets neither significantvalue nor
19
+ * significantdate.
20
+ *
21
+ * - A module topic overriding a field that its topic does not define, which is usually a
22
+ * misspelling.
23
+ *
24
+ * Usage:
25
+ * node scripts/check-schema.js # checks ./config.d/schema
26
+ * node scripts/check-schema.js <dir> [<dir> ...] # each dir holding topics.yaml and modules.yaml
27
+ *
28
+ * Exits 0 whatever it finds, so it can run from a prerelease script without blocking a release for
29
+ * something that may well be deliberate. Exits 1 only if it could not read a schema at all.
30
+ */
31
+
32
+ import yaml from 'js-yaml';
33
+ import { readFileSync, existsSync } from 'fs';
34
+ import path from 'path';
35
+
36
+ // The types the logger records when a topic does not say - keep in step with shouldLog() in
37
+ // frugal-iot-logger/index.js
38
+ const TYPES_LOGGED_BY_DEFAULT = ['float', 'int', 'bool'];
39
+
40
+ function loadYaml(file) {
41
+ if (!existsSync(file)) return null;
42
+ return yaml.load(readFileSync(file, 'utf8')) || {};
43
+ }
44
+
45
+ /*
46
+ * Check one directory holding topics.yaml and (optionally) modules.yaml.
47
+ * Returns an array of warning strings.
48
+ */
49
+ function checkSchemaDir(dir) {
50
+ const topics = loadYaml(path.join(dir, 'topics.yaml'));
51
+ if (topics === null) return [`${dir}/topics.yaml does not exist`];
52
+ const modules = loadYaml(path.join(dir, 'modules.yaml')) || {};
53
+ const warnings = [];
54
+
55
+ for (const [name, topic] of Object.entries(topics)) {
56
+ if (!topic || (typeof topic !== 'object')) {
57
+ warnings.push(`topic "${name}" is not a set of settings`);
58
+ continue;
59
+ }
60
+ // Whether it is logged, and whether that was actually stated
61
+ const wouldBeLogged = TYPES_LOGGED_BY_DEFAULT.includes(topic.type) && (topic.rw === 'r');
62
+ if (topic.log === undefined) {
63
+ warnings.push(`topic "${name}" does not say whether it is logged`
64
+ + ` - with type "${topic.type}" and rw "${topic.rw}" it ${wouldBeLogged ? 'IS' : 'is not'} recorded.`
65
+ + ` Add "log: ${wouldBeLogged}" if that is what you want.`);
66
+ }
67
+ const logged = (topic.log === undefined) ? wouldBeLogged : topic.log;
68
+ if (logged && !topic.duplicates) {
69
+ warnings.push(`topic "${name}" is logged but has no "duplicates" rule, so every reading is`
70
+ + ` written to disk as it arrives. Add significantvalue (how far it has to move) and`
71
+ + ` significantdate (how long since the last one, in milliseconds).`);
72
+ }
73
+ if (topic.duplicates
74
+ && (topic.duplicates.significantvalue === undefined)
75
+ && (topic.duplicates.significantdate === undefined)) {
76
+ warnings.push(`topic "${name}" has a "duplicates" rule setting neither significantvalue nor`
77
+ + ` significantdate, so it does nothing`);
78
+ }
79
+ }
80
+
81
+ // A module topic can override any field of the topic it is built from. A field the topic does not
82
+ // have is usually a spelling mistake, and one that quietly does nothing.
83
+ for (const [moduleName, module] of Object.entries(modules)) {
84
+ for (const topic of ((module && module.topics) || [])) {
85
+ const from = topic.leaf_from || topic.leaf;
86
+ const base = topics[from];
87
+ if (!base) {
88
+ warnings.push(`module "${moduleName}" has topic "${topic.leaf}" built from "${from}",`
89
+ + ` which is not in topics.yaml`);
90
+ continue;
91
+ }
92
+ for (const field of Object.keys(topic)) {
93
+ if (['leaf', 'leaf_from', 'name'].includes(field)) continue; // Always local to the module
94
+ if (base[field] === undefined) {
95
+ warnings.push(`module "${moduleName}" topic "${topic.leaf}" sets "${field}", which`
96
+ + ` "${from}" in topics.yaml does not have - a misspelling?`);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ return warnings;
102
+ }
103
+
104
+ const dirs = (process.argv.length > 2) ? process.argv.slice(2) : ['config.d/schema'];
105
+ let unreadable = 0;
106
+ let total = 0;
107
+
108
+ for (const dir of dirs) {
109
+ const warnings = checkSchemaDir(dir);
110
+ if ((warnings.length === 1) && warnings[0].endsWith('does not exist')) {
111
+ console.error(`${dir}: ${warnings[0]}`);
112
+ unreadable++;
113
+ continue;
114
+ }
115
+ if (!warnings.length) {
116
+ console.log(`${dir}: nothing to report`);
117
+ continue;
118
+ }
119
+ console.log(`${dir}: ${warnings.length} thing(s) worth a look`);
120
+ for (const w of warnings) console.log(` ${w}`);
121
+ total += warnings.length;
122
+ }
123
+
124
+ if (total) {
125
+ console.log(`\n${total} warning(s) - these are for you to decide about, nothing has been changed.`);
126
+ }
127
+ process.exit(unreadable ? 1 : 0);
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env zsh
2
+ #
3
+ # Copy this server's sensor schema into the examples that ship with frugal-iot-logger, so that they
4
+ # do not drift away from the schema actually in use.
5
+ #
6
+ # Called by the prerelease script of both packages - whichever one you are about to publish, the
7
+ # examples end up current. It is here rather than in frugal-iot-logger because this is where the
8
+ # schema is maintained.
9
+ #
10
+ # Usage:
11
+ # scripts/copy-schema-to-examples.zsh [<frugal-iot-logger directory>]
12
+ #
13
+ # With no argument it looks for frugal-iot-logger beside this checkout, which is how these are
14
+ # developed. Says what it did and changes nothing else; committing is up to you.
15
+
16
+ set -euo pipefail
17
+
18
+ HERE="${0:A:h:h}" # The frugal-iot-server checkout this script is in
19
+ SCHEMA="${HERE}/config.d/schema"
20
+ LOGGER="${1:-${HERE:h}/frugal-iot-logger}"
21
+
22
+ if [[ ! -d "$LOGGER" ]]; then
23
+ echo " No frugal-iot-logger checkout at ${LOGGER}, so nothing copied."
24
+ echo " The examples in that package keep their own copy of the schema and will drift."
25
+ echo " Clone it beside this one and run this again if you want them updated."
26
+ exit 0 # Not an error - you may simply not have it checked out
27
+ fi
28
+
29
+ DIRS=("${LOGGER}"/examples/*/config.d/schema(N/))
30
+ if (( ! ${#DIRS} )); then
31
+ echo " No examples/*/config.d/schema directories under ${LOGGER}"
32
+ exit 0
33
+ fi
34
+
35
+ COPIED=0
36
+ for d in $DIRS; do
37
+ for f in topics.yaml modules.yaml; do
38
+ if [[ ! -f "${SCHEMA}/${f}" ]]; then
39
+ echo " ${SCHEMA}/${f} does not exist - skipped"
40
+ continue
41
+ fi
42
+ if cmp -s "${SCHEMA}/${f}" "${d}/${f}"; then
43
+ echo " same ${d#${LOGGER}/}/${f}"
44
+ else
45
+ cp "${SCHEMA}/${f}" "${d}/${f}"
46
+ echo " updated ${d#${LOGGER}/}/${f}"
47
+ COPIED=$((COPIED+1))
48
+ fi
49
+ done
50
+ done
51
+
52
+ if (( COPIED )); then
53
+ echo
54
+ echo " ${COPIED} file(s) changed in ${LOGGER} - commit them there before publishing the logger."
55
+ fi
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env zsh
2
+ #
3
+ # Report the state of a Frugal IoT installation, for working out why something in INSTALLATION.md
4
+ # did not do what it said it would. It only looks - it changes nothing.
5
+ #
6
+ # Run it from the directory the server is installed in (the one holding frugal-iot.db):
7
+ # zsh diagnostic.zsh
8
+ # or, once installed from npm:
9
+ # npx --no frugal-iot-diagnostic
10
+ #
11
+ # Paste the whole output into a bug report. Passwords are deliberately not printed: the password
12
+ # file is reported by account name only, and organization configs by name only.
13
+
14
+ # No "set -e": a failing check should report and carry on, not stop the script.
15
+ setopt no_unset 2>/dev/null
16
+
17
+ section() { print -r -- ""; print -r -- "===== $* ====="; }
18
+ item() { print -r -- " $*"; }
19
+ have() { command -v "$1" >/dev/null 2>&1; }
20
+ # Ownership and mode of a file, spelled for either Linux or BSD/macOS stat
21
+ fileinfo() {
22
+ local f=$1
23
+ if [[ ! -e "$f" ]]; then print -r -- "missing"; return; fi
24
+ stat -c '%U:%G %a %s bytes' "$f" 2>/dev/null || stat -f '%Su:%Sg %Lp %z bytes' "$f" 2>/dev/null || print -r -- "(cannot stat)"
25
+ }
26
+
27
+ PROBLEMS=()
28
+ problem() { PROBLEMS+=("$1"); }
29
+
30
+ section "When and where"
31
+ item "date: $(date 2>/dev/null)"
32
+ item "host: $(hostname 2>/dev/null)"
33
+ item "user: $(id -un 2>/dev/null) (groups: $(id -Gn 2>/dev/null))"
34
+ item "directory: $PWD"
35
+
36
+ section "The machine"
37
+ have uname && item "kernel: $(uname -srm)"
38
+ [[ -r /etc/os-release ]] && item "os: $(grep PRETTY_NAME /etc/os-release | cut -d'"' -f2)"
39
+ [[ -r /proc/device-tree/model ]] && item "board: $(tr -d '\0' < /proc/device-tree/model)"
40
+ have free && item "memory: $(free -h | awk '/^Mem:/ {print $3 " used of " $2 ", " $7 " available"}')"
41
+ have df && item "disk (/): $(df -h / | awk 'NR==2 {print $3 " used of " $2 ", " $4 " free"}')"
42
+ if have timedatectl; then
43
+ item "clock: $(timedatectl show -p NTPSynchronized --value 2>/dev/null | sed 's/^yes$/synchronized with a time server/; s/^no$/NOT synchronized - timestamps may be wrong/')"
44
+ fi
45
+
46
+ section "Software versions"
47
+ for c in node npm sqlite3 zsh mosquitto mosquitto_passwd mosquitto_sub; do
48
+ if have $c; then
49
+ case $c in
50
+ node|npm) item "$c: $($c -v 2>&1 | head -1)" ;;
51
+ sqlite3) item "$c: $(sqlite3 --version 2>&1 | awk '{print $1}')" ;;
52
+ zsh) item "$c: $ZSH_VERSION" ;;
53
+ mosquitto) item "$c: $(mosquitto -h 2>&1 | head -1)" ;;
54
+ *) item "$c: present" ;;
55
+ esac
56
+ else
57
+ item "$c: NOT INSTALLED"
58
+ [[ $c == (node|npm|sqlite3) ]] && problem "$c is not installed - see INSTALLATION.md step 3"
59
+ fi
60
+ done
61
+
62
+ section "This installation"
63
+ for f in config.yaml config.d frugal-iot.db data ota node_modules; do
64
+ if [[ -e $f ]]; then item "$f: present"; else item "$f: MISSING"; fi
65
+ done
66
+ [[ ! -e frugal-iot.db ]] && problem "No frugal-iot.db here - either the wrong directory, or 'npx --no frugal-iot-init' has not been run"
67
+ if [[ -d node_modules/frugal-iot-server ]]; then
68
+ item "frugal-iot-server: $(node -e 'console.log(require("./node_modules/frugal-iot-server/package.json").version)' 2>/dev/null)"
69
+ item "frugal-iot-client: $(node -e 'console.log(require("./node_modules/frugal-iot-client/package.json").version)' 2>/dev/null)"
70
+ item "frugal-iot-logger: $(node -e 'console.log(require("./node_modules/frugal-iot-logger/package.json").version)' 2>/dev/null)"
71
+ item "commands: $(ls node_modules/.bin 2>/dev/null | grep frugal | tr '\n' ' ')"
72
+ else
73
+ item "frugal-iot-server is not installed in this directory"
74
+ fi
75
+ # sqlite3 is the one dependency with compiled code in it. Where no ready-made binary exists for the
76
+ # platform (32-bit ARM, so any Pi Zero W or Pi 1) npm compiles it during install - and if that fails,
77
+ # npm can still report success, leaving a package directory with nothing usable inside it. Nothing
78
+ # else here looks wrong when that happens, and the server simply refuses to start.
79
+ if [[ -d node_modules/sqlite3 ]]; then
80
+ if node -e "require('sqlite3')" >/dev/null 2>&1; then
81
+ item "sqlite3 native module: builds and loads OK"
82
+ else
83
+ item "sqlite3 native module: PRESENT BUT NOT BUILT"
84
+ problem "node_modules/sqlite3 has no compiled binding, so the server cannot start. Build it with:
85
+ npm rebuild sqlite3 --foreground-scripts
86
+ (about 40 minutes on a Pi Zero W; needs build-essential python3-dev python3-setuptools)"
87
+ fi
88
+ fi
89
+ if [[ -f config.d/mqtt.yaml ]]; then item "broker configured as: $(grep -h '^broker:' config.d/mqtt.yaml 2>/dev/null)"; fi
90
+ if [[ -d config.d/organizations ]]; then
91
+ # Names only - these files contain the broker password.
92
+ # (N) makes a non-matching glob expand to nothing rather than erroring - and the list has to be
93
+ # tested before use, because "ls" with no arguments would list the current directory instead.
94
+ ORGS=(config.d/organizations/*.yaml(N:t:r))
95
+ if (( ${#ORGS} )); then
96
+ item "organizations: ${ORGS}"
97
+ else
98
+ item "organizations: none defined yet"
99
+ fi
100
+ fi
101
+ if [[ -f frugal-iot.db ]] && have sqlite3; then
102
+ item "accounts: $(sqlite3 frugal-iot.db 'SELECT group_concat(username, ", ") FROM users;' 2>/dev/null)"
103
+ fi
104
+
105
+ section "Mosquitto configuration"
106
+ if [[ -f /etc/mosquitto/mosquitto.conf ]]; then
107
+ item "/etc/mosquitto/mosquitto.conf: $(fileinfo /etc/mosquitto/mosquitto.conf)"
108
+ else
109
+ item "/etc/mosquitto/mosquitto.conf: missing - is mosquitto installed?"
110
+ fi
111
+ item "files in /etc/mosquitto/conf.d:"
112
+ for f in /etc/mosquitto/conf.d/*(N); do
113
+ item " ${f}: $(fileinfo $f)"
114
+ grep -nE '^[[:space:]]*(listener|protocol|password_file|allow_anonymous)' $f 2>/dev/null | sed 's/^/ /'
115
+ done
116
+ [[ -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)"
117
+
118
+ # Which password file does the running configuration actually name?
119
+ # mosquitto's include_dir only loads files ending in .conf, so look at exactly those, last one wins
120
+ CONFIGURED_PWFILE=$(grep -hE '^[[:space:]]*password_file[[:space:]]' /etc/mosquitto/mosquitto.conf /etc/mosquitto/conf.d/*.conf(N) 2>/dev/null | tail -1 | awk '{print $2}')
121
+ section "Mosquitto password file"
122
+ if [[ -n "$CONFIGURED_PWFILE" ]]; then
123
+ item "configuration names: $CONFIGURED_PWFILE"
124
+ item " that file: $(fileinfo $CONFIGURED_PWFILE)"
125
+ if [[ ! -e "$CONFIGURED_PWFILE" ]]; then
126
+ problem "Mosquitto is configured to use $CONFIGURED_PWFILE but that file does not exist - it will refuse to start"
127
+ else
128
+ # Account names only, never the hashes
129
+ item " accounts in it: $(cut -d: -f1 "$CONFIGURED_PWFILE" 2>/dev/null | tr '\n' ' ')"
130
+ [[ ! -s "$CONFIGURED_PWFILE" ]] && item " (the file is empty - no accounts yet)"
131
+ fi
132
+ else
133
+ item "no password_file line found in any mosquitto configuration"
134
+ problem "No password_file configured - the broker would allow anonymous access or reject everything"
135
+ fi
136
+ # The copy in this installation is what step 5 tells you to put into /etc - if the two disagree,
137
+ # either the copy never happened or the installed package is older than the instructions.
138
+ if [[ -f extras/mosquitto.conf ]]; then
139
+ item "this installation's extras/mosquitto.conf names: $(grep -hE '^[[:space:]]*password_file' extras/mosquitto.conf 2>/dev/null | awk '{print $2}')"
140
+ if [[ -n "$CONFIGURED_PWFILE" ]] && ! diff -q extras/mosquitto.conf /etc/mosquitto/conf.d/frugal-iot.conf >/dev/null 2>&1; then
141
+ problem "extras/mosquitto.conf here differs from /etc/mosquitto/conf.d/frugal-iot.conf - if you followed step 5 with an older installed package, copy it again after upgrading"
142
+ fi
143
+ fi
144
+ item "candidate locations, whether or not configured:"
145
+ for f in /var/lib/mosquitto/passwords /etc/mosquitto/mosquitto_passwords; do
146
+ item " ${f}: $(fileinfo $f)"
147
+ done
148
+ item "directories they live in:"
149
+ for d in /var/lib/mosquitto /etc/mosquitto; do
150
+ item " ${d}: $(fileinfo $d)"
151
+ done
152
+
153
+ section "Mosquitto service"
154
+ if have systemctl; then
155
+ item "enabled: $(systemctl is-enabled mosquitto 2>&1)"
156
+ item "active: $(systemctl is-active mosquitto 2>&1)"
157
+ [[ "$(systemctl is-active mosquitto 2>/dev/null)" != "active" ]] && problem "Mosquitto is not running - see its own log, quoted above under 'Mosquitto service'"
158
+ print -r -- " --- last 12 journal lines (systemd's view: usually only an exit code) ---"
159
+ journalctl -u mosquitto -n 12 --no-pager 2>&1 | sed 's/^/ /'
160
+ else
161
+ item "systemctl not available on this machine"
162
+ fi
163
+ # Debian's mosquitto logs to a file rather than the journal, so this is where the real reason is.
164
+ MOSQUITTO_LOG=$(grep -hE '^[[:space:]]*log_dest[[:space:]]+file' /etc/mosquitto/mosquitto.conf /etc/mosquitto/conf.d/*(N) 2>/dev/null | tail -1 | awk '{print $3}')
165
+ [[ -z "$MOSQUITTO_LOG" ]] && MOSQUITTO_LOG=/var/log/mosquitto/mosquitto.log
166
+ item "its own log file: ${MOSQUITTO_LOG} ($(fileinfo $MOSQUITTO_LOG))"
167
+ if [[ -r "$MOSQUITTO_LOG" ]]; then
168
+ print -r -- " --- last 12 lines of that log (this is where startup errors appear) ---"
169
+ tail -12 "$MOSQUITTO_LOG" 2>&1 | sed 's/^/ /'
170
+ elif [[ -e "$MOSQUITTO_LOG" ]]; then
171
+ # The log belongs to the mosquitto user, so reading it needs privilege. Try without prompting -
172
+ # on Raspberry Pi OS the first user has passwordless sudo, so this usually just works.
173
+ if have sudo && MOSQUITTO_LOG_TAIL=$(sudo -n tail -12 "$MOSQUITTO_LOG" 2>/dev/null); then
174
+ print -r -- " --- last 12 lines of that log, read with sudo (startup errors appear here) ---"
175
+ print -r -- "$MOSQUITTO_LOG_TAIL" | sed 's/^/ /'
176
+ else
177
+ item " it belongs to $(fileinfo $MOSQUITTO_LOG | awk '{print $1}') so it cannot be read as $(id -un)."
178
+ item " Re-run this whole script with sudo to include it: sudo zsh $0"
179
+ problem "Could not read ${MOSQUITTO_LOG}, which is where Mosquitto explains itself - re-run with sudo"
180
+ fi
181
+ fi
182
+
183
+ section "Listening ports"
184
+ PORTS=""
185
+ if have ss; then
186
+ PORTS=$(ss -tln 2>/dev/null | grep -E ':(1883|9012|8080)\b')
187
+ elif have netstat; then
188
+ PORTS=$(netstat -an 2>/dev/null | grep -E '[.:](1883|9012|8080) ')
189
+ else
190
+ item "neither ss nor netstat available"
191
+ fi
192
+ if [[ -n "$PORTS" ]]; then
193
+ print -r -- "$PORTS" | sed 's/^/ /'
194
+ else
195
+ item "none of 1883 (mqtt), 9012 (websockets), 8080 (web) are listening"
196
+ fi
197
+
198
+ section "Frugal IoT server service"
199
+ if have systemctl; then
200
+ if systemctl list-unit-files 2>/dev/null | grep -q '^frugaliot.service'; then
201
+ item "enabled: $(systemctl is-enabled frugaliot 2>&1)"
202
+ item "active: $(systemctl is-active frugaliot 2>&1)"
203
+ print -r -- " --- last 20 journal lines ---"
204
+ journalctl -u frugaliot -n 20 --no-pager 2>&1 | sed 's/^/ /'
205
+ else
206
+ item "no frugaliot service installed yet (step 8 not reached)"
207
+ fi
208
+ else
209
+ item "systemctl not available on this machine"
210
+ fi
211
+
212
+ section "Name resolution (steps 3, 4)"
213
+ # The broker URL in config.d/mqtt.yaml has to resolve from every machine that uses it - this one,
214
+ # and whatever browser or node talks to it.
215
+ BROKER_URL=$(grep -h '^broker:' config.d/mqtt.yaml 2>/dev/null | awk '{print $2}')
216
+ # Strip scheme, then any path (wss://host/wss), then any port - leaving just the hostname
217
+ BROKER_HOST=${${${BROKER_URL#*://}%%/*}%%:*}
218
+ if [[ -n "$BROKER_HOST" ]]; then
219
+ item "broker host: $BROKER_HOST"
220
+ if have getent && BROKER_IP=$(getent hosts "$BROKER_HOST" 2>/dev/null | head -1 | awk '{print $1}') && [[ -n "$BROKER_IP" ]]; then
221
+ item " resolves to $BROKER_IP from this machine"
222
+ elif have ping && ping -c1 -W2 "$BROKER_HOST" >/dev/null 2>&1; then
223
+ item " answers ping from this machine"
224
+ else
225
+ item " DOES NOT RESOLVE from this machine"
226
+ problem "The broker host '$BROKER_HOST' does not resolve here - the server's own logger cannot connect either"
227
+ fi
228
+ [[ "$BROKER_HOST" == *.local ]] && item " note: .local names do not resolve on most Android phones - use an IP address there"
229
+ fi
230
+ MYADDRS=$( (have ip && ip -4 -o addr show scope global | awk '{print $2"="$4}') 2>/dev/null | tr '\n' ' ')
231
+ item "this machine's addresses: ${MYADDRS:-(could not determine)}"
232
+
233
+ section "Broker authentication (steps 5, 6)"
234
+ if have mosquitto_sub; then
235
+ # A wrong password must be refused - that is the check in step 5
236
+ WRONGOUT=$(mosquitto_sub -h localhost -u nobody -P wrong -t '#' -W 2 2>&1)
237
+ if [[ "$WRONGOUT" == *"not authorised"* ]]; then
238
+ item "wrong password: correctly refused"
239
+ elif [[ "$WRONGOUT" == *"Connection refused"* || "$WRONGOUT" == *"Error"* ]]; then
240
+ item "wrong password: broker did not answer - $WRONGOUT"
241
+ problem "The broker is not answering on localhost:1883"
242
+ else
243
+ item "wrong password: ACCEPTED - the broker is not requiring credentials"
244
+ problem "A wrong password was not refused - check password_file is set in the mosquitto config"
245
+ fi
246
+ # Then each configured organization should be able to connect with its own credentials.
247
+ # The password is read from the config and used, never printed.
248
+ for f in config.d/organizations/*.yaml(N); do
249
+ ORG=${f:t:r}
250
+ ORG_PW=$(sed -n 's/^mqtt_password:[[:space:]]*//p' "$f" 2>/dev/null | head -1 | tr -d '"'"'"'')
251
+ if [[ -z "$ORG_PW" ]]; then
252
+ item "organization $ORG: no mqtt_password in $f"
253
+ continue
254
+ fi
255
+ ORGOUT=$(mosquitto_sub -h localhost -u "$ORG" -P "$ORG_PW" -t '#' -W 2 2>&1)
256
+ if [[ "$ORGOUT" == *"not authorised"* ]]; then
257
+ item "organization $ORG: REFUSED by the broker"
258
+ problem "Organization '$ORG' cannot log in to the broker - its password in $f does not match the broker's password file (step 6)"
259
+ elif [[ "$ORGOUT" == *"Connection refused"* ]]; then
260
+ item "organization $ORG: broker not answering"
261
+ else
262
+ item "organization $ORG: authenticates, and saw $(print -r -- "$ORGOUT" | grep -c . ) message(s) in a 2 second sample"
263
+ fi
264
+ done
265
+ else
266
+ item "mosquitto_sub not installed - cannot test broker logins (sudo apt install mosquitto-clients)"
267
+ fi
268
+
269
+ section "Web server (step 7)"
270
+ WEBPORT=$(grep -h '^port:' config.d/server.yaml 2>/dev/null | awk '{print $2}')
271
+ [[ -z "$WEBPORT" ]] && WEBPORT=8080
272
+ if have curl; then
273
+ HOMECODE=$(curl -s -o /dev/null -w '%{http_code}' -m 5 http://localhost:${WEBPORT}/ 2>/dev/null)
274
+ if [[ "$HOMECODE" == "000" ]]; then
275
+ item "nothing is answering on port ${WEBPORT} - the server is not running"
276
+ item " (expected if you have not reached step 7 yet; otherwise start it, or see the frugaliot service below)"
277
+ else
278
+ item "GET / -> ${HOMECODE} (expect 200)"
279
+ item "GET /config.json -> $(curl -s -o /dev/null -w '%{http_code}' -m 5 http://localhost:${WEBPORT}/config.json 2>/dev/null) (expect 401 when not logged in)"
280
+ [[ "$HOMECODE" != "200" ]] && problem "The web server answered ${HOMECODE} rather than 200 on port ${WEBPORT}"
281
+ fi
282
+ else
283
+ item "curl not installed - cannot test the web server"
284
+ fi
285
+
286
+ section "Logged data (step 9)"
287
+ if [[ -d data ]]; then
288
+ DATADIRS=(data/*(N/))
289
+ if (( ${#DATADIRS} )); then
290
+ for d in $DATADIRS; do
291
+ item "${d}: $(find $d -type f 2>/dev/null | wc -l | tr -d ' ') files, newest $(ls -t $d/**/*(N.om[1]) 2>/dev/null | head -1)"
292
+ done
293
+ else
294
+ item "data/ is empty - no readings logged yet, which is expected until a node reports (step 9)"
295
+ fi
296
+ fi
297
+
298
+ section "Wear on the SD card"
299
+ # An SD card wears out from being written to, and a server logging sensor readings writes constantly
300
+ # unless told not to. This reports how much is actually being written and whether the settings that
301
+ # reduce it are in force. There is no threshold to compare against - run it twice a few days apart
302
+ # and look at the rate.
303
+ if [[ -r /proc/diskstats && -r /proc/uptime ]]; then
304
+ # Field 10 of each line is sectors written, and a sector is 512 bytes. Take the whole card rather
305
+ # than a partition (mmcblk0, not mmcblk0p2) so the boot partition and swap are included too.
306
+ ROOTDEV=$( (have findmnt && findmnt -no SOURCE / 2>/dev/null) || print -r -- "" )
307
+ ROOTDISK=${${ROOTDEV:t}%p[0-9]*} # /dev/mmcblk0p2 -> mmcblk0
308
+ [[ -z "$ROOTDISK" ]] && ROOTDISK=mmcblk0
309
+ WRITTEN=$(awk -v d="$ROOTDISK" '$3 == d {print $10}' /proc/diskstats 2>/dev/null)
310
+ UPSECS=$(awk '{print int($1)}' /proc/uptime 2>/dev/null)
311
+ if [[ -n "$WRITTEN" && -n "$UPSECS" && "$UPSECS" -gt 0 ]]; then
312
+ item "device: /dev/${ROOTDISK}"
313
+ item "written: $(awk -v s="$WRITTEN" 'BEGIN {printf "%.1f MB", s*512/1048576}') since boot, over $(awk -v u="$UPSECS" 'BEGIN {printf "%.1f", u/3600}') hours"
314
+ item " that is: $(awk -v s="$WRITTEN" -v u="$UPSECS" 'BEGIN {printf "%.1f MB/hour", s*512/1048576/(u/3600)}')"
315
+ else
316
+ item "could not read write counters for /dev/${ROOTDISK} from /proc/diskstats"
317
+ fi
318
+ else
319
+ item "no /proc/diskstats on this machine - write counters are Linux only"
320
+ fi
321
+
322
+ # Swapping to an SD card wears it fast. On a Pi 4 there should be little or none; a Pi Zero W may
323
+ # need some. See INSTALLATION.md, "Wear and tear on the SD card".
324
+ if have free; then
325
+ item "swap: $(free -h | awk '/^Swap:/ {print $3 " used of " $2}')"
326
+ fi
327
+ if [[ -r /proc/sys/vm/swappiness ]]; then
328
+ SWAPPINESS=$(< /proc/sys/vm/swappiness)
329
+ item "swappiness: ${SWAPPINESS} (1 means swap only when there is no alternative)"
330
+ fi
331
+ have swapon && item "swap areas: $(swapon --show=NAME,TYPE,SIZE,USED --noheadings 2>/dev/null | tr '\n' ';' || print -r -- '(none)')"
332
+
333
+ # The journal is the other thing that writes on every event, if it is kept on disk at all
334
+ if [[ -d /var/log/journal ]]; then
335
+ item "journal: stored on disk in /var/log/journal - every logged line is a write"
336
+ have journalctl && item " using: $(journalctl --disk-usage 2>/dev/null | sed 's/^Archived and active journals take up //')"
337
+ else
338
+ item "journal: kept in RAM only (no /var/log/journal), so it costs no writes"
339
+ fi
340
+
341
+ # The settings that decide how much gets logged in the first place
342
+ if [[ -f config.d/logger.yaml ]]; then
343
+ VERBOSE=$(sed -n 's/^verbose:[[:space:]]*//p' config.d/logger.yaml 2>/dev/null | head -1)
344
+ FLUSHSECS=$(sed -n 's/^flushseconds:[[:space:]]*//p' config.d/logger.yaml 2>/dev/null | head -1)
345
+ item "logger verbose: ${VERBOSE:-not set, so on - a line logged per message received}"
346
+ item "logger flushseconds: ${FLUSHSECS:-not set, so readings are written as they arrive}"
347
+ fi
348
+ if [[ -f config.d/server.yaml ]]; then
349
+ MORGANSET=$(sed -n 's/^morgan:[[:space:]]*//p' config.d/server.yaml 2>/dev/null | head -1)
350
+ item "server morgan: ${MORGANSET:-not set, so on - a line logged per HTTP request}"
351
+ fi
352
+ if [[ -f /etc/mosquitto/conf.d/frugal-iot.conf ]]; then
353
+ if grep -qE '^[[:space:]]*connection_messages[[:space:]]+false' /etc/mosquitto/conf.d/frugal-iot.conf 2>/dev/null; then
354
+ item "mosquitto connections: not logged"
355
+ else
356
+ item "mosquitto connections: logged - every connect and disconnect is a write"
357
+ fi
358
+ fi
359
+ if [[ -e "$MOSQUITTO_LOG" ]]; then
360
+ item "mosquitto log size: $(fileinfo $MOSQUITTO_LOG | awk '{print $3, $4}')"
361
+ else
362
+ item "mosquitto log size: no log file at ${MOSQUITTO_LOG}"
363
+ fi
364
+
365
+ # How much the readings themselves are taking up, and whether the old ones have been compressed
366
+ if [[ -d data ]]; then
367
+ CSVCOUNT=$(find data -name '*.csv' 2>/dev/null | wc -l | tr -d ' ')
368
+ GZCOUNT=$(find data -name '*.csv.gz' 2>/dev/null | wc -l | tr -d ' ')
369
+ item "readings: ${CSVCOUNT} csv files and ${GZCOUNT} compressed, $(du -sh data 2>/dev/null | awk '{print $1}') in total"
370
+ # Filenames are the date, so the earliest name is the oldest day held. Both suffixes stripped
371
+ # separately, because BSD sed (macOS) has no "\?" for an optional group.
372
+ OLDEST=$(find data \( -name '*.csv' -o -name '*.csv.gz' \) 2>/dev/null | sed 's|.*/||; s|\.gz$||; s|\.csv$||' | sort | head -1)
373
+ [[ -n "$OLDEST" ]] && item " oldest day: ${OLDEST}"
374
+ fi
375
+
376
+ section "Summary"
377
+ if (( ${#PROBLEMS} == 0 )); then
378
+ item "No problems detected by these checks."
379
+ else
380
+ for p in $PROBLEMS; do item "PROBLEM: $p"; done
381
+ fi
382
+ print -r -- ""
package/scripts/init.zsh CHANGED
@@ -5,7 +5,7 @@
5
5
  # in node_modules (or which sits in a git clone).
6
6
  #
7
7
  # Usage:
8
- # npx frugal-iot-init # after: npm install frugal-iot-server
8
+ # npx --no frugal-iot-init # after: npm install frugal-iot-server
9
9
  # scripts/init.zsh # in a git clone, where the config files are already present
10
10
  #
11
11
  # Safe to run again: existing files are left exactly as they are, only missing ones are added,
@@ -28,11 +28,24 @@ if [[ ! -f "${PKG}/frugal-iot-createdb.sql" ]]; then
28
28
  fi
29
29
 
30
30
  # ---- 1. Configuration files, copied only if not already here ----
31
- COPIED=() # which files this run actually created, so existing ones are never rewritten below
31
+ COPIED=() # which files this run actually created, so existing ones are never rewritten below
32
+ DIFFER_TO=() # files kept that no longer match what this release ships, and where to compare them
33
+ DIFFER_FROM=()
34
+ # copy_if_missing <packaged file> <local file> [compare]
35
+ # "compare" asks for a warning when the file exists but differs from the one this release ships.
36
+ # Only pass it for files nobody is expected to edit - config.yaml, mqtt.yaml and server.yaml are
37
+ # this installation's own settings, so of course they differ, and warning about them every time
38
+ # would train you to ignore the warning that matters.
32
39
  copy_if_missing() {
33
- local from=$1 to=$2
40
+ local from=$1 to=$2 compare=${3:-}
34
41
  if [[ -e "$to" ]]; then
35
- echo " kept ${to} (already present)"
42
+ if [[ "$compare" == compare && -e "$from" ]] && ! cmp -s "$from" "$to"; then
43
+ DIFFER_TO+=("$to")
44
+ DIFFER_FROM+=("$from")
45
+ echo " kept ${to} (DIFFERS from this release - see below)"
46
+ else
47
+ echo " kept ${to} (already present)"
48
+ fi
36
49
  elif [[ -e "$from" ]]; then
37
50
  mkdir -p "${to:h}"
38
51
  cp "$from" "$to"
@@ -46,12 +59,14 @@ copy_if_missing "${PKG}/config.yaml" "./config.yaml"
46
59
  for f in logger.yaml mqtt.yaml server.yaml; do
47
60
  copy_if_missing "${PKG}/config.d/${f}" "./config.d/${f}"
48
61
  done
62
+ # The schema describes the sensor types the software understands, so a release changing it matters
49
63
  for f in "${PKG}"/config.d/schema/*.yaml(N); do
50
- copy_if_missing "$f" "./config.d/schema/${f:t}"
64
+ copy_if_missing "$f" "./config.d/schema/${f:t}" compare
51
65
  done
52
- # Copied so that mosquitto.conf and frugaliot.service can be edited and installed from here
66
+ # Copied so that mosquitto.conf and frugaliot.service can be edited and installed from here. Worth
67
+ # comparing: these get copied on somewhere else (/etc/...), where an old version lingers unnoticed.
53
68
  for f in "${PKG}"/extras/*(N); do
54
- copy_if_missing "$f" "./extras/${f:t}"
69
+ copy_if_missing "$f" "./extras/${f:t}" compare
55
70
  done
56
71
 
57
72
  # ---- 1a. Point server.yaml at the web client this instance actually has ----
@@ -104,8 +119,20 @@ else
104
119
  echo " created ${DB}"
105
120
  fi
106
121
 
122
+ if (( ${#DIFFER_TO} )); then
123
+ echo
124
+ echo "These files were left as you have them, but this release ships a different version:"
125
+ for i in {1..${#DIFFER_TO}}; do
126
+ echo " ${DIFFER_TO[$i]}"
127
+ echo " compare with: diff ${DIFFER_TO[$i]} ${DIFFER_FROM[$i]}"
128
+ done
129
+ echo "Usually that just means you edited it, and there is nothing to do. But a release can also"
130
+ echo "change one of these files - and anything installed elsewhere from it, such as"
131
+ echo "/etc/mosquitto/conf.d/frugal-iot.conf, keeps the old content until you copy it again."
132
+ fi
133
+
107
134
  echo
108
135
  echo "Ready. Next:"
109
- echo " npx frugal-iot-setpassword superuser <password> # so you can log in as the administrator"
110
- echo " npx frugal-iot-addorganization <org-id> <name> <email> <phone> <broker-password>"
111
- echo " npx frugal-iot-server # start it"
136
+ echo " npx --no frugal-iot-setpassword superuser <password> # so you can log in as the administrator"
137
+ echo " npx --no frugal-iot-addorganization <org-id> <name> <email> <phone> <broker-password>"
138
+ echo " npx --no frugal-iot-server # start it"