frugal-iot-server 2.0.8 → 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 +19 -15
- package/config.d/schema/modules.yaml +13 -0
- package/config.d/schema/topics.yaml +50 -0
- package/{GRAFANA.md → docs/GRAFANA.md} +37 -6
- package/{INSTALL-OPENSEARCH.md → docs/INSTALL-OPENSEARCH.md} +183 -97
- 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 +28 -7
- package/lib/forwarders.js +49 -10
- package/lib/grafana.js +2 -2
- package/lib/language-requests.js +50 -0
- package/lib/opensearch-access.js +9 -2
- 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/{INSTALLATION.md → docs/INSTALLATION.md} +0 -0
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
#
|
|
3
|
+
# Install a complete Frugal IoT server - broker, server, service - on an OpenWrt router.
|
|
4
|
+
#
|
|
5
|
+
# UNTESTED ON REAL HARDWARE. It is install-pi.sh's job done with opkg, procd and BusyBox, written
|
|
6
|
+
# from knowledge of OpenWrt rather than from a router. Run scripts/openwrt-preflight.sh first; if that
|
|
7
|
+
# reports FAIL, this will too. docs/OPENWRT.md says what to send back when something goes wrong.
|
|
8
|
+
#
|
|
9
|
+
# wget -O install-openwrt.sh https://raw.githubusercontent.com/mitra42/frugal-iot-server/main/scripts/install-openwrt.sh
|
|
10
|
+
# sh install-openwrt.sh --org myfarm --name "My Farm" --email you@example.com --phone +61123456789 \
|
|
11
|
+
# --dir /mnt/sda1/frugal-iot
|
|
12
|
+
#
|
|
13
|
+
# Plain /bin/sh on purpose - a fresh OpenWrt has neither bash nor zsh. (zsh IS installed here, because
|
|
14
|
+
# the server's own setup commands - npx frugal-iot-addorganization and friends - are zsh scripts.)
|
|
15
|
+
#
|
|
16
|
+
# Passwords not given are generated and printed at the end, and kept in a log readable only by root.
|
|
17
|
+
# Safe to run again: each step checks whether it is already done.
|
|
18
|
+
#
|
|
19
|
+
# What it does NOT do: set up HTTPS (over-the-air updates need it), open anything in the firewall, or
|
|
20
|
+
# flash your sensor nodes. It prints the settings to give them.
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------- log everything, keep the exit code
|
|
23
|
+
# Re-run ourselves with output going to the terminal and a log. A pipe to tee loses the exit status
|
|
24
|
+
# in plain sh, so it is passed out through a file.
|
|
25
|
+
case " $* " in
|
|
26
|
+
*" --dry-run "*|*" --help "*|*" -h "*) ;;
|
|
27
|
+
*)
|
|
28
|
+
if [ -z "${FRUGAL_INSTALL_LOGGING:-}" ] && [ -f "$0" ]; then
|
|
29
|
+
FRUGAL_INSTALL_LOG="/root/frugal-iot-install-$(date +%Y%m%d-%H%M%S).log"
|
|
30
|
+
( umask 077; : >> "$FRUGAL_INSTALL_LOG" )
|
|
31
|
+
RC_FILE=$(mktemp)
|
|
32
|
+
export FRUGAL_INSTALL_LOGGING=1 FRUGAL_INSTALL_LOG
|
|
33
|
+
( sh "$0" "$@" && echo 0 > "$RC_FILE" || echo $? > "$RC_FILE" ) 2>&1 | tee -a "$FRUGAL_INSTALL_LOG"
|
|
34
|
+
RC=$(cat "$RC_FILE"); rm -f "$RC_FILE"
|
|
35
|
+
exit "${RC:-1}"
|
|
36
|
+
fi ;;
|
|
37
|
+
esac
|
|
38
|
+
|
|
39
|
+
set -eu
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------- settings and arguments
|
|
42
|
+
|
|
43
|
+
ORG_ID=""; ORG_NAME=""; ORG_EMAIL=""; ORG_PHONE=""
|
|
44
|
+
BROKER_PW=""; SUPER_PW=""; LOGIN_PW=""; BROKER_HOST=""
|
|
45
|
+
INSTALL_DIR="/opt/frugal-iot"; RUN_USER="root"
|
|
46
|
+
ASSUME_YES=0; DRY_RUN=0; FORCE=0
|
|
47
|
+
LOG="${FRUGAL_INSTALL_LOG:-none}"
|
|
48
|
+
MQ_STATE="/etc/mosquitto/state"
|
|
49
|
+
|
|
50
|
+
usage() {
|
|
51
|
+
cat <<USAGE
|
|
52
|
+
Usage: sh install-openwrt.sh [options]
|
|
53
|
+
|
|
54
|
+
--org <id> organization id, 1-10 lower-case letters or digits. Becomes the first part of
|
|
55
|
+
every MQTT topic, so it must match what your nodes publish to.
|
|
56
|
+
--name "<name>" organization display name
|
|
57
|
+
--email <address> contact address for the organization
|
|
58
|
+
--phone <number> contact phone, "+" and digits only
|
|
59
|
+
--superuser-password password for this server's administrator login (generated if not given)
|
|
60
|
+
--login-password web login password for the organization's own account (generated if not given)
|
|
61
|
+
--broker-password the organization's shared broker credential. Generated, and not needed: nodes
|
|
62
|
+
are issued their own when they enrol. Set it only to match nodes flashed
|
|
63
|
+
before enrolment existed.
|
|
64
|
+
--broker-host <host> what browsers and nodes should call this router. Default: its LAN address.
|
|
65
|
+
--dir <path> where to install. Default: ${INSTALL_DIR}. Needs ~400 MB free on ext4/f2fs/btrfs,
|
|
66
|
+
so on most routers this is a USB or SSD drive: --dir /mnt/sda1/frugal-iot
|
|
67
|
+
--user <name> account to run the server as. Default: root, which is what most OpenWrt
|
|
68
|
+
services do. A named account is created if it does not exist.
|
|
69
|
+
--yes never ask anything
|
|
70
|
+
--force carry on past checks that would otherwise stop the install
|
|
71
|
+
--dry-run check the arguments and the router, say what would be done, change nothing
|
|
72
|
+
--help
|
|
73
|
+
USAGE
|
|
74
|
+
exit "${1:-0}"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
need_arg() { [ $# -ge 2 ] || { echo "$1 needs a value" >&2; usage 1; }; }
|
|
78
|
+
|
|
79
|
+
while [ $# -gt 0 ]; do
|
|
80
|
+
case "$1" in
|
|
81
|
+
--org) need_arg "$@"; ORG_ID=$2; shift 2 ;;
|
|
82
|
+
--name) need_arg "$@"; ORG_NAME=$2; shift 2 ;;
|
|
83
|
+
--email) need_arg "$@"; ORG_EMAIL=$2; shift 2 ;;
|
|
84
|
+
--phone) need_arg "$@"; ORG_PHONE=$2; shift 2 ;;
|
|
85
|
+
--superuser-password) need_arg "$@"; SUPER_PW=$2; shift 2 ;;
|
|
86
|
+
--login-password) need_arg "$@"; LOGIN_PW=$2; shift 2 ;;
|
|
87
|
+
--broker-password) need_arg "$@"; BROKER_PW=$2; shift 2 ;;
|
|
88
|
+
--broker-host) need_arg "$@"; BROKER_HOST=$2; shift 2 ;;
|
|
89
|
+
--dir) need_arg "$@"; INSTALL_DIR=$2; shift 2 ;;
|
|
90
|
+
--user) need_arg "$@"; RUN_USER=$2; shift 2 ;;
|
|
91
|
+
--yes|-y) ASSUME_YES=1; shift ;;
|
|
92
|
+
--force) FORCE=1; shift ;;
|
|
93
|
+
--dry-run) DRY_RUN=1; shift ;;
|
|
94
|
+
--help|-h) usage 0 ;;
|
|
95
|
+
*) echo "Unknown option: $1" >&2; usage 1 ;;
|
|
96
|
+
esac
|
|
97
|
+
done
|
|
98
|
+
|
|
99
|
+
STEP_N=0
|
|
100
|
+
step() { STEP_N=$((STEP_N+1)); echo; echo "=== ${STEP_N}. $* ==="; }
|
|
101
|
+
info() { echo " $*"; }
|
|
102
|
+
ok() { echo " ok: $*"; }
|
|
103
|
+
skip() { echo " already done: $*"; }
|
|
104
|
+
warn() { echo " WARNING: $*"; }
|
|
105
|
+
die() { echo; echo "STOPPED: $*" >&2; exit 1; }
|
|
106
|
+
|
|
107
|
+
on_exit() {
|
|
108
|
+
rc=$?
|
|
109
|
+
if [ "$rc" -ne 0 ] && [ "$DRY_RUN" = 0 ]; then
|
|
110
|
+
echo
|
|
111
|
+
echo "=== Stopped (exit ${rc}) ==="
|
|
112
|
+
echo "Nothing after the last 'ok' above has been done. Run it again once the cause is dealt"
|
|
113
|
+
echo "with - the steps that succeeded are not repeated."
|
|
114
|
+
[ "$LOG" != none ] && echo "The log of this run, to send back if you need help: ${LOG}"
|
|
115
|
+
fi
|
|
116
|
+
}
|
|
117
|
+
trap on_exit EXIT
|
|
118
|
+
|
|
119
|
+
randpw() { LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16 || true; }
|
|
120
|
+
|
|
121
|
+
ask() { # ask <variable> <prompt>
|
|
122
|
+
eval "cur=\${$1:-}"
|
|
123
|
+
[ -n "$cur" ] && return 0
|
|
124
|
+
if [ "$ASSUME_YES" = 1 ] || [ ! -t 0 ]; then
|
|
125
|
+
die "nothing given for '$2', and there is no terminal to ask at. Pass it on the command line - see --help."
|
|
126
|
+
fi
|
|
127
|
+
printf ' %s: ' "$2"; read -r reply
|
|
128
|
+
[ -n "$reply" ] || die "that cannot be empty"
|
|
129
|
+
eval "$1=\$reply"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Passwords go inside single quotes on a shell command line, so refuse one that contains a quote
|
|
133
|
+
no_quote() { case "$2" in *\'*) die "$1 must not contain a single quote" ;; esac; }
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------- package manager
|
|
136
|
+
if command -v opkg >/dev/null 2>&1; then PM=opkg
|
|
137
|
+
elif command -v apk >/dev/null 2>&1; then PM=apk
|
|
138
|
+
else PM=""; fi
|
|
139
|
+
|
|
140
|
+
pkg_update() { case "$PM" in opkg) opkg update ;; apk) apk update ;; esac; }
|
|
141
|
+
pkg_installed() {
|
|
142
|
+
case "$PM" in
|
|
143
|
+
opkg) opkg list-installed 2>/dev/null | grep -q "^$1 " ;;
|
|
144
|
+
apk) apk info -e "$1" >/dev/null 2>&1 ;;
|
|
145
|
+
*) return 1 ;;
|
|
146
|
+
esac
|
|
147
|
+
}
|
|
148
|
+
pkg_available() {
|
|
149
|
+
case "$PM" in
|
|
150
|
+
opkg) opkg list 2>/dev/null | grep -q "^$1 - " ;;
|
|
151
|
+
apk) apk search -e "$1" 2>/dev/null | grep -q . ;;
|
|
152
|
+
*) return 1 ;;
|
|
153
|
+
esac
|
|
154
|
+
}
|
|
155
|
+
pkg_add() { # install packages, stopping on the first that cannot be had, and saying which
|
|
156
|
+
for p in "$@"; do
|
|
157
|
+
pkg_installed "$p" && continue
|
|
158
|
+
case "$PM" in
|
|
159
|
+
opkg) opkg install "$p" ;;
|
|
160
|
+
apk) apk add "$p" ;;
|
|
161
|
+
esac || die "could not install package '${p}'"
|
|
162
|
+
info "installed ${p}"
|
|
163
|
+
done
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
node_version_ok() { # "v20.17.0" -> succeeds if at least 20.17
|
|
167
|
+
echo "$1" | sed 's/^v//' | awk -F'[.-]' '{ exit !($1 > 20 || ($1 == 20 && $2 >= 17)) }'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
mount_info() { # "mountpoint fstype" of whatever holds a path (or its nearest existing parent)
|
|
171
|
+
p=$1
|
|
172
|
+
while [ ! -e "$p" ] && [ "$p" != / ]; do p=$(dirname "$p"); done
|
|
173
|
+
p=$(cd "$p" 2>/dev/null && pwd -P || echo "$p")
|
|
174
|
+
awk -v p="$p" '
|
|
175
|
+
{ m = $2; pre = (m == "/") ? "/" : (m "/")
|
|
176
|
+
if ((p == m || index(p, pre) == 1) && length(m) >= length(best)) { best = m; fs = $3 } }
|
|
177
|
+
END { print best, fs }' /proc/mounts
|
|
178
|
+
}
|
|
179
|
+
free_mb() {
|
|
180
|
+
p=$1; while [ ! -e "$p" ] && [ "$p" != / ]; do p=$(dirname "$p"); done
|
|
181
|
+
df -Pk "$p" 2>/dev/null | awk 'NR==2 {print int($4/1024)}'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Run a command as the account that owns the install, from the install directory. npm's cache goes
|
|
185
|
+
# under the install directory too: the router's own flash is small, and the default is ~/.npm.
|
|
186
|
+
in_dir() {
|
|
187
|
+
cmd="export HOME='${INSTALL_DIR}' npm_config_cache='${INSTALL_DIR}/.npm-cache' NPM_CONFIG_UPDATE_NOTIFIER=false; cd '${INSTALL_DIR}' && $1"
|
|
188
|
+
if [ "$RUN_USER" = root ]; then sh -c "$cmd"; else su -s /bin/sh "$RUN_USER" -c "$cmd"; fi
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# OpenWrt's BusyBox has no install(1) - it is in the separate coreutils-install package, which
|
|
192
|
+
# stopped the first real run at the first file it tried to place. Same effect with cp, chown, chmod
|
|
193
|
+
# and mv; the mv makes it atomic, so the broker never sees a half-written file.
|
|
194
|
+
put() { # put <mode> <owner> <group> <source> <destination>
|
|
195
|
+
put_tmp="$5.new.$$"
|
|
196
|
+
cp "$4" "$put_tmp" && chown "$2:$3" "$put_tmp" && chmod "$1" "$put_tmp" && mv -f "$put_tmp" "$5"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------- is this a machine we can do this on
|
|
200
|
+
|
|
201
|
+
echo "Frugal IoT server installation - OpenWrt"
|
|
202
|
+
[ "$DRY_RUN" = 1 ] && echo "Dry run - nothing will be changed" || echo "Log: ${LOG}"
|
|
203
|
+
|
|
204
|
+
step "Checking the router"
|
|
205
|
+
[ "$(uname -s)" = Linux ] || die "this is $(uname -s), not Linux"
|
|
206
|
+
[ "$(id -u)" = 0 ] || die "run this as root - it installs packages and a system service"
|
|
207
|
+
if [ -r /etc/openwrt_release ]; then
|
|
208
|
+
. /etc/openwrt_release
|
|
209
|
+
info "${DISTRIB_DESCRIPTION:-OpenWrt} (${DISTRIB_TARGET:-?}, ${DISTRIB_ARCH:-?})"
|
|
210
|
+
elif [ "$FORCE" = 1 ]; then
|
|
211
|
+
warn "not OpenWrt - carrying on because of --force"
|
|
212
|
+
DISTRIB_ARCH=$(uname -m)
|
|
213
|
+
else
|
|
214
|
+
die "this does not look like OpenWrt (no /etc/openwrt_release). For Raspberry Pi OS or Debian use install-pi.sh. --force overrides."
|
|
215
|
+
fi
|
|
216
|
+
[ -n "$PM" ] || die "found neither opkg nor apk"
|
|
217
|
+
info "package manager: ${PM}"
|
|
218
|
+
[ -r /tmp/sysinfo/model ] && info "board: $(cat /tmp/sysinfo/model)"
|
|
219
|
+
|
|
220
|
+
case "${DISTRIB_ARCH:-}" in
|
|
221
|
+
x86_64*|aarch64*) ;;
|
|
222
|
+
*) warn "CPU architecture ${DISTRIB_ARCH:-unknown}: node and the sqlite3 module may not exist for it. openwrt-preflight.sh says whether they do." ;;
|
|
223
|
+
esac
|
|
224
|
+
|
|
225
|
+
MEM_KB=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)
|
|
226
|
+
info "memory: $((MEM_KB / 1024)) MB"
|
|
227
|
+
if [ "$MEM_KB" -lt 240000 ] && [ "$FORCE" != 1 ]; then die "under 256 MB of memory - the server alone uses about 250 MB. --force overrides."; fi
|
|
228
|
+
NPM_FLAGS="--no-audit --no-fund"
|
|
229
|
+
[ "$MEM_KB" -lt 1200000 ] && NPM_FLAGS="--maxsockets 1 --no-audit --no-fund"
|
|
230
|
+
|
|
231
|
+
set -- $(mount_info "$INSTALL_DIR"); DMNT=${1:-/}; DFS=${2:-?}
|
|
232
|
+
DFREE=$(free_mb "$INSTALL_DIR"); DFREE=${DFREE:-0}
|
|
233
|
+
info "install into ${INSTALL_DIR}: on ${DMNT} (${DFS}), ${DFREE} MB free"
|
|
234
|
+
case "$DFS" in
|
|
235
|
+
vfat|exfat|msdos|ntfs|fuseblk)
|
|
236
|
+
die "${INSTALL_DIR} is on ${DFS}, which has no Unix permissions or symlinks. Use an ext4 drive: --dir /mnt/<drive>/frugal-iot" ;;
|
|
237
|
+
esac
|
|
238
|
+
if [ "$DFREE" -lt 400 ] && [ "$FORCE" != 1 ]; then
|
|
239
|
+
die "only ${DFREE} MB free under ${INSTALL_DIR}; about 400 MB is wanted. Attach a drive and use --dir, or --force."
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
ask ORG_ID "Organization id (1-10 lower-case letters or digits, e.g. myfarm)"
|
|
243
|
+
echo "$ORG_ID" | grep -qE '^[a-z0-9]{1,10}$' || die "organization id must be 1-10 lower-case letters or digits: '${ORG_ID}'"
|
|
244
|
+
ask ORG_NAME "Organization display name (e.g. My Farm)"
|
|
245
|
+
ask ORG_EMAIL "Contact email"
|
|
246
|
+
ask ORG_PHONE "Contact phone ('+' and digits only)"
|
|
247
|
+
echo "$ORG_PHONE" | grep -qE '^[+]?[0-9]+$' || die "phone must be '+' and digits only: '${ORG_PHONE}'"
|
|
248
|
+
for v in "ORG_NAME:$ORG_NAME" "ORG_EMAIL:$ORG_EMAIL" "SUPER_PW:$SUPER_PW" "LOGIN_PW:$LOGIN_PW" "BROKER_PW:$BROKER_PW"; do
|
|
249
|
+
no_quote "${v%%:*}" "${v#*:}"
|
|
250
|
+
done
|
|
251
|
+
|
|
252
|
+
GENERATED=""
|
|
253
|
+
[ -n "$SUPER_PW" ] || { SUPER_PW=$(randpw); GENERATED="$GENERATED SUPER_PW"; }
|
|
254
|
+
[ -n "$LOGIN_PW" ] || { LOGIN_PW=$(randpw); GENERATED="$GENERATED LOGIN_PW"; }
|
|
255
|
+
[ -n "$BROKER_PW" ] || BROKER_PW=$(randpw)
|
|
256
|
+
|
|
257
|
+
# A router has a fixed LAN address, which beats a ".local" name: that needs umdns installed, and
|
|
258
|
+
# Android cannot resolve it at all.
|
|
259
|
+
if [ -z "$BROKER_HOST" ]; then
|
|
260
|
+
BROKER_HOST=$(uci -q get network.lan.ipaddr 2>/dev/null | sed 's|/.*||' || true)
|
|
261
|
+
[ -n "$BROKER_HOST" ] || BROKER_HOST=$(ip -4 -o addr show br-lan 2>/dev/null | awk '{print $4}' | sed 's|/.*||' | head -1 || true)
|
|
262
|
+
[ -n "$BROKER_HOST" ] || die "could not work out this router's LAN address - say it with --broker-host"
|
|
263
|
+
fi
|
|
264
|
+
info "organization: ${ORG_ID} (${ORG_NAME})"
|
|
265
|
+
info "broker URL: ws://${BROKER_HOST}:9012"
|
|
266
|
+
info "run as: ${RUN_USER}"
|
|
267
|
+
|
|
268
|
+
# Node, from wherever it came. OpenWrt 25.12 has no node package for aarch64 at all, so a router
|
|
269
|
+
# like that has it unpacked by hand from a musl build (docs/OPENWRT.md says how). Insisting on the
|
|
270
|
+
# package then fails with "no such package" on a router that has a perfectly good node.
|
|
271
|
+
NODE_PKGS="node node-npm"
|
|
272
|
+
if command -v node >/dev/null 2>&1; then
|
|
273
|
+
node -v >/dev/null 2>&1 || die "node is on this router but will not run. A musl build of node usually needs libstdcpp6 and libatomic1 ($( [ "$PM" = apk ] && echo 'apk add' || echo 'opkg install' ) libstdcpp6 libatomic1)."
|
|
274
|
+
command -v npm >/dev/null 2>&1 || die "node $(node -v) is here but npm is not. Put npm on the PATH, or install node-npm."
|
|
275
|
+
NODE_PKGS=""
|
|
276
|
+
info "node: $(node -v), already on this router, so no node package is installed"
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
if [ "$DRY_RUN" = 1 ]; then
|
|
280
|
+
cat <<PLAN
|
|
281
|
+
|
|
282
|
+
Nothing has been changed. With the same arguments and without --dry-run, this would:
|
|
283
|
+
* install packages: ${NODE_PKGS:+${NODE_PKGS} }zsh sqlite3-cli curl ca-bundle, and mosquitto with its clients
|
|
284
|
+
* npm install frugal-iot-server into ${INSTALL_DIR}
|
|
285
|
+
* point the broker at ws://${BROKER_HOST}:9012, keeping its state under ${MQ_STATE} (not /var,
|
|
286
|
+
which OpenWrt keeps in RAM and empties at every reboot)
|
|
287
|
+
* configure mosquitto, with listeners on 1883 and 9012, then create the superuser and the ${ORG_ID} organization
|
|
288
|
+
* install /etc/init.d/frugaliot (procd) and start it
|
|
289
|
+
PLAN
|
|
290
|
+
exit 0
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
# ---------------------------------------------------------------- packages
|
|
294
|
+
|
|
295
|
+
step "Installing packages"
|
|
296
|
+
info "refreshing the package list"
|
|
297
|
+
pkg_update || die "could not refresh the package list - does the router have internet?"
|
|
298
|
+
|
|
299
|
+
if pkg_installed mosquitto-ssl || pkg_installed mosquitto-nossl; then MQ_PKG=installed
|
|
300
|
+
elif pkg_available mosquitto-ssl; then MQ_PKG=mosquitto-ssl
|
|
301
|
+
elif pkg_available mosquitto-nossl; then MQ_PKG=mosquitto-nossl
|
|
302
|
+
else die "this package feed has neither mosquitto-ssl nor mosquitto-nossl"; fi
|
|
303
|
+
case "$MQ_PKG" in
|
|
304
|
+
mosquitto-nossl) MQ_CLIENT=mosquitto-client-nossl ;;
|
|
305
|
+
*) MQ_CLIENT=mosquitto-client-ssl ;;
|
|
306
|
+
esac
|
|
307
|
+
[ "$MQ_PKG" = installed ] && MQ_PKG=""; pkg_installed mosquitto-nossl && MQ_CLIENT=mosquitto-client-nossl
|
|
308
|
+
|
|
309
|
+
info "free space on the router's own flash before: $(free_mb /) MB"
|
|
310
|
+
if [ -n "$NODE_PKGS" ] && ! pkg_available node; then
|
|
311
|
+
die "this package feed has no node (OpenWrt 25.12 has none for aarch64). Install Node.js 20.17 or later yourself - docs/OPENWRT.md, 'If the feed has no node' - then run this again."
|
|
312
|
+
fi
|
|
313
|
+
# shellcheck disable=SC2086
|
|
314
|
+
pkg_add ca-bundle ${NODE_PKGS} zsh sqlite3-cli curl ${MQ_PKG} ${MQ_CLIENT}
|
|
315
|
+
info "free space on the router's own flash after: $(free_mb /) MB"
|
|
316
|
+
# Written to the log on purpose: which mosquitto parts this OpenWrt ships is the main unknown
|
|
317
|
+
case "$PM" in
|
|
318
|
+
opkg) info "mosquitto packages in this feed: $(opkg list 2>/dev/null | awk '/^(lib)?mosquitto/ {printf "%s ", $1}')" ;;
|
|
319
|
+
apk) info "mosquitto packages in this feed: $(apk search mosquitto 2>/dev/null | tr '\n' ' ')" ;;
|
|
320
|
+
esac
|
|
321
|
+
|
|
322
|
+
NODE_VERSION=$(node -v)
|
|
323
|
+
info "node ${NODE_VERSION}, npm $(npm -v)"
|
|
324
|
+
node_version_ok "$NODE_VERSION" || die "node ${NODE_VERSION} is older than the 20.17 the server needs"
|
|
325
|
+
command -v mosquitto_passwd >/dev/null 2>&1 || die "no mosquitto_passwd command after installing mosquitto - the organization's broker account cannot be created"
|
|
326
|
+
command -v mosquitto_pub >/dev/null 2>&1 || die "no mosquitto_pub - the broker test cannot run"
|
|
327
|
+
ok "packages installed"
|
|
328
|
+
|
|
329
|
+
# ---------------------------------------------------------------- the account that runs it
|
|
330
|
+
|
|
331
|
+
step "The account the server runs as"
|
|
332
|
+
if [ "$RUN_USER" = root ]; then
|
|
333
|
+
info "root - as most OpenWrt services run. Use --user <name> to run as a separate account."
|
|
334
|
+
else
|
|
335
|
+
if id "$RUN_USER" >/dev/null 2>&1; then
|
|
336
|
+
skip "user ${RUN_USER} exists"
|
|
337
|
+
else
|
|
338
|
+
# BusyBox adduser: -D no password, -H no home directory. The shell is /bin/false, so everything
|
|
339
|
+
# run as this account goes through "su -s /bin/sh".
|
|
340
|
+
if command -v adduser >/dev/null 2>&1 && adduser -D -H -s /bin/false -h "$INSTALL_DIR" "$RUN_USER" 2>/dev/null; then :
|
|
341
|
+
elif command -v useradd >/dev/null 2>&1 && useradd -M -s /bin/false -d "$INSTALL_DIR" "$RUN_USER"; then :
|
|
342
|
+
else die "could not create user ${RUN_USER}. Install the 'shadow-useradd' package, or leave --user out to run as root."; fi
|
|
343
|
+
ok "created user ${RUN_USER}"
|
|
344
|
+
fi
|
|
345
|
+
fi
|
|
346
|
+
mkdir -p "$INSTALL_DIR"
|
|
347
|
+
[ "$RUN_USER" = root ] || chown "$RUN_USER" "$INSTALL_DIR"
|
|
348
|
+
|
|
349
|
+
# ---------------------------------------------------------------- the server
|
|
350
|
+
|
|
351
|
+
step "Installing the Frugal IoT server"
|
|
352
|
+
PKG_DIR="${INSTALL_DIR}/node_modules/frugal-iot-server"
|
|
353
|
+
if [ -d "$PKG_DIR" ]; then
|
|
354
|
+
skip "already installed in ${INSTALL_DIR}"
|
|
355
|
+
else
|
|
356
|
+
# shellcheck disable=SC2086
|
|
357
|
+
in_dir "npm install ${NPM_FLAGS} frugal-iot-server" || die "npm install failed. The reason is above."
|
|
358
|
+
ok "installed"
|
|
359
|
+
fi
|
|
360
|
+
info "server $(in_dir "node -e \"console.log(require('${PKG_DIR}/package.json').version)\"")"
|
|
361
|
+
|
|
362
|
+
step "Checking the compiled part of sqlite3 actually loads"
|
|
363
|
+
if in_dir "node -e \"require('sqlite3')\"" >/dev/null 2>&1; then
|
|
364
|
+
ok "sqlite3 loads"
|
|
365
|
+
else
|
|
366
|
+
in_dir "node -e \"require('sqlite3')\"" 2>&1 | tail -5 | sed 's/^/ /' || true
|
|
367
|
+
die "sqlite3 will not load on this router. It is a compiled module, and the ready-made binary the server relies on is probably not published for this CPU and C library (OpenWrt uses musl). The messages above are what to send back."
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
step "Setting up the server's directory"
|
|
371
|
+
in_dir "npx --no frugal-iot-init" || die "frugal-iot-init failed"
|
|
372
|
+
ok "configuration, directories and database in place"
|
|
373
|
+
|
|
374
|
+
step "Pointing the server at this router's own broker"
|
|
375
|
+
MQTT_YAML="${INSTALL_DIR}/config.d/mqtt.yaml"
|
|
376
|
+
CURRENT_BROKER=$(sed -n 's/^broker:[[:space:]]*//p' "$MQTT_YAML" 2>/dev/null | head -1 || true)
|
|
377
|
+
WANTED_BROKER="ws://${BROKER_HOST}:9012"
|
|
378
|
+
if [ "$CURRENT_BROKER" = "$WANTED_BROKER" ]; then
|
|
379
|
+
skip "broker already ${WANTED_BROKER}"
|
|
380
|
+
else
|
|
381
|
+
in_dir "printf '# This router is its own broker - see docs/OPENWRT.md\nbroker: %s\n' '${WANTED_BROKER}' > '${MQTT_YAML}'"
|
|
382
|
+
ok "broker set to ${WANTED_BROKER} (was ${CURRENT_BROKER:-unset})"
|
|
383
|
+
fi
|
|
384
|
+
|
|
385
|
+
# ---------------------------------------------------------------- the broker
|
|
386
|
+
|
|
387
|
+
step "Configuring the broker"
|
|
388
|
+
# Who mosquitto drops to after reading its configuration. Everything it opens afterwards - the
|
|
389
|
+
# password file, the ACL, the plugin's state - has to belong to that account.
|
|
390
|
+
MQ_CONF=/etc/mosquitto/mosquitto.conf
|
|
391
|
+
MQ_USER=$(awk '/^[[:space:]]*user[[:space:]]/ {print $2}' "$MQ_CONF" 2>/dev/null | tail -1 || true)
|
|
392
|
+
[ -n "$MQ_USER" ] || MQ_USER=mosquitto
|
|
393
|
+
id "$MQ_USER" >/dev/null 2>&1 || { warn "no '${MQ_USER}' account on this router - the broker will use 'nobody'"; MQ_USER=nobody; }
|
|
394
|
+
MQ_GROUP=$(id -gn "$MQ_USER")
|
|
395
|
+
info "the broker runs as ${MQ_USER}:${MQ_GROUP}"
|
|
396
|
+
|
|
397
|
+
mkdir -p /etc/mosquitto/conf.d "$MQ_STATE"
|
|
398
|
+
chown "$MQ_USER:$MQ_GROUP" "$MQ_STATE"; chmod 700 "$MQ_STATE"
|
|
399
|
+
|
|
400
|
+
# OpenWrt's /var is /tmp, i.e. RAM. The shipped configuration keeps the password file and the dynamic
|
|
401
|
+
# security state in /var/lib/mosquitto, which here would be gone at every reboot - taking every
|
|
402
|
+
# account with it - so both are redirected to ${MQ_STATE}.
|
|
403
|
+
SRC_CONF="${PKG_DIR}/extras/mosquitto.conf"; [ -f "$SRC_CONF" ] || die "no extras/mosquitto.conf in ${PKG_DIR}"
|
|
404
|
+
SRC_ACL="${PKG_DIR}/extras/aclfile"; [ -f "$SRC_ACL" ] || die "no extras/aclfile in ${PKG_DIR}"
|
|
405
|
+
|
|
406
|
+
# The main configuration must read conf.d, and must keep its persistence somewhere that survives a
|
|
407
|
+
# reboot: retained values are how the dashboard shows something the moment it opens.
|
|
408
|
+
if [ ! -f "$MQ_CONF" ]; then
|
|
409
|
+
printf 'user %s\npersistence true\npersistence_location %s/\ninclude_dir /etc/mosquitto/conf.d\n' "$MQ_USER" "$MQ_STATE" > "$MQ_CONF"
|
|
410
|
+
ok "wrote ${MQ_CONF}"
|
|
411
|
+
else
|
|
412
|
+
[ -f "${MQ_CONF}.pre-frugal" ] || cp -p "$MQ_CONF" "${MQ_CONF}.pre-frugal"
|
|
413
|
+
grep -qE '^[[:space:]]*include_dir' "$MQ_CONF" || { printf '\ninclude_dir /etc/mosquitto/conf.d\n' >> "$MQ_CONF"; ok "added include_dir to ${MQ_CONF}"; }
|
|
414
|
+
if grep -qE '^[[:space:]]*persistence[[:space:]]' "$MQ_CONF"; then
|
|
415
|
+
sed -i 's/^[[:space:]]*persistence[[:space:]].*/persistence true/' "$MQ_CONF"
|
|
416
|
+
else
|
|
417
|
+
printf 'persistence true\n' >> "$MQ_CONF"
|
|
418
|
+
fi
|
|
419
|
+
PLOC=$(awk '/^[[:space:]]*persistence_location[[:space:]]/ {print $2}' "$MQ_CONF" | tail -1)
|
|
420
|
+
case "$PLOC" in
|
|
421
|
+
""|/tmp*|/var/*|/run/*)
|
|
422
|
+
if [ -n "$PLOC" ]; then sed -i "s|^[[:space:]]*persistence_location[[:space:]].*|persistence_location ${MQ_STATE}/|" "$MQ_CONF"
|
|
423
|
+
else printf 'persistence_location %s/\n' "$MQ_STATE" >> "$MQ_CONF"; fi
|
|
424
|
+
ok "persistence moved from '${PLOC:-unset}' to ${MQ_STATE}/ so it survives a reboot (original saved as ${MQ_CONF}.pre-frugal)" ;;
|
|
425
|
+
*) skip "persistence already at ${PLOC}" ;;
|
|
426
|
+
esac
|
|
427
|
+
fi
|
|
428
|
+
|
|
429
|
+
# The ACL goes in first: the configuration names it, and the broker will not start without it
|
|
430
|
+
if [ ! -e /etc/mosquitto/aclfile ]; then
|
|
431
|
+
put 600 "$MQ_USER" "$MQ_GROUP" "$SRC_ACL" /etc/mosquitto/aclfile
|
|
432
|
+
ok "access control list installed"
|
|
433
|
+
else
|
|
434
|
+
T1=$(mktemp); T2=$(mktemp)
|
|
435
|
+
grep -vE '^[[:space:]]*(#|$)' "$SRC_ACL" > "$T1" || true
|
|
436
|
+
grep -vE '^[[:space:]]*(#|$)' /etc/mosquitto/aclfile > "$T2" || true
|
|
437
|
+
ACL_MISSING=$(grep -vxFf "$T2" "$T1" || true)
|
|
438
|
+
ACL_ADDED=$(grep -cE '^[[:space:]]*user[[:space:]]' /etc/mosquitto/aclfile || true)
|
|
439
|
+
rm -f "$T1" "$T2"
|
|
440
|
+
if [ -z "$ACL_MISSING" ]; then skip "/etc/mosquitto/aclfile already has every rule this release ships"
|
|
441
|
+
elif [ "${ACL_ADDED:-0}" = 0 ]; then
|
|
442
|
+
put 600 "$MQ_USER" "$MQ_GROUP" "$SRC_ACL" /etc/mosquitto/aclfile
|
|
443
|
+
ok "access control list brought up to date"
|
|
444
|
+
else
|
|
445
|
+
warn "/etc/mosquitto/aclfile is missing rules this release ships and holds ${ACL_ADDED} added by hand, which replacing it would delete. Add these yourself:"
|
|
446
|
+
echo "$ACL_MISSING" | sed 's/^/ /'
|
|
447
|
+
fi
|
|
448
|
+
fi
|
|
449
|
+
|
|
450
|
+
# The dynamic security plugin, if this mosquitto has one
|
|
451
|
+
DYNSEC_SO=""
|
|
452
|
+
for cand in /usr/lib/mosquitto/mosquitto_dynamic_security.so /usr/lib/mosquitto_dynamic_security.so \
|
|
453
|
+
/usr/lib/*/mosquitto_dynamic_security.so /usr/lib/*/mosquitto/mosquitto_dynamic_security.so; do
|
|
454
|
+
[ -f "$cand" ] && { DYNSEC_SO=$cand; break; }
|
|
455
|
+
done
|
|
456
|
+
DYNSEC_JSON="${MQ_STATE}/dynamic-security.json"
|
|
457
|
+
SECRETS="${INSTALL_DIR}/config.d/secrets.yaml"
|
|
458
|
+
if [ -z "$DYNSEC_SO" ]; then
|
|
459
|
+
warn "no mosquitto dynamic security plugin on this router. The broker will run on the organization's"
|
|
460
|
+
warn " shared password instead: every node and browser uses the same credential. It works, but it"
|
|
461
|
+
warn " is the older and weaker scheme. Tell whoever sent you this - it matters."
|
|
462
|
+
elif ! command -v mosquitto_ctrl >/dev/null 2>&1; then
|
|
463
|
+
warn "the plugin is there (${DYNSEC_SO}) but there is no mosquitto_ctrl to initialise it - carrying on without it"
|
|
464
|
+
DYNSEC_SO=""
|
|
465
|
+
elif [ -f "$DYNSEC_JSON" ]; then
|
|
466
|
+
if grep -q '^dynsec_admin_password:' "$SECRETS" 2>/dev/null; then skip "dynamic security already initialised"
|
|
467
|
+
else
|
|
468
|
+
warn "${DYNSEC_JSON} exists but ${SECRETS} has no dynsec_admin_password - a previous run stopped between the two."
|
|
469
|
+
warn " Remove ${DYNSEC_JSON} and run this again."
|
|
470
|
+
DYNSEC_SO=""
|
|
471
|
+
fi
|
|
472
|
+
else
|
|
473
|
+
DYNSEC_PW=$(randpw)
|
|
474
|
+
if printf '%s\n%s\n' "$DYNSEC_PW" "$DYNSEC_PW" | mosquitto_ctrl dynsec init "$DYNSEC_JSON" frugal-admin >/dev/null 2>&1 && [ -s "$DYNSEC_JSON" ]; then
|
|
475
|
+
chown "$MQ_USER:$MQ_GROUP" "$DYNSEC_JSON"; chmod 600 "$DYNSEC_JSON"
|
|
476
|
+
{
|
|
477
|
+
echo ""
|
|
478
|
+
echo "# The broker account the server uses to create and remove other accounts, over"
|
|
479
|
+
echo "# \$CONTROL/dynamic-security/v1. Created by install-openwrt.sh."
|
|
480
|
+
echo "dynsec_admin_user: \"frugal-admin\""
|
|
481
|
+
echo "dynsec_admin_password: \"${DYNSEC_PW}\""
|
|
482
|
+
} >> "$SECRETS"
|
|
483
|
+
chmod 600 "$SECRETS"; [ "$RUN_USER" = root ] || chown "$RUN_USER" "$SECRETS"
|
|
484
|
+
ok "dynamic security initialised; admin credential saved to config.d/secrets.yaml"
|
|
485
|
+
else
|
|
486
|
+
warn "mosquitto_ctrl dynsec init failed - continuing without it"
|
|
487
|
+
rm -f "$DYNSEC_JSON"; DYNSEC_SO=""
|
|
488
|
+
fi
|
|
489
|
+
fi
|
|
490
|
+
|
|
491
|
+
# The shipped configuration, with the plugin path and the state directory filled in
|
|
492
|
+
TMP_CONF=$(mktemp)
|
|
493
|
+
if [ -n "$DYNSEC_SO" ]; then
|
|
494
|
+
sed -e "s|^plugin PLUGIN_PATH_SET_BY_INSTALLER|plugin ${DYNSEC_SO}|" \
|
|
495
|
+
-e "s|/var/lib/mosquitto/|${MQ_STATE}/|g" "$SRC_CONF" > "$TMP_CONF"
|
|
496
|
+
else
|
|
497
|
+
sed -e "s|^plugin PLUGIN_PATH_SET_BY_INSTALLER|#plugin (not installed)|" \
|
|
498
|
+
-e "s|^plugin_opt_config_file|#plugin_opt_config_file|" \
|
|
499
|
+
-e "s|/var/lib/mosquitto/|${MQ_STATE}/|g" "$SRC_CONF" > "$TMP_CONF"
|
|
500
|
+
fi
|
|
501
|
+
if cmp -s "$TMP_CONF" /etc/mosquitto/conf.d/frugal-iot.conf 2>/dev/null; then
|
|
502
|
+
skip "/etc/mosquitto/conf.d/frugal-iot.conf already current"
|
|
503
|
+
else
|
|
504
|
+
put 644 root root "$TMP_CONF" /etc/mosquitto/conf.d/frugal-iot.conf
|
|
505
|
+
ok "configuration installed at /etc/mosquitto/conf.d/frugal-iot.conf"
|
|
506
|
+
fi
|
|
507
|
+
rm -f "$TMP_CONF"
|
|
508
|
+
|
|
509
|
+
PWFILE=$(grep -hE '^[[:space:]]*password_file[[:space:]]' /etc/mosquitto/conf.d/frugal-iot.conf | tail -1 | awk '{print $2}' || true)
|
|
510
|
+
[ -n "$PWFILE" ] || die "no password_file line in /etc/mosquitto/conf.d/frugal-iot.conf"
|
|
511
|
+
if [ -f "$PWFILE" ]; then skip "password file ${PWFILE} exists"
|
|
512
|
+
else put 600 "$MQ_USER" "$MQ_GROUP" /dev/null "$PWFILE"; ok "empty password file created at ${PWFILE}"; fi
|
|
513
|
+
|
|
514
|
+
# Everything the broker opens after dropping privileges must be its own. mosquitto_passwd run as
|
|
515
|
+
# root rewrites the password file as root, and the broker then cannot read it - so this is repeated
|
|
516
|
+
# after each step below that touches it.
|
|
517
|
+
fix_broker_files() {
|
|
518
|
+
[ -e "$PWFILE" ] && { chown "$MQ_USER:$MQ_GROUP" "$PWFILE"; chmod 600 "$PWFILE"; }
|
|
519
|
+
[ -e "$DYNSEC_JSON" ] && { chown "$MQ_USER:$MQ_GROUP" "$DYNSEC_JSON"; chmod 600 "$DYNSEC_JSON"; }
|
|
520
|
+
[ -e /etc/mosquitto/aclfile ] && { chown "$MQ_USER:$MQ_GROUP" /etc/mosquitto/aclfile; chmod 600 /etc/mosquitto/aclfile; }
|
|
521
|
+
return 0
|
|
522
|
+
}
|
|
523
|
+
broker_listening() { netstat -ln 2>/dev/null | grep -q "[:.]$1 "; }
|
|
524
|
+
restart_broker() {
|
|
525
|
+
fix_broker_files
|
|
526
|
+
/etc/init.d/mosquitto enable
|
|
527
|
+
/etc/init.d/mosquitto restart
|
|
528
|
+
sleep 3
|
|
529
|
+
if ! pidof mosquitto >/dev/null 2>&1; then
|
|
530
|
+
echo "Mosquitto did not start. What it logged:" >&2
|
|
531
|
+
logread -e mosquitto 2>/dev/null | tail -15 >&2 || true
|
|
532
|
+
die "the broker is not running (see above)"
|
|
533
|
+
fi
|
|
534
|
+
broker_listening 1883 || die "the broker is running but not listening on 1883"
|
|
535
|
+
broker_listening 9012 || die "the broker is running but not listening on 9012 - its websockets listener. This mosquitto build probably has no websockets support, and the browser dashboard cannot work without it."
|
|
536
|
+
}
|
|
537
|
+
restart_broker
|
|
538
|
+
ok "broker running, listening on 1883 and 9012"
|
|
539
|
+
|
|
540
|
+
# ---------------------------------------------------------------- accounts
|
|
541
|
+
|
|
542
|
+
step "Creating the administrator login"
|
|
543
|
+
if in_dir "sqlite3 frugal-iot.db \"SELECT 1 FROM users WHERE username='superuser' AND hashed_password IS NOT NULL;\"" | grep -q 1; then
|
|
544
|
+
skip "superuser already has a password"
|
|
545
|
+
SUPER_PW="(unchanged - set by an earlier run)" # not the one generated above, which was never used
|
|
546
|
+
else
|
|
547
|
+
in_dir "npx --no frugal-iot-setpassword superuser '${SUPER_PW}'" >/dev/null
|
|
548
|
+
ok "superuser password set"
|
|
549
|
+
fi
|
|
550
|
+
|
|
551
|
+
step "Creating the organization"
|
|
552
|
+
if [ -f "${INSTALL_DIR}/config.d/organizations/${ORG_ID}.yaml" ]; then
|
|
553
|
+
skip "organization ${ORG_ID} already exists"
|
|
554
|
+
LOGIN_PW="(unchanged - set by an earlier run)"
|
|
555
|
+
BROKER_PW=$(sed -n 's/^mqtt_password:[[:space:]]*//p' "${INSTALL_DIR}/config.d/organizations/${ORG_ID}.yaml" | head -1 | tr -d "\"'" || true)
|
|
556
|
+
info "using the broker password already in its configuration"
|
|
557
|
+
else
|
|
558
|
+
in_dir "npx --no frugal-iot-addorganization '${ORG_ID}' '${ORG_NAME}' '${ORG_EMAIL}' '${ORG_PHONE}' '${BROKER_PW}'"
|
|
559
|
+
fix_broker_files
|
|
560
|
+
ok "organization ${ORG_ID} created"
|
|
561
|
+
# addorganization set the web login password to the broker password; they are for different things
|
|
562
|
+
in_dir "npx --no frugal-iot-setpassword '${ORG_ID}' '${LOGIN_PW}'" >/dev/null
|
|
563
|
+
ok "web login password for ${ORG_ID} set, different from the broker password"
|
|
564
|
+
fi
|
|
565
|
+
/etc/init.d/mosquitto restart # so it re-reads the password file
|
|
566
|
+
sleep 3
|
|
567
|
+
|
|
568
|
+
step "Creating the broker's roles and groups"
|
|
569
|
+
if [ -n "$DYNSEC_SO" ]; then
|
|
570
|
+
in_dir "npx --no frugal-iot-rebuild-dynsec"
|
|
571
|
+
ok "roles and groups built from the database"
|
|
572
|
+
else
|
|
573
|
+
skip "no dynamic security plugin, so there are no groups to build"
|
|
574
|
+
fi
|
|
575
|
+
|
|
576
|
+
step "Testing the broker end to end"
|
|
577
|
+
TEST_TOPIC="${ORG_ID}/installtest/hello"
|
|
578
|
+
SUB_OUT=$(mktemp)
|
|
579
|
+
mosquitto_sub -h localhost -u "$ORG_ID" -P "$BROKER_PW" -t "$TEST_TOPIC" -C 1 -W 10 > "$SUB_OUT" 2>&1 &
|
|
580
|
+
SUB_PID=$!
|
|
581
|
+
sleep 2
|
|
582
|
+
mosquitto_pub -h localhost -u "$ORG_ID" -P "$BROKER_PW" -t "$TEST_TOPIC" -m '42' || true
|
|
583
|
+
wait "$SUB_PID" 2>/dev/null || true
|
|
584
|
+
if grep -qx '42' "$SUB_OUT"; then
|
|
585
|
+
ok "published and received on ${TEST_TOPIC}"
|
|
586
|
+
else
|
|
587
|
+
echo "The broker did not deliver a test message. It said:" >&2
|
|
588
|
+
cat "$SUB_OUT" >&2
|
|
589
|
+
rm -f "$SUB_OUT"
|
|
590
|
+
die "broker test failed"
|
|
591
|
+
fi
|
|
592
|
+
rm -f "$SUB_OUT"
|
|
593
|
+
|
|
594
|
+
# ---------------------------------------------------------------- the service
|
|
595
|
+
|
|
596
|
+
step "Running the server as a service"
|
|
597
|
+
# procd, not systemd. It has no "working directory" setting, so the command changes directory itself,
|
|
598
|
+
# and "respawn 3600 10 0" restarts the server for ever, ten seconds after each exit. Output goes to
|
|
599
|
+
# the system log: read it with "logread -e frugaliot".
|
|
600
|
+
USER_LINE=""
|
|
601
|
+
[ "$RUN_USER" = root ] || USER_LINE=" procd_set_param user ${RUN_USER}"
|
|
602
|
+
TMP_INIT=$(mktemp)
|
|
603
|
+
cat > "$TMP_INIT" <<INIT
|
|
604
|
+
#!/bin/sh /etc/rc.common
|
|
605
|
+
# Written by install-openwrt.sh. The Frugal IoT server.
|
|
606
|
+
START=95
|
|
607
|
+
STOP=10
|
|
608
|
+
USE_PROCD=1
|
|
609
|
+
|
|
610
|
+
start_service() {
|
|
611
|
+
procd_open_instance
|
|
612
|
+
procd_set_param command /bin/sh -c "cd '${INSTALL_DIR}' && exec '${INSTALL_DIR}/node_modules/.bin/frugal-iot-server'"
|
|
613
|
+
${USER_LINE}
|
|
614
|
+
procd_set_param env HOME='${INSTALL_DIR}'
|
|
615
|
+
procd_set_param respawn 3600 10 0
|
|
616
|
+
procd_set_param stdout 1
|
|
617
|
+
procd_set_param stderr 1
|
|
618
|
+
procd_close_instance
|
|
619
|
+
}
|
|
620
|
+
INIT
|
|
621
|
+
if cmp -s "$TMP_INIT" /etc/init.d/frugaliot 2>/dev/null; then
|
|
622
|
+
skip "/etc/init.d/frugaliot already current"
|
|
623
|
+
else
|
|
624
|
+
put 755 root root "$TMP_INIT" /etc/init.d/frugaliot
|
|
625
|
+
ok "/etc/init.d/frugaliot installed, running as ${RUN_USER} from ${INSTALL_DIR}"
|
|
626
|
+
fi
|
|
627
|
+
rm -f "$TMP_INIT"
|
|
628
|
+
/etc/init.d/frugaliot enable
|
|
629
|
+
/etc/init.d/frugaliot restart
|
|
630
|
+
|
|
631
|
+
step "Checking the server answers"
|
|
632
|
+
PORT=$(sed -n 's/^port:[[:space:]]*//p' "${INSTALL_DIR}/config.d/server.yaml" 2>/dev/null | head -1 || true)
|
|
633
|
+
PORT=${PORT:-8080}
|
|
634
|
+
CODE=""; i=0
|
|
635
|
+
while [ "$i" -lt 20 ]; do
|
|
636
|
+
CODE=$(curl -s -o /dev/null -w '%{http_code}' -m 5 "http://localhost:${PORT}/" || true)
|
|
637
|
+
[ "$CODE" = 200 ] && break
|
|
638
|
+
sleep 2; i=$((i+1))
|
|
639
|
+
done
|
|
640
|
+
if [ "$CODE" = 200 ]; then
|
|
641
|
+
ok "http://localhost:${PORT}/ answered 200"
|
|
642
|
+
else
|
|
643
|
+
echo "The server is not answering on port ${PORT} (last response: ${CODE:-none}). What it logged:" >&2
|
|
644
|
+
logread -e frugaliot 2>/dev/null | tail -30 >&2 || true
|
|
645
|
+
if [ "$CODE" = 404 ]; then
|
|
646
|
+
echo "A 404 means the server is up but cannot find its web pages. Look at publicdir and htmldir in" >&2
|
|
647
|
+
echo "${INSTALL_DIR}/config.d/server.yaml: after a normal install they point into ./node_modules." >&2
|
|
648
|
+
echo "If publicdir is still './public', an earlier frugal-iot-init stopped part-way and its re-run" >&2
|
|
649
|
+
echo "left server.yaml alone. Set publicdir, privatedir, htmldir as in the packaged server.yaml." >&2
|
|
650
|
+
fi
|
|
651
|
+
die "server not answering"
|
|
652
|
+
fi
|
|
653
|
+
ls /etc/rc.d/ 2>/dev/null | grep -q 'frugaliot$' && ok "will start again at boot"
|
|
654
|
+
|
|
655
|
+
# ---------------------------------------------------------------- what is left to do
|
|
656
|
+
|
|
657
|
+
ENROL_SECRET=$(grep -A1 "^enrolment_${ORG_ID}:" "$SECRETS" 2>/dev/null | tail -1 | sed -e 's/^[[:space:]]*-[[:space:]]*//' -e 's/^"//' -e 's/"$//' || true)
|
|
658
|
+
|
|
659
|
+
shown() {
|
|
660
|
+
eval "shown_v=\${$1}"
|
|
661
|
+
case "$shown_v" in "("*) echo "$shown_v"; return ;; esac # not set by this run
|
|
662
|
+
case " $GENERATED " in *" $1 "*) echo "$shown_v" ;; *) echo "(the one you gave)" ;; esac
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
cat <<SUMMARY
|
|
666
|
+
|
|
667
|
+
=======================================================================
|
|
668
|
+
Frugal IoT is installed and running.
|
|
669
|
+
=======================================================================
|
|
670
|
+
|
|
671
|
+
Dashboard http://${BROKER_HOST}:${PORT}/
|
|
672
|
+
|
|
673
|
+
Logins superuser / $(shown SUPER_PW)
|
|
674
|
+
${ORG_ID} / $(shown LOGIN_PW)
|
|
675
|
+
(Change either with: cd ${INSTALL_DIR} && npx --no frugal-iot-setpassword <login> "<password>")
|
|
676
|
+
|
|
677
|
+
Broker ws://${BROKER_HOST}:9012 for browsers
|
|
678
|
+
${BROKER_HOST}:1883 for sensor nodes
|
|
679
|
+
|
|
680
|
+
Installed in ${INSTALL_DIR}, running as ${RUN_USER}
|
|
681
|
+
Log of this run ${LOG} (readable only by root; any generated password above is in it)
|
|
682
|
+
|
|
683
|
+
Still to do:
|
|
684
|
+
|
|
685
|
+
1. A project. Create one on the dashboard before flashing any node: a node says which project
|
|
686
|
+
it is in, and enrolling into one this server does not know is refused.
|
|
687
|
+
|
|
688
|
+
2. Your sensor nodes. In the PlatformIO project's platformio-secrets-local.ini:
|
|
689
|
+
|
|
690
|
+
build_flags_local =
|
|
691
|
+
'-D SYSTEM_FRUGAL_ORG="${ORG_ID}"'
|
|
692
|
+
'-D SYSTEM_FRUGAL_PROJECT="<the project from 1>"'
|
|
693
|
+
'-D SYSTEM_MQTT_HOST="${BROKER_HOST}"'
|
|
694
|
+
'-D SYSTEM_MQTT_ENROL_URL="http://${BROKER_HOST}:${PORT}/enrol"'
|
|
695
|
+
'-D SYSTEM_MQTT_ENROL_SECRET="${ENROL_SECRET:-see the dashboard, gear icon -> Nodes}"'
|
|
696
|
+
|
|
697
|
+
http and not https, because this router has no certificate yet, so no over-the-air updates.
|
|
698
|
+
|
|
699
|
+
3. The firewall. By default OpenWrt accepts connections from the LAN and refuses them from the
|
|
700
|
+
WAN, which is what you want: do NOT forward or open ports 1883, 9012 or ${PORT} to the internet.
|
|
701
|
+
|
|
702
|
+
Useful:
|
|
703
|
+
logread -e frugaliot the server's log
|
|
704
|
+
logread -e mosquitto the broker's log
|
|
705
|
+
service frugaliot restart
|
|
706
|
+
cd ${INSTALL_DIR} && npx --no frugal-iot-diagnostic (written for systemd; parts of it will
|
|
707
|
+
report things that do not apply here)
|
|
708
|
+
|
|
709
|
+
SUMMARY
|