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.
@@ -1164,8 +1164,8 @@ but close enough for sensor data.
1164
1164
  ### Wear and tear on the SD card
1165
1165
 
1166
1166
  SD cards wear out from being written to, and a server recording sensor readings writes all the time.
1167
- We have not yet seen a card fail, but plan for one eventually: keep a copy of `config.d/` and
1168
- `frugal-iot.db` somewhere else, and treat the readings in `data/` as valuable but not irreplaceable.
1167
+ We have not yet seen a card fail, but plan for one eventually: see [Backups](#backups), and keep
1168
+ the snapshots somewhere other than the card.
1169
1169
 
1170
1170
  A new installation is set up to write as little as it reasonably can, and the settings are worth
1171
1171
  knowing about, because turning one of them back on for debugging and forgetting is easy to do:
@@ -1199,6 +1199,90 @@ and read the **Wear on the SD card** section, which reports how much has been wr
1199
1199
  whether the machine is swapping, and whether each of the settings above is in force. Run it twice a
1200
1200
  few days apart — the interesting number is the rate, not the total.
1201
1201
 
1202
+ ## Backups
1203
+
1204
+ `frugal-iot-backup` takes a snapshot of everything on this machine that cannot be rebuilt: the
1205
+ database, `config.d/` (including `secrets.yaml`), the broker's configuration, password file, ACL
1206
+ file and TLS certificate, and the server's systemd unit. It finds the broker's files by asking the
1207
+ broker's own configuration, so it works however this machine is laid out.
1208
+
1209
+ Not in it: `data/` (large, and copied separately - see below), `ota/` (rebuilt at each release),
1210
+ and anything the broker rebuilds or is running on.
1211
+
1212
+ First see what it would take, and from where - this writes nothing:
1213
+
1214
+ ```
1215
+ cd ~/frugal-iot
1216
+ npx --no frugal-iot-backup check
1217
+ ```
1218
+
1219
+ Then take one now, and install a timer that takes one every night:
1220
+
1221
+ ```
1222
+ npx --no frugal-iot-backup
1223
+ npx --no frugal-iot-backup install
1224
+ ```
1225
+
1226
+ Both ask for `sudo`, because the broker's files belong to root and to `mosquitto`. Snapshots go to
1227
+ `/var/backups/frugal-iot/`, keeping one a day for a week, one a week for a month, one a month for
1228
+ three months, one a quarter for a year, and one a year for ever - about 20 small files.
1229
+
1230
+ **A snapshot holds every credential this server has** - with it, someone can log in as any node or
1231
+ user on the broker. So each one is mode 600, owned by the account the server runs as, in a directory
1232
+ nobody else can open. Copy them off the machine the same way: by `rsync` or `scp` as that account,
1233
+ to a machine you trust as much as this one.
1234
+
1235
+ A snapshot on the same SD card is lost with the card, so it only protects against mistakes. To
1236
+ protect against losing the machine, copy `/var/backups/frugal-iot/` and `data/` somewhere else
1237
+ regularly - for example from a laptop:
1238
+
1239
+ ```
1240
+ rsync -a pi@frugaliot.local:/var/backups/frugal-iot/ ~/frugal-iot-backups/frugaliot/state/
1241
+ rsync -a pi@frugaliot.local:frugal-iot/data/ ~/frugal-iot-backups/frugaliot/data/
1242
+ ```
1243
+
1244
+ Leave out `--delete` on the second: the server may delete old readings to save space, and this way
1245
+ the copy keeps them.
1246
+
1247
+ `npx --no frugal-iot-diagnostic` reports how old the newest snapshot is, and calls it a problem if
1248
+ the timer is installed and nothing has been written for two days.
1249
+
1250
+ ### Restoring
1251
+
1252
+ Restore onto a machine where the server is already installed - after re-flashing a Pi, run the
1253
+ installer first, then restore over what it set up. Copy the snapshot onto the machine, then from
1254
+ the server's directory see what would happen - this changes nothing:
1255
+
1256
+ ```
1257
+ npx --no frugal-iot-restore <snapshot>
1258
+ ```
1259
+
1260
+ `latest` in place of a file name means the newest one in `/var/backups/frugal-iot/`. Then do it,
1261
+ in one of two ways:
1262
+
1263
+ * **`state`** - onto a machine its own installer set up, laid out however that is. It restores the
1264
+ database, `config.d/` (including `secrets.yaml`) and the broker's password file, ACL file and
1265
+ account state, each to wherever *this* broker keeps them. It keeps this machine's own broker
1266
+ configuration, TLS certificate, systemd unit, `mqtt.yaml` and `server.yaml`.
1267
+ * **`all`** - onto the same machine, or one laid out exactly like it: everything goes back where it
1268
+ came from. Broker configuration files the snapshot does not have are renamed to
1269
+ `<name>.pre-restore`, so the broker does not read both.
1270
+
1271
+ ```
1272
+ npx --no frugal-iot-restore <snapshot> state
1273
+ ```
1274
+
1275
+ If the machine already has logins of its own, add `force` as the last word - it says so rather
1276
+ than replacing them silently.
1277
+
1278
+ Before changing anything it takes a snapshot of what is there into
1279
+ `/var/backups/frugal-iot/pre-restore/`, so a restore can be undone with
1280
+ `npx --no frugal-iot-restore <that snapshot> all force`. It stops the server and the broker while it
1281
+ works - every node disconnects for a few seconds - and finishes by checking the broker's accounts
1282
+ against the database and running the diagnostic.
1283
+
1284
+ `data/` is not in a snapshot: copy it back from wherever it is mirrored.
1285
+
1202
1286
  ## Moving the Pi to another WiFi network
1203
1287
 
1204
1288
  This is how to give the Pi a new WiFi network when you cannot log in to it, because it no longer
@@ -1317,7 +1401,8 @@ Two things to know:
1317
1401
 
1318
1402
  If you have not reached step 4 yet, the command does not exist. Copy
1319
1403
  [scripts/diagnostic.zsh](https://github.com/mitra42/frugal-iot-server/blob/main/scripts/diagnostic.zsh)
1320
- to the Pi and run `zsh diagnostic.zsh` instead.
1404
+ and [scripts/broker-config.zsh](https://github.com/mitra42/frugal-iot-server/blob/main/scripts/broker-config.zsh),
1405
+ which it needs, into the same directory on the Pi and run `zsh diagnostic.zsh` instead.
1321
1406
 
1322
1407
  ### A topic that will not go away
1323
1408
 
@@ -1363,8 +1448,11 @@ sudo systemctl restart frugaliot
1363
1448
  Taking those in turn:
1364
1449
 
1365
1450
  * `npm update` fetches the new version of the server, and of the client and logger it depends on.
1451
+ It may report vulnerabilities, typically in `uuid`. `frugal-iot-init`, next, is what fixes those,
1452
+ so ignore that report and do not run `npm audit fix`.
1366
1453
  * `frugal-iot-init` adds any configuration file or directory the new version expects and you do not
1367
- have yet. It never changes files you already have, so your settings, database and data are safe.
1454
+ have yet. It never changes your settings, database or data. It also copies the release's
1455
+ dependency `overrides` into your `package.json` and re-runs `npm install` - see below.
1368
1456
  * `systemctl restart frugaliot` is what actually puts the new version into service — without it the
1369
1457
  old one keeps running from memory, and nothing appears to have changed. (Skip this if you are
1370
1458
  running the server by hand rather than as a service; stop it with `Ctrl-C` and start it again.)
@@ -1377,17 +1465,37 @@ journalctl -u frugaliot -n 30
1377
1465
  npm ls frugal-iot-server
1378
1466
  ```
1379
1467
 
1380
- `frugal-iot-init` leaves your own files alone. Two sections of its output are worth reading.
1468
+ `frugal-iot-init` leaves your own files alone, with two exceptions, and tells you what differs.
1381
1469
 
1382
- **"These files were left as you have them, but this release ships a different version"** covers the
1383
- sensor schema under `config.d/schema/`, and gives you a `diff` command for each. If you added a
1384
- sensor type of your own, that is exactly what it should say; if you did not, this release changed
1385
- the schema and the difference is worth reading.
1470
+ **`package.json`** in the install directory gets the `overrides` section from frugal-iot-server's
1471
+ own. These pin transitive dependencies (such as `uuid`, deep below the logger) to versions without
1472
+ known vulnerabilities. npm only reads `overrides` from the top-level project, which here is your
1473
+ `package.json`, not the package's, so without this step `npm audit` reports problems that a checkout
1474
+ of the same release does not. When it adds or changes one it prints `set ./package.json override ...`
1475
+ and runs `npm install` to apply it; `npm audit` should then come back clean.
1386
1476
 
1387
- **"Installed elsewhere from extras/, and older than this release"** covers the files that live
1388
- somewhere else — the broker configuration, its ACL, the systemd unit. It compares what is actually
1389
- in `/etc`, not the copy in `extras/`, because that copy is never overwritten and so differs for ever
1390
- once a release changes it. To bring anything it names up to date, re-run the installer:
1477
+ **The sensor schema** under `config.d/schema/` is the other. If every line of your copy is still
1478
+ in this release's version, all the release did was add to it, so it installs the new one and says
1479
+ `updated ... (this release only adds to it)`. If your copy has lines the release does not, it keeps
1480
+ yours and shows just those lines: if you added them, that is exactly right; if you did not, this
1481
+ release changed or removed them, and it prints the `cp` that takes its version.
1482
+
1483
+ **The broker's configuration** is compared setting by setting across every file the broker reads,
1484
+ wherever they are - one `frugal-iot.conf` from the installer, or several files in
1485
+ `/etc/mosquitto/conf.d/` assembled by hand. Each difference is one line:
1486
+
1487
+ ```
1488
+ missing: connection_messages false - not set (mosquitto's default applies)
1489
+ (fine) password_file - here /etc/mosquitto/mosquitto_passwords (this release's default is /var/lib/mosquitto/passwords)
1490
+ ```
1491
+
1492
+ A file kept at a different path is fine. A `missing:` or `different:` line is something this release
1493
+ sets that your broker does not. Add it to any file in `/etc/mosquitto/conf.d/`, then
1494
+ `sudo systemctl restart mosquitto` - or, on a Pi set up by `install-pi.sh`, re-run that.
1495
+
1496
+ **"Installed elsewhere from extras/, and different from this release"** covers the ACL file and the
1497
+ systemd units, found where the broker and systemd say they are. It lists the differing lines,
1498
+ marked `here only` and `release only`. To bring them up to date on a Pi, re-run the installer:
1391
1499
 
1392
1500
  ```
1393
1501
  bash node_modules/frugal-iot-server/scripts/install-pi.sh
@@ -1403,29 +1511,18 @@ install these three, because none of them can simply be copied into place:
1403
1511
  | `extras/frugaliot.service` | written for user `pi` in `/home/pi/frugal-iot`; the installer rewrites `User`, `WorkingDirectory` and `ExecStart` for wherever this actually went. |
1404
1512
 
1405
1513
  The setup network's watchdog, `extras/frugaliot-ap-watchdog` and its `.service` and `.timer`, *can* be
1406
- copied as they are; step 8a has the commands. On a Pi installed before the setup network existed,
1407
- re-running the installer adds it, and asks once for the `pi` login's password to give it.
1514
+ copied as they are; step 8a has the commands. It is only checked where it is installed. On a Pi
1515
+ installed before the setup network existed, re-running the installer adds it, and asks once for the
1516
+ `pi` login's password to give it.
1408
1517
 
1409
1518
  The installer will not overwrite an ACL file that has per-bridge rules in it. If this release adds a
1410
1519
  rule to a broker that has them, it says which lines are missing and leaves the file alone for you to
1411
1520
  add them by hand, then `sudo systemctl reload mosquitto`.
1412
1521
 
1413
1522
  > **If your broker's configuration was assembled by hand** — several small files in
1414
- > `/etc/mosquitto/conf.d/` rather than the single `frugal-iot.conf` the installer writes — then
1415
- > `frugal-iot-init` reports those files under "Nothing at the usual place, so these were not
1416
- > compared", and re-running the installer is *not* what you want: it would add `frugal-iot.conf`
1417
- > alongside what you already have, declaring the same listeners and the same plugin twice. Compare
1418
- > by hand instead, and add only what is missing:
1419
- >
1420
- > ```
1421
- > sudo grep -hvE '^\s*(#|$)' /etc/mosquitto/conf.d/*.conf | sort -u > /tmp/running.txt
1422
- > grep -hvE '^\s*(#|$)' node_modules/frugal-iot-server/extras/mosquitto.conf | sort -u > /tmp/shipped.txt
1423
- > diff /tmp/running.txt /tmp/shipped.txt
1424
- > ```
1425
- >
1426
- > Lines only in the running config are usually this machine's own — TLS `certfile`/`keyfile`, a
1427
- > bridge `listener`, a `password_file` in a different place. Lines only in the shipped one are what
1428
- > this release added.
1523
+ > `/etc/mosquitto/conf.d/` rather than the single `frugal-iot.conf` the installer writes — do not
1524
+ > re-run the installer: it would add `frugal-iot.conf` alongside what you already have, declaring the
1525
+ > same listeners and the same plugin twice. Add the `missing:` and `different:` settings by hand.
1429
1526
 
1430
1527
  > It does not compare `config.yaml`, `config.d/mqtt.yaml`, `config.d/logger.yaml` or
1431
1528
  > `config.d/server.yaml`, because those hold your own settings and would differ every time. If a
@@ -1437,7 +1534,7 @@ which for the settings under [Wear and tear on the SD card](#wear-and-tear-on-th
1437
1534
  goes on writing as often as it did. A server that has been running since before those settings
1438
1535
  existed keeps logging every message and every web request, and writing every reading as it arrives —
1439
1536
  deliberately, since quietening somebody's server without being asked is not a thing an upgrade should
1440
- do. On a Pi, compare the two files above and copy across the settings you want.
1537
+ do. On a Pi, compare the files named above and copy across the settings you want.
1441
1538
 
1442
1539
  Your organizations, accounts, database and logged data are untouched by an upgrade — they live in
1443
1540
  this directory, not in `node_modules`.
package/docs/OPENWRT.md CHANGED
@@ -24,8 +24,8 @@ Run the preflight script (below) and it checks every line of this table for you.
24
24
 
25
25
  | | Needed | Why |
26
26
  | --- | --- | --- |
27
- | **CPU** | **aarch64 or x86_64.** 32-bit ARM might work. MIPS will not. | OpenWrt's `node` package is only built for some CPUs, and the database module (`sqlite3`) is a compiled binary that has to exist for that CPU and for OpenWrt's C library (musl). Both are most likely on 64-bit. |
28
- | **Memory** | 512 MB or more (256 MB at the very least) | The server uses about 250 MB once running. |
27
+ | **CPU** | **aarch64 or x86_64.** 32-bit ARM might work. **MIPS will not** (Node.js has no MIPS build at all, so older and cheaper routers - the GL.iNet AR300M, for one - are out). | OpenWrt's `node` package is only built for some CPUs, and the database module (`sqlite3`) is a compiled binary that has to exist for that CPU and for OpenWrt's C library (musl). Both are most likely on 64-bit. |
28
+ | **Memory** | **512 MB or more** | The server alone uses about 250 MB once running, on top of the router's own software. A router with 256 MB cannot do it (a Cudy with 233 MB usable was refused). |
29
29
  | **Storage** | **About 400 MB free** on ext4, f2fs or btrfs | The software is ~100 MB; npm needs room to work; readings accumulate. **Most routers' own flash is far too small**, so this means a USB stick or SSD, set up as extroot or just mounted, and `--dir` pointing at it. Not FAT, exFAT or NTFS: the install needs symlinks and Unix permissions. The image may also lack the USB storage driver - see [Preparing the router](#preparing-the-router). |
30
30
  | **Node.js** | **20.17 or later**, and npm | From OpenWrt's feed if it has one - **OpenWrt 25.12 has none for aarch64**, in which case it is unpacked by hand from a musl build ([how](#if-the-feed-has-no-node)). The preflight and the installer accept either. |
31
31
  | **mosquitto** | A build with **websockets** | The browser dashboard connects over websockets. If the router's mosquitto has none, the dashboard cannot work. |
package/package.json CHANGED
@@ -12,8 +12,8 @@
12
12
  "cookie-parser": "^1.4.7",
13
13
  "express": "^5.2.1",
14
14
  "express-session": "^1.19.0",
15
- "frugal-iot-client": "^2.0.11",
16
- "frugal-iot-logger": "^2.0.11",
15
+ "frugal-iot-client": "^2.0.12",
16
+ "frugal-iot-logger": "^2.0.12",
17
17
  "hash-wasm": "^4.12.0",
18
18
  "js-yaml": "^4.2.0",
19
19
  "morgan": "^1.11.0",
@@ -28,6 +28,7 @@
28
28
  "frugal-iot-server": "frugal-iot-server.js",
29
29
  "frugal-iot-init": "scripts/init.zsh",
30
30
  "frugal-iot-addorganization": "scripts/addorganization.zsh",
31
+ "frugal-iot-backup": "scripts/backup.zsh",
31
32
  "frugal-iot-setpassword": "scripts/setpassword.zsh",
32
33
  "frugal-iot-diagnostic": "scripts/diagnostic.zsh",
33
34
  "frugal-iot-clearretained": "scripts/clearretained.js",
@@ -36,6 +37,7 @@
36
37
  "frugal-iot-rebuild-dynsec": "scripts/rebuild-dynsec.js",
37
38
  "frugal-iot-dynsec-init": "scripts/dynsec-init.js",
38
39
  "frugal-iot-resetnode": "scripts/resetnode.js",
40
+ "frugal-iot-restore": "scripts/restore.zsh",
39
41
  "frugal-iot-grafana-token": "scripts/grafana-token.js",
40
42
  "frugal-iot-backfill-opensearch": "scripts/backfill-opensearch.js"
41
43
  },
@@ -95,7 +97,7 @@
95
97
  "check-files": "node scripts/check-published-files.js"
96
98
  },
97
99
  "type": "module",
98
- "version": "2.0.11",
100
+ "version": "2.0.12",
99
101
  "overrides": {
100
102
  "uuid": "^11.1.1"
101
103
  }
@@ -6,7 +6,7 @@
6
6
  // it, an installed browser keeps serving the files it already cached - and since the list now covers
7
7
  // the dashboard's own code, that means old JS behind freshly fetched HTML. Any edit to the list
8
8
  // needs this bumped, or it does not reach anyone who already has the old one.
9
- const CACHE_NAME = 'frugal-iot-cache-2.0.11';
9
+ const CACHE_NAME = 'frugal-iot-cache-2.0.12';
10
10
  /*
11
11
  * Everything the app needs, so that a phone on a poor or expensive link fetches it once.
12
12
  *
@@ -95,27 +95,23 @@ if [[ "$BRIDGE_CREDENTIAL" == derived ]]; then
95
95
  exit 1
96
96
  fi
97
97
 
98
- # Which files the running broker actually uses, rather than where they usually are. Only ".conf"
99
- # files in conf.d are read, and the last setting wins.
100
- MOSQ_CONFS=(/etc/mosquitto/mosquitto.conf(N) /etc/mosquitto/conf.d/*.conf(N))
101
- if (( ${#MOSQ_CONFS} == 0 )); then
102
- echo "Error: no mosquitto configuration found in /etc/mosquitto" >&2
98
+ # Which files the running broker actually uses, rather than where they usually are - production's
99
+ # configuration is several hand-made files in conf.d. scripts/broker-config.zsh reads each file with
100
+ # sudo when it is root-only; this script needs sudo below anyway, so it may prompt.
101
+ BROKER_SUDO=(sudo)
102
+ source "${0:A:h}/broker-config.zsh"
103
+ broker_conf_read
104
+ if (( ${#BROKER_CONF_FILES} == 0 )); then
105
+ echo "Error: no mosquitto configuration found at $(broker_conf_main)" >&2
103
106
  exit 1
104
107
  fi
105
- # Read one setting out of the broker's configuration. Two things to be careful of: a conf.d file
106
- # can be root-only (a bridge configuration holds a password, so it should be), which makes grep
107
- # fail rather than return nothing - and under "set -e" a failing command substitution would end
108
- # this script silently, before it had printed anything at all. Hence "|| true" and the sudo retry.
109
- mosq_setting() {
110
- local out
111
- out=$(grep -hE "^[[:space:]]*$1[[:space:]]" "${MOSQ_CONFS[@]}" 2>/dev/null | tail -1 | awk '{print $2}') || true
112
- if [[ -z "$out" ]] && command -v sudo >/dev/null; then
113
- out=$(sudo grep -hE "^[[:space:]]*$1[[:space:]]" "${MOSQ_CONFS[@]}" 2>/dev/null | tail -1 | awk '{print $2}') || true
114
- fi
115
- print -r -- "$out"
116
- }
117
- PWFILE=$(mosq_setting password_file)
118
- ACLFILE=$(mosq_setting acl_file)
108
+ if (( ${#BROKER_CONF_UNREADABLE} )); then
109
+ echo "Error: could not read ${(j:, :)BROKER_CONF_UNREADABLE}, so cannot tell where the broker" >&2
110
+ echo "keeps its password and ACL files. Re-run with sudo." >&2
111
+ exit 1
112
+ fi
113
+ broker_setting password_file; PWFILE="$BROKER_SETTING"
114
+ broker_setting acl_file; ACLFILE="$BROKER_SETTING"
119
115
 
120
116
  if [[ -z "$PWFILE" ]]; then
121
117
  echo "Error: no password_file in the mosquitto configuration - this broker is not using accounts." >&2
@@ -77,9 +77,15 @@ fi
77
77
 
78
78
  # Ask the broker's own configuration where its password file is, rather than guessing - that way
79
79
  # this works whatever layout a machine uses, including installations older than this script.
80
- # Mosquitto reads /etc/mosquitto/mosquitto.conf and then the .conf files in conf.d, last one wins.
81
- MOSQUITTO_PASSWD_FILE=$(grep -hE '^[[:space:]]*password_file[[:space:]]' \
82
- /etc/mosquitto/mosquitto.conf /etc/mosquitto/conf.d/*.conf(N) 2>/dev/null | tail -1 | awk '{print $2}')
80
+ # sudo may prompt: a Pi's bridge file is root-only, and the password file needs sudo below anyway.
81
+ BROKER_SUDO=(sudo)
82
+ source "${0:A:h}/broker-config.zsh"
83
+ broker_conf_read
84
+ broker_setting password_file; MOSQUITTO_PASSWD_FILE="$BROKER_SETTING"
85
+ if (( ${#BROKER_CONF_UNREADABLE} )); then
86
+ echo "Warning: could not read ${(j:, :)BROKER_CONF_UNREADABLE} - if the password file is named there," >&2
87
+ echo " the one used below may be the wrong one." >&2
88
+ fi
83
89
  USED_LOCAL_MOSQUITTO_FILE=0
84
90
  if [[ -n "$MOSQUITTO_PASSWD_FILE" ]]; then
85
91
  MOSQUITTO_PASSWD_FROM_CONFIG=1