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.
- package/README.md +3 -3
- package/config.d/email.yaml +1 -1
- package/config.d/opensearch.yaml +2 -2
- package/config.d/schema/modules.yaml +13 -0
- package/config.d/schema/topics.yaml +50 -0
- package/docs/OPENWRT.md +208 -0
- package/extras/frugaliot.service +1 -1
- package/extras/mosquitto.conf +1 -1
- package/frugal-iot-createdb.sql +10 -1
- package/frugal-iot-server.js +26 -5
- package/lib/grafana.js +2 -2
- package/lib/language-requests.js +50 -0
- package/package.json +7 -6
- package/public/service-worker.js +10 -2
- package/scripts/addbridge-pi.zsh +1 -1
- package/scripts/addbridge-prod.zsh +2 -2
- package/scripts/addorganization.zsh +11 -2
- package/scripts/diagnostic.zsh +4 -4
- package/scripts/grafana-token.js +1 -1
- package/scripts/init.zsh +5 -1
- package/scripts/install-openwrt.sh +709 -0
- package/scripts/install-pi.sh +46 -12
- package/scripts/openwrt-preflight.sh +363 -0
- /package/{GRAFANA.md → docs/GRAFANA.md} +0 -0
- /package/{INSTALL-OPENSEARCH.md → docs/INSTALL-OPENSEARCH.md} +0 -0
- /package/{INSTALLATION.md → docs/INSTALLATION.md} +0 -0
package/scripts/install-pi.sh
CHANGED
|
@@ -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
|
-
|
|
464
|
-
|
|
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() {
|
|
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
|
|
File without changes
|