frugal-iot-server 2.0.9 → 2.0.10

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.
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # Install a complete Frugal IoT server - broker, server, service - on a freshly flashed Raspberry Pi.
4
4
  #
5
- # This does automatically what INSTALLATION.md walks through by hand, steps 3 to 8. Read that if you
5
+ # This does automatically what docs/INSTALLATION.md walks through by hand, steps 3 to 8. Read that if you
6
6
  # want to know why any of it is being done; this only says what it is doing as it goes.
7
7
  #
8
8
  # curl -fsSLO https://raw.githubusercontent.com/mitra42/frugal-iot-server/main/scripts/install-pi.sh
@@ -20,15 +20,15 @@
20
20
  # step: compiling sqlite3 on a Zero W takes about 40 minutes and is not repeated once it is done.
21
21
  #
22
22
  # What it does NOT do:
23
- # * flash the SD card, or set up wifi - INSTALLATION.md steps 1 and 2, before you can ssh in
23
+ # * flash the SD card, or set up wifi - docs/INSTALLATION.md steps 1 and 2, before you can ssh in
24
24
  # * point your sensor nodes at this server - that means flashing the nodes, which happens on your
25
25
  # workstation, not here. It prints the settings to give them.
26
- # * HTTPS, which over-the-air firmware updates to ESP32 need - INSTALLATION.md step 10
26
+ # * HTTPS, which over-the-air firmware updates to ESP32 need - docs/INSTALLATION.md step 10
27
27
  # * reboot. A reboot is worth doing afterwards if the upgrade brought a new kernel.
28
28
  #
29
29
  # It does set up what happens when the Pi later LOSES its WiFi - moved to another site, or the router
30
30
  # replaced: a network of its own, frugaliot-bridge-setup, whose page sets a new one. Otherwise a Pi
31
- # on WiFi is unreachable at exactly the moment it needs its WiFi changed. INSTALLATION.md, "Moving the
31
+ # on WiFi is unreachable at exactly the moment it needs its WiFi changed. docs/INSTALLATION.md, "Moving the
32
32
  # Pi to another WiFi network", is the operator's side of it.
33
33
 
34
34
  set -Eeuo pipefail
@@ -460,8 +460,25 @@ ask ORG_EMAIL "Contact email" || exit 1
460
460
  ask ORG_PHONE "Contact phone ('+' and digits only)" || exit 1
461
461
  [[ "$ORG_PHONE" =~ ^[+]?[0-9]+$ ]] || { echo "Phone must be '+' and digits only: '${ORG_PHONE}'" >&2; exit 1; }
462
462
  # Asked for in the order they matter. Each may be typed, passed as an option, or generated.
463
- ask_password SUPER_PW "the superuser login (this server's administrator)"
464
- ask_password LOGIN_PW "the ${ORG_ID} web login"
463
+ #
464
+ # But not on a re-run where the account is already there: further down, an existing superuser or
465
+ # organization keeps the password it has, so asking for one would take what you typed and quietly
466
+ # ignore it - and the summary would then print a made-up password that was never set. What stands
467
+ # in for the password is shown in the summary instead.
468
+ KEPT_PW="(unchanged - set by an earlier run)"
469
+ if [[ -f "${INSTALL_DIR}/frugal-iot.db" ]] && command -v sqlite3 >/dev/null 2>&1 \
470
+ && [[ -n "$(sqlite3 "${INSTALL_DIR}/frugal-iot.db" "SELECT 1 FROM users WHERE username='superuser' AND hashed_password IS NOT NULL;" 2>/dev/null || true)" ]]; then
471
+ SUPER_PW="$KEPT_PW"
472
+ info "the superuser already has a password - leaving it alone"
473
+ else
474
+ ask_password SUPER_PW "the superuser login (this server's administrator)"
475
+ fi
476
+ if [[ -f "${INSTALL_DIR}/config.d/organizations/${ORG_ID}.yaml" ]]; then
477
+ LOGIN_PW="$KEPT_PW"
478
+ info "organization ${ORG_ID} already exists - leaving its login password alone"
479
+ else
480
+ ask_password LOGIN_PW "the ${ORG_ID} web login"
481
+ fi
465
482
  # Not asked for. addorganization.zsh requires one, and lib/retained.js falls back to it on a broker
466
483
  # with no dynamic security - but this installer always sets dynamic security up, so on this Pi every
467
484
  # node is issued its own credential when it enrols and every browser derives its own when it logs
@@ -679,7 +696,7 @@ WANTED_BROKER="ws://${BROKER_HOST}:9012"
679
696
  if [[ "$CURRENT_BROKER" == "$WANTED_BROKER" ]]; then
680
697
  skip "broker already ${WANTED_BROKER}"
681
698
  else
682
- as_user_sh "printf '# This Pi is its own broker - see INSTALLATION.md step 4\nbroker: %s\n' '${WANTED_BROKER}' > '${MQTT_YAML}'"
699
+ as_user_sh "printf '# This Pi is its own broker - see docs/INSTALLATION.md step 4\nbroker: %s\n' '${WANTED_BROKER}' > '${MQTT_YAML}'"
683
700
  ok "broker set to ${WANTED_BROKER} (was ${CURRENT_BROKER:-unset})"
684
701
  fi
685
702
 
@@ -730,7 +747,7 @@ SRC_ACL="${PKG_DIR}/extras/aclfile"
730
747
  # it root-owned 644, which works and only draws a warning from the broker; "fixing" it to 600
731
748
  # without also chowning it to mosquitto stops the broker starting, because it reads this file after
732
749
  # dropping privileges. That is a migration to make deliberately, not a side effect of an upgrade -
733
- # INSTALLATION.md step 11 has it.
750
+ # docs/INSTALLATION.md step 11 has it.
734
751
  if [[ ! -e /etc/mosquitto/aclfile ]]; then
735
752
  sudo_ install -o mosquitto -g mosquitto -m 600 "$SRC_ACL" /etc/mosquitto/aclfile
736
753
  ok "access control list installed at /etc/mosquitto/aclfile"
@@ -875,6 +892,7 @@ ok "broker running"
875
892
  step "Creating the administrator login"
876
893
  if as_user_sh "cd '${INSTALL_DIR}' && sqlite3 frugal-iot.db \"SELECT 1 FROM users WHERE username='superuser' AND hashed_password IS NOT NULL;\"" | grep -q 1; then
877
894
  skip "superuser already has a password"
895
+ SUPER_PW="$KEPT_PW" # whatever was asked or generated above was never used
878
896
  else
879
897
  as_user_sh "cd '${INSTALL_DIR}' && npx --no frugal-iot-setpassword superuser '${SUPER_PW}'" >/dev/null
880
898
  ok "superuser password set"
@@ -883,6 +901,7 @@ fi
883
901
  step "Creating the organization"
884
902
  if [[ -f "${INSTALL_DIR}/config.d/organizations/${ORG_ID}.yaml" ]]; then
885
903
  skip "organization ${ORG_ID} already exists"
904
+ LOGIN_PW="$KEPT_PW"
886
905
  BROKER_PW="$(sed -n 's/^mqtt_password:[[:space:]]*//p' "${INSTALL_DIR}/config.d/organizations/${ORG_ID}.yaml" | head -1 | tr -d "\"'" || true)"
887
906
  info "using the broker password already in its configuration"
888
907
  else
@@ -980,6 +999,18 @@ else
980
999
  echo "The server is not answering on port ${PORT} (last response: ${CODE:-none})." >&2
981
1000
  echo "What it logged:" >&2
982
1001
  sudo_ journalctl -u frugaliot -n 30 --no-pager >&2 || true
1002
+ if [[ "${CODE:-}" == "404" ]]; then
1003
+ # The server is up but cannot find its web pages. Seen when an earlier frugal-iot-init stopped
1004
+ # part-way: the re-run leaves an existing server.yaml alone, still saying "publicdir: ./public".
1005
+ echo "A 404 means the server is up but cannot find its web pages. Look at publicdir and htmldir in" >&2
1006
+ echo "${INSTALL_DIR}/config.d/server.yaml: after a normal install they point into ./node_modules." >&2
1007
+ echo "If publicdir is still './public', set these four lines there and restart" >&2
1008
+ echo "(sudo systemctl restart frugaliot):" >&2
1009
+ echo " htmldir: ./node_modules/frugal-iot-client" >&2
1010
+ echo " nodemodulesdir: ./node_modules" >&2
1011
+ echo " publicdir: ./node_modules/frugal-iot-server/public" >&2
1012
+ echo " privatedir: ./node_modules/frugal-iot-server/private" >&2
1013
+ fi
983
1014
  exit 1
984
1015
  fi
985
1016
  systemctl is-enabled --quiet frugaliot && ok "will start again at boot"
@@ -1055,7 +1086,10 @@ ENROL_SECRET="$(as_user_sh "grep -A1 '^enrolment_${ORG_ID}:' '${INSTALL_DIR}/con
1055
1086
  # Only the generated ones are worth printing - and printing a password you typed would put it in the
1056
1087
  # log for no reason
1057
1088
  was_generated() { local v; for v in ${GENERATED[@]+"${GENERATED[@]}"}; do [[ "$v" == "$1" ]] && return 0; done; return 1; }
1058
- shown() { if was_generated "$1"; then echo "${!1}"; else echo "(the one you gave)"; fi; }
1089
+ shown() {
1090
+ [[ "${!1}" == "("* ]] && { echo "${!1}"; return; } # not set by this run
1091
+ if was_generated "$1"; then echo "${!1}"; else echo "(the one you gave)"; fi
1092
+ }
1059
1093
 
1060
1094
  if (( ${#REBOOT_REASONS[@]} )); then
1061
1095
  REBOOT_TEXT=" 3. REBOOT NEEDED - $(printf '%s' "${REBOOT_REASONS[0]}")."
@@ -1079,7 +1113,7 @@ else
1079
1113
  AP_TEXT=" If this Pi cannot find its WiFi - moved, or a new router - it raises a network
1080
1114
  called ${AP_NAME}. Join it from a phone or laptop; the password is
1081
1115
  ${AP_PW_TEXT}. A page opens to choose the new WiFi.
1082
- INSTALLATION.md, \"Moving the Pi to another WiFi network\", has the rest."
1116
+ docs/INSTALLATION.md, \"Moving the Pi to another WiFi network\", has the rest."
1083
1117
  fi
1084
1118
 
1085
1119
  cat <<SUMMARY
@@ -1130,9 +1164,9 @@ ${REBOOT_TEXT}
1130
1164
 
1131
1165
  4. Outgoing mail, if you want people to be able to reset a forgotten password themselves.
1132
1166
  Without it only you can, on this Pi, with frugal-iot-setpassword.
1133
- Optional - see INSTALLATION.md step 9a.
1167
+ Optional - see docs/INSTALLATION.md step 9a.
1134
1168
 
1135
- 5. HTTPS. NOT WRITTEN YET: step 10 of INSTALLATION.md describes what it will cover - a DNS
1169
+ 5. HTTPS. NOT WRITTEN YET: step 10 of docs/INSTALLATION.md describes what it will cover - a DNS
1136
1170
  name, port forwarding, nginx in front of port ${PORT}, and certbot - but there are no
1137
1171
  instructions to follow there yet. Until then this is a plain HTTP server on your own
1138
1172
  network, which is all an offline installation needs. What you do not get is over-the-air
@@ -0,0 +1,363 @@
1
+ #!/bin/sh
2
+ #
3
+ # Will a Frugal IoT server run on this OpenWrt router? Run this BEFORE install-openwrt.sh.
4
+ #
5
+ # wget -O openwrt-preflight.sh https://raw.githubusercontent.com/mitra42/frugal-iot-server/main/scripts/openwrt-preflight.sh
6
+ # sh openwrt-preflight.sh --install 2>&1 | tee preflight.txt
7
+ #
8
+ # It looks at the things that have decided whether this works, in the order they are likely to stop it:
9
+ # the CPU, memory, a place to put ~400 MB, whether OpenWrt's package feed has a recent enough node,
10
+ # and - by actually trying them - whether this build of mosquitto can do websockets and load the dynamic
11
+ # security plugin, and whether the sqlite3 module can be installed here at all.
12
+ #
13
+ # Without --install it changes nothing except refreshing the package list. With --install it also
14
+ # installs the packages the server needs (the same ones install-openwrt.sh would), so the last three
15
+ # checks can be run for real. It asks first.
16
+ #
17
+ # Plain /bin/sh on purpose: a fresh OpenWrt has no bash. Safe to run as often as you like.
18
+
19
+ set -u
20
+
21
+ DIR=/opt/frugal-iot; INSTALL=0; ASSUME_YES=0
22
+
23
+ usage() {
24
+ cat <<USAGE
25
+ Usage: sh openwrt-preflight.sh [options]
26
+
27
+ --dir <path> where the server would be installed (default: ${DIR}). Only used to check there is
28
+ room, and as the place to try the sqlite3 module.
29
+ --install also install node, mosquitto and the other packages, so everything can be tested
30
+ --yes do not ask before installing
31
+ --help
32
+ USAGE
33
+ exit "${1:-0}"
34
+ }
35
+
36
+ while [ $# -gt 0 ]; do
37
+ case "$1" in
38
+ --dir) [ $# -ge 2 ] || usage 1; DIR=$2; shift 2 ;;
39
+ --install) INSTALL=1; shift ;;
40
+ --yes|-y) ASSUME_YES=1; shift ;;
41
+ --help|-h) usage 0 ;;
42
+ *) echo "Unknown option: $1" >&2; usage 1 ;;
43
+ esac
44
+ done
45
+
46
+ N_PASS=0; N_WARN=0; N_FAIL=0
47
+ BLOCKERS=""
48
+ pass() { N_PASS=$((N_PASS+1)); echo " PASS $*"; }
49
+ warn() { N_WARN=$((N_WARN+1)); echo " WARN $*"; }
50
+ fail() { N_FAIL=$((N_FAIL+1)); echo " FAIL $*"; BLOCKERS="${BLOCKERS}
51
+ - $*"; }
52
+ info() { echo " $*"; }
53
+ head_() { echo; echo "== $* =="; }
54
+
55
+ # Needed by the server and its setup commands. zsh because every "npx frugal-iot-..." command is a
56
+ # zsh script; ca-bundle because npm fetches over https; sqlite3-cli because frugal-iot-init uses it.
57
+ PKGS="node node-npm zsh sqlite3-cli curl ca-bundle"
58
+
59
+ # ---------------------------------------------------------------- package manager
60
+ if command -v opkg >/dev/null 2>&1; then PM=opkg
61
+ elif command -v apk >/dev/null 2>&1; then PM=apk
62
+ else PM=""; fi
63
+
64
+ pkg_update() { case "$PM" in opkg) opkg update >/dev/null 2>&1 ;; apk) apk update >/dev/null 2>&1 ;; esac; }
65
+ pkg_installed() {
66
+ case "$PM" in
67
+ opkg) opkg list-installed 2>/dev/null | grep -q "^$1 " ;;
68
+ apk) apk info -e "$1" >/dev/null 2>&1 ;;
69
+ *) return 1 ;;
70
+ esac
71
+ }
72
+ # Prints the version a feed offers for a package, nothing if it has none
73
+ pkg_version() {
74
+ case "$PM" in
75
+ opkg) opkg list "$1" 2>/dev/null | awk -v n="$1" '$1==n {print $3; exit}' ;;
76
+ apk) apk search -e -v "$1" 2>/dev/null | head -1 | sed "s/^$1-//" ;;
77
+ esac
78
+ }
79
+ pkg_install() {
80
+ case "$PM" in
81
+ opkg) opkg install "$@" ;;
82
+ apk) apk add "$@" ;;
83
+ esac
84
+ }
85
+
86
+ # "20.18.1-r1" -> true if at least 20.17
87
+ node_version_ok() {
88
+ echo "$1" | sed 's/^v//' | awk -F'[.-]' '{ exit !($1 > 20 || ($1 == 20 && $2 >= 17)) }'
89
+ }
90
+
91
+ # Longest mount point that holds a path, and its filesystem type
92
+ mount_info() {
93
+ p=$1
94
+ while [ ! -e "$p" ] && [ "$p" != / ]; do p=$(dirname "$p"); done
95
+ p=$(cd "$p" 2>/dev/null && pwd -P || echo "$p")
96
+ awk -v p="$p" '
97
+ { m = $2; pre = (m == "/") ? "/" : (m "/")
98
+ if ((p == m || index(p, pre) == 1) && length(m) >= length(best)) { best = m; fs = $3 } }
99
+ END { print best, fs }' /proc/mounts
100
+ }
101
+ free_mb() { p=$1; while [ ! -e "$p" ] && [ "$p" != / ]; do p=$(dirname "$p"); done
102
+ df -Pk "$p" 2>/dev/null | awk 'NR==2 {print int($4/1024)}'; }
103
+
104
+ echo "Frugal IoT on OpenWrt - preflight check"
105
+ echo "Date: $(date)"
106
+
107
+ # ---------------------------------------------------------------- the machine
108
+ head_ "The router"
109
+ if [ -r /etc/openwrt_release ]; then
110
+ . /etc/openwrt_release
111
+ info "${DISTRIB_DESCRIPTION:-OpenWrt} target ${DISTRIB_TARGET:-?} package arch ${DISTRIB_ARCH:-?}"
112
+ pass "this is OpenWrt"
113
+ else
114
+ warn "no /etc/openwrt_release - this does not look like OpenWrt. The checks below still run."
115
+ DISTRIB_ARCH=$(uname -m)
116
+ fi
117
+ MODEL=$( { cat /tmp/sysinfo/model || tr -d '\0' < /proc/device-tree/model; } 2>/dev/null || echo unknown)
118
+ info "board: ${MODEL}"
119
+ info "kernel: $(uname -m) $(uname -r)"
120
+ [ -n "$PM" ] && pass "package manager: ${PM}" || fail "neither opkg nor apk found"
121
+ [ "$(id -u)" = 0 ] && pass "running as root" || warn "not root - the installer must run as root"
122
+
123
+ case "${DISTRIB_ARCH:-}" in
124
+ x86_64*|aarch64*)
125
+ pass "CPU architecture ${DISTRIB_ARCH} - the kind this is most likely to work on" ;;
126
+ arm_*|i386*|x86*)
127
+ warn "CPU architecture ${DISTRIB_ARCH}: 32-bit. node may exist for it, but sqlite3 has no ready-made binary there, so it would have to be compiled" ;;
128
+ *)
129
+ fail "CPU architecture ${DISTRIB_ARCH}: OpenWrt is not known to have node for this (mips, powerpc, riscv...). The feed check below is the real test" ;;
130
+ esac
131
+
132
+ MEM_KB=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)
133
+ AVAIL_KB=$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)
134
+ info "memory: $((MEM_KB/1024)) MB total, $((AVAIL_KB/1024)) MB available now"
135
+ if [ "$MEM_KB" -lt 240000 ]; then fail "under 256 MB of memory: the server alone uses about 250 MB"
136
+ elif [ "$MEM_KB" -lt 460000 ]; then warn "under 512 MB: may work, but the router's own services and the server will be competing"
137
+ else pass "memory"; fi
138
+
139
+ YEAR=$(date +%Y)
140
+ if [ "$YEAR" -lt 2024 ]; then warn "the clock says ${YEAR}. Readings are stamped on arrival, so set the time (NTP) first"
141
+ else pass "clock is set (${YEAR})"; fi
142
+
143
+ # ---------------------------------------------------------------- storage
144
+ head_ "Storage"
145
+ info "wanted: about 400 MB free on a normal Linux filesystem (ext4, f2fs, btrfs). The software is"
146
+ info "~100 MB, npm needs room while installing, and sensor data keeps growing."
147
+ info "Not FAT/exFAT/NTFS - the install uses symlinks and Unix permissions."
148
+ echo " Filesystems with room:"
149
+ awk '$3 ~ /^(ext2|ext3|ext4|f2fs|btrfs|xfs|overlay|ubifs|jffs2)$/ {print $2, $3}' /proc/mounts | sort -u | while read -r mnt fs; do
150
+ mb=$(free_mb "$mnt"); [ -n "$mb" ] && echo " ${mnt} (${fs}) ${mb} MB free"
151
+ done
152
+
153
+ set -- $(mount_info "$DIR"); DMNT=${1:-/}; DFS=${2:-?}
154
+ DFREE=$(free_mb "$DIR"); DFREE=${DFREE:-0}
155
+ info "install directory ${DIR} is on ${DMNT} (${DFS}), ${DFREE} MB free"
156
+ case "$DFS" in
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
+ *) if [ "$DFREE" -lt 400 ]; then
159
+ fail "only ${DFREE} MB free for ${DIR}. Attach a USB/SSD drive (extroot, or mount it) and use --dir"
160
+ if ! ls /dev/sd[a-z]* /dev/mmcblk[1-9]* /dev/nvme* >/dev/null 2>&1; then
161
+ # Seen on a real router: the drive was plugged in and formatted ext4, and invisible, because
162
+ # the image carried no USB storage driver.
163
+ info "No USB, SD or NVMe drive is visible at all. If one is plugged in, this OpenWrt image has no"
164
+ info "driver for it. Install the driver, then mount it:"
165
+ info " $( [ "$PM" = apk ] && echo 'apk add' || echo 'opkg install' ) kmod-usb-storage kmod-fs-ext4 block-mount"
166
+ info " block detect > /etc/config/fstab; block mount"
167
+ info "and run this again with --dir /mnt/<drive>/frugal-iot (see 'block info' for the name)."
168
+ else
169
+ info "A drive is visible but has no room under ${DIR}. If it is not mounted: block detect > /etc/config/fstab; block mount"
170
+ fi
171
+ else pass "room for the install (${DFREE} MB)"; fi ;;
172
+ esac
173
+
174
+ # ---------------------------------------------------------------- the package feed
175
+ head_ "OpenWrt's package feed"
176
+ if [ -n "$PM" ]; then
177
+ info "refreshing the package list ($PM) - this changes nothing else"
178
+ pkg_update || warn "could not refresh the package list - no internet from the router?"
179
+ # Node may be on the router without being a package: OpenWrt 25.12 has no node for aarch64, and
180
+ # one that was unpacked by hand from a musl build is just as good. So look at what runs, not at
181
+ # what the package manager knows.
182
+ HAVE_NODE=0
183
+ command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1 && HAVE_NODE=1
184
+ for p in $PKGS; do
185
+ v=$(pkg_version "$p")
186
+ if pkg_installed "$p"; then info "${p}: installed"
187
+ elif [ -n "$v" ]; then info "${p}: available (${v})"
188
+ else
189
+ case "$p" in
190
+ node|node-npm)
191
+ if [ "$HAVE_NODE" = 1 ]; then info "${p}: not in this feed, but node and npm are already on this router"
192
+ elif [ "$p" = node ]; then
193
+ fail "no node package in this feed, and no node on this router. Node 20.17 or later has to be installed by hand - docs/OPENWRT.md, 'If the feed has no node'"
194
+ fi ;;
195
+ zsh) fail "package '${p}' is not in this feed" ;;
196
+ *) warn "package '${p}' is not in this feed" ;;
197
+ esac
198
+ fi
199
+ done
200
+ NV=""; NSRC="in the feed"
201
+ if command -v node >/dev/null 2>&1; then
202
+ NSRC="already on this router"
203
+ NV=$(node -v 2>/dev/null || true)
204
+ [ -n "$NV" ] || fail "node is on this router but will not run - a musl build needs libstdcpp6 and libatomic1"
205
+ else
206
+ NV=$(pkg_version node)
207
+ fi
208
+ if [ -n "$NV" ]; then
209
+ if node_version_ok "$NV"; then pass "node ${NV}, ${NSRC} (the server needs 20.17 or later)"
210
+ else fail "node ${NV} is older than the 20.17 the server needs"; fi
211
+ fi
212
+ if command -v node >/dev/null 2>&1 && ! command -v npm >/dev/null 2>&1; then warn "node is here but npm is not on the PATH"; fi
213
+ MQ=""
214
+ for cand in mosquitto-ssl mosquitto-nossl; do
215
+ if pkg_installed "$cand" || [ -n "$(pkg_version "$cand")" ]; then MQ=$cand; break; fi
216
+ done
217
+ [ -n "$MQ" ] && pass "mosquitto package: ${MQ}" || fail "no mosquitto-ssl or mosquitto-nossl package in this feed"
218
+ info "everything the feed has with 'mosquitto' in its name:"
219
+ case "$PM" in
220
+ opkg) opkg list 2>/dev/null | awk '/^(lib)?mosquitto/ {print " " $1, $2, $3}' ;;
221
+ apk) apk search mosquitto 2>/dev/null | sed 's/^/ /' ;;
222
+ esac
223
+ fi
224
+
225
+ # ---------------------------------------------------------------- optional install
226
+ if [ "$INSTALL" = 1 ] && [ -n "$PM" ]; then
227
+ head_ "Installing what is needed to test the rest"
228
+ MQ_ALL="${MQ:-mosquitto-ssl}"
229
+ case "$MQ_ALL" in *-nossl) MQC=mosquitto-client-nossl ;; *) MQC=mosquitto-client-ssl ;; esac
230
+ WANT=""
231
+ for p in $PKGS; do
232
+ case "$p" in node|node-npm) [ "$HAVE_NODE" = 1 ] && continue ;; esac
233
+ WANT="$WANT $p"
234
+ done
235
+ WANT="${WANT# } $MQ_ALL $MQC"
236
+ info "will install: ${WANT}"
237
+ info "free space on the router's own flash: $(free_mb /) MB. Node alone is tens of MB."
238
+ go=1
239
+ if [ "$ASSUME_YES" != 1 ]; then
240
+ printf ' Install these now? [y/N] '; read -r reply || reply=n
241
+ case "$reply" in y|Y|yes) ;; *) go=0 ;; esac
242
+ fi
243
+ if [ "$go" = 1 ]; then
244
+ for p in $WANT; do
245
+ pkg_installed "$p" && continue
246
+ [ -n "$(pkg_version "$p")" ] || { info "${p} is not in this feed - skipped"; continue; }
247
+ pkg_install "$p" >/dev/null 2>&1 && info "installed ${p}" || warn "could not install ${p}"
248
+ done
249
+ else
250
+ info "skipped - the broker and sqlite3 tests below will be skipped too"
251
+ fi
252
+ fi
253
+
254
+ # ---------------------------------------------------------------- the broker, tried for real
255
+ head_ "mosquitto, tried for real"
256
+ TMP=$(mktemp -d 2>/dev/null || echo /tmp/frugal-preflight-$$); mkdir -p "$TMP"
257
+ cleanup() {
258
+ [ -n "${MQPID:-}" ] && kill "$MQPID" 2>/dev/null
259
+ rm -rf "$TMP"
260
+ [ -n "${SQDIR:-}" ] && rm -rf "$SQDIR"
261
+ }
262
+ trap cleanup EXIT
263
+
264
+ if ! command -v mosquitto >/dev/null 2>&1; then
265
+ warn "mosquitto is not installed, so it cannot be tested. Run again with --install"
266
+ else
267
+ info "$(mosquitto -h 2>&1 | head -1)"
268
+ # Does this build do websockets? The dashboard in a browser needs it. Ask the broker to open one.
269
+ cat > "$TMP/ws.conf" <<CONF
270
+ allow_anonymous true
271
+ listener 29883 127.0.0.1
272
+ protocol mqtt
273
+ listener 29012 127.0.0.1
274
+ protocol websockets
275
+ CONF
276
+ mosquitto -c "$TMP/ws.conf" > "$TMP/ws.log" 2>&1 &
277
+ MQPID=$!; sleep 2
278
+ if kill -0 "$MQPID" 2>/dev/null; then
279
+ pass "websockets listener starts (the browser dashboard needs one)"
280
+ kill "$MQPID" 2>/dev/null; MQPID=""
281
+ else
282
+ fail "this mosquitto will not start with a websockets listener: $(tail -1 "$TMP/ws.log")"
283
+ fi
284
+
285
+ # The dynamic security plugin: where is it, and does it load?
286
+ SO=""
287
+ for cand in /usr/lib/mosquitto/mosquitto_dynamic_security.so /usr/lib/mosquitto_dynamic_security.so \
288
+ /usr/lib/*/mosquitto_dynamic_security.so /usr/lib/*/mosquitto/mosquitto_dynamic_security.so; do
289
+ [ -f "$cand" ] && { SO=$cand; break; }
290
+ done
291
+ if [ -z "$SO" ]; then
292
+ warn "no mosquitto_dynamic_security.so found. The server would run on the older shared-password scheme: every node and browser uses the organization's password. Works, but less secure"
293
+ else
294
+ cat > "$TMP/dyn.conf" <<CONF
295
+ allow_anonymous true
296
+ listener 29884 127.0.0.1
297
+ plugin ${SO}
298
+ plugin_opt_config_file ${TMP}/dynsec.json
299
+ CONF
300
+ mosquitto -c "$TMP/dyn.conf" > "$TMP/dyn.log" 2>&1 &
301
+ MQPID=$!; sleep 2
302
+ if kill -0 "$MQPID" 2>/dev/null; then pass "dynamic security plugin loads (${SO})"
303
+ else warn "dynamic security plugin found at ${SO} but did not load: $(tail -1 "$TMP/dyn.log")"; fi
304
+ kill "$MQPID" 2>/dev/null; MQPID=""
305
+ fi
306
+ for t in mosquitto_ctrl mosquitto_passwd mosquitto_pub mosquitto_sub; do
307
+ command -v "$t" >/dev/null 2>&1 && info "have ${t}" || {
308
+ case "$t" in
309
+ mosquitto_passwd) fail "no mosquitto_passwd: the installer needs it" ;;
310
+ mosquitto_ctrl) [ -n "$SO" ] && warn "no mosquitto_ctrl: the dynamic security plugin cannot be set up without it" ;;
311
+ *) warn "no ${t}" ;;
312
+ esac; }
313
+ done
314
+ fi
315
+
316
+ # ---------------------------------------------------------------- sqlite3, the module that most often fails
317
+ head_ "sqlite3 (the server's database module)"
318
+ if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then
319
+ if [ "$DFREE" -lt 150 ]; then
320
+ warn "not enough room under ${DIR} to try it (${DFREE} MB) - skipped"
321
+ else
322
+ SQDIR="${DIR}/.preflight-$$"
323
+ mkdir -p "$SQDIR" && cd "$SQDIR" || SQDIR=""
324
+ if [ -n "$SQDIR" ]; then
325
+ info "node $(node -v), npm $(npm -v). Trying: npm install sqlite3 (needs the internet)"
326
+ export npm_config_cache="${SQDIR}/.cache"
327
+ if npm install --no-audit --no-fund --loglevel=error 'sqlite3@^6.0.1' > "$SQDIR/npm.log" 2>&1 \
328
+ && node -e "
329
+ const s = require('sqlite3');
330
+ new s.Database(':memory:').get('select sqlite_version() v', (e, r) => {
331
+ if (e) { console.error(e); process.exit(1); } console.log(r.v); })" > "$SQDIR/run.log" 2>&1; then
332
+ pass "sqlite3 installs and runs here (SQLite $(cat "$SQDIR/run.log"))"
333
+ else
334
+ fail "sqlite3 would not install or load on this router. Last lines:"
335
+ tail -8 "$SQDIR/npm.log" "$SQDIR/run.log" 2>/dev/null | sed 's/^/ /'
336
+ fi
337
+ cd /
338
+ fi
339
+ fi
340
+ else
341
+ warn "node/npm are not installed, so sqlite3 cannot be tested. Run again with --install"
342
+ fi
343
+
344
+ # ---------------------------------------------------------------- ports
345
+ head_ "Ports"
346
+ for port in 1883 9012 8080; do
347
+ if netstat -ln 2>/dev/null | grep -q "[:.]${port} "; then warn "port ${port} is already in use here - something else is listening on it"
348
+ else info "port ${port} is free"; fi
349
+ done
350
+
351
+ # ---------------------------------------------------------------- verdict
352
+ head_ "Verdict"
353
+ echo " ${N_PASS} passed, ${N_WARN} warnings, ${N_FAIL} failed"
354
+ if [ "$N_FAIL" -gt 0 ]; then
355
+ echo " NOT READY:${BLOCKERS}"
356
+ elif [ "$N_WARN" -gt 0 ]; then
357
+ echo " Probably workable - read the warnings above."
358
+ else
359
+ echo " Looks good."
360
+ fi
361
+ echo
362
+ echo "Please send everything above to whoever asked you to try this."
363
+ exit 0
File without changes
File without changes