Skip to content

Commit b35aa3f

Browse files
authored
Merge pull request #3 from airplanes-live/feat/mlat-user-schema-migration
Migrate legacy USER schema to MLAT_USER+MLAT_ENABLED on save
2 parents 9de5e4e + 1b0a679 commit b35aa3f

7 files changed

Lines changed: 587 additions & 17 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ jobs:
3030
bash -n update-webconfig.sh
3131
bash -n helpers/bg-update.sh
3232
bash -n helpers/update-channel.sh
33+
bash -n helpers/migrate-config.sh
34+
bash -n helpers/install-adsbconfig.sh
35+
bash -n helpers/restart-services.sh
3336
bash -n test/update-channel-test.sh
37+
bash -n test/migrate-config-test.sh
3438
3539
- name: Shellcheck changed scripts
3640
run: |
@@ -39,7 +43,14 @@ jobs:
3943
update-webconfig.sh \
4044
helpers/bg-update.sh \
4145
helpers/update-channel.sh \
42-
test/update-channel-test.sh
46+
helpers/migrate-config.sh \
47+
helpers/install-adsbconfig.sh \
48+
helpers/restart-services.sh \
49+
test/update-channel-test.sh \
50+
test/migrate-config-test.sh
4351
4452
- name: Run channel tests
4553
run: bash test/update-channel-test.sh
54+
55+
- name: Run migrate-config tests
56+
run: bash test/migrate-config-test.sh

helpers/install-adsbconfig.sh

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
#!/bin/bash
2-
3-
cp /tmp/webconfig/airplanes-config.txt /boot/airplanes-config.txt
4-
2+
# Installs the PHP-rendered airplanes-config.txt into /boot, applying the
3+
# MLAT_USER schema migration before publishing. Wrapped under flock so
4+
# restart-services.sh cannot read a half-migrated file. The temp file is
5+
# adjacent to /boot/airplanes-config.txt so readers never see a
6+
# legacy-only intermediate state at the canonical path.
7+
set -euo pipefail
8+
exec flock /var/lock/airplanes-config.lock env AIRPLANES_CONFIG_LOCK_HELD=1 bash -c '
9+
set -euo pipefail
10+
tmp="$(mktemp /boot/airplanes-config.txt.XXXXXX)"
11+
trap "rm -f \"$tmp\"" EXIT
12+
cp /tmp/webconfig/airplanes-config.txt "$tmp"
13+
/airplanes/webconfig/helpers/migrate-config.sh "$tmp"
14+
chmod --reference=/boot/airplanes-config.txt "$tmp" 2>/dev/null || chmod 0644 "$tmp"
15+
mv -f "$tmp" /boot/airplanes-config.txt
16+
trap - EXIT
17+
'

helpers/migrate-config.sh

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
#!/usr/bin/env bash
2+
# migrate-config.sh - Migrate legacy USER= schema in airplanes-config.txt
3+
# to the split MLAT_USER + MLAT_ENABLED schema expected by the new feed
4+
# daemons in airplanes-live/feed.
5+
#
6+
# Canonical home: airplanes-live/airplanes-webconfig:helpers/migrate-config.sh
7+
# Vendored byte-equivalent copy: airplanes-live/airplanes-update:
8+
# skeleton/usr/local/lib/airplanes-update/migrate-config.sh
9+
# Drift between the two is enforced by a CI test in airplanes-update.
10+
#
11+
# Usage:
12+
# migrate-config.sh <path> apply migration in-place
13+
# migrate-config.sh --version print MIGRATOR_VERSION and exit
14+
#
15+
# Behavior:
16+
# - Idempotent. Byte-compares the rendered output against the input;
17+
# only mv's if different (mtime unchanged on no-op).
18+
# - Never sources the file. Sourcing would execute user-supplied shell.
19+
# - Atomic via mktemp adjacent + mv.
20+
# - On first migration of a legacy file (USER present, MLAT_USER absent)
21+
# writes a `.pre-mlat-split` backup. Idempotent: never overwritten.
22+
# - When USER is present it is authoritative: any stale MLAT_USER /
23+
# MLAT_ENABLED lines are stripped and re-derived. The USER= line is
24+
# re-emitted in `USER="<escaped>"` form for shell-meta safety
25+
# (defense for hand-edited files; PHP webconfig sanitize already
26+
# restricts to [A-Za-z0-9_.-]).
27+
# - When USER is absent, the file is treated as already on the new
28+
# schema and not touched.
29+
# - Empty USER (USER="") is treated as "opted in, no name" and
30+
# produces MLAT_USER="Anonymous" / MLAT_ENABLED=true, matching the
31+
# defaults in feed/configure.sh and feed/scripts/apl-feed/mlat.sh.
32+
#
33+
# Env vars:
34+
# AIRPLANES_CONFIG_LOCK_HELD=1 Skip flock (caller already holds it).
35+
# Used by install-adsbconfig.sh wrapper.
36+
37+
set -euo pipefail
38+
39+
MIGRATOR_VERSION=1
40+
LOCK_FILE="/var/lock/airplanes-config.lock"
41+
42+
# Extract the last-wins value of KEY from FILE without sourcing it.
43+
#
44+
# Returns:
45+
# 0 KEY present (value printed to stdout; may be empty)
46+
# 1 KEY absent
47+
# 2 KEY present but malformed (e.g. unterminated quote)
48+
extract_key() {
49+
local file="$1" key="$2"
50+
local line raw found=0
51+
raw=""
52+
while IFS= read -r line || [[ -n "$line" ]]; do
53+
line="${line%$'\r'}"
54+
[[ -z "$line" || "${line:0:1}" == "#" ]] && continue
55+
case "$line" in
56+
"${key}="*)
57+
raw="${line#"${key}="}"
58+
found=1
59+
;;
60+
esac
61+
done < "$file"
62+
63+
if (( ! found )); then
64+
return 1
65+
fi
66+
67+
case "$raw" in
68+
'"'*)
69+
# Double-quoted: handle \\, \", \$, \` escapes; reject if
70+
# closing quote is missing.
71+
local result="" i len=${#raw} escaped=0 closed=0 c
72+
for (( i=1; i<len; i++ )); do
73+
c="${raw:$i:1}"
74+
if (( escaped )); then
75+
result+="$c"
76+
escaped=0
77+
elif [[ "$c" == '\' ]]; then
78+
escaped=1
79+
elif [[ "$c" == '"' ]]; then
80+
closed=1
81+
break
82+
else
83+
result+="$c"
84+
fi
85+
done
86+
if (( ! closed )); then
87+
printf 'migrate-config.sh: %s: malformed double-quoted value for %s=\n' "$file" "$key" >&2
88+
return 2
89+
fi
90+
printf '%s' "$result"
91+
;;
92+
"'"*)
93+
# Single-quoted: no escapes; reject if closing quote missing.
94+
local body="${raw#\'}"
95+
case "$body" in
96+
*"'"*)
97+
body="${body%%\'*}"
98+
printf '%s' "$body"
99+
;;
100+
*)
101+
printf 'migrate-config.sh: %s: malformed single-quoted value for %s=\n' "$file" "$key" >&2
102+
return 2
103+
;;
104+
esac
105+
;;
106+
*)
107+
# Unquoted: ends at first whitespace (trailing comments must
108+
# be space-separated per shell parsing).
109+
printf '%s' "${raw%%[[:space:]]*}"
110+
;;
111+
esac
112+
return 0
113+
}
114+
115+
# Escape a literal value for safe inclusion in `KEY="<value>"` form.
116+
# Order matters: backslash first so subsequent escapes don't double-up.
117+
escape_for_double_quoted() {
118+
local v="$1"
119+
v="${v//\\/\\\\}"
120+
v="${v//\$/\\\$}"
121+
v="${v//\`/\\\`}"
122+
v="${v//\"/\\\"}"
123+
printf '%s' "$v"
124+
}
125+
126+
# Decide MLAT_USER / MLAT_ENABLED from a USER value per migration rules.
127+
# Sets caller-scope variables MLAT_USER_OUT and MLAT_ENABLED_OUT.
128+
derive_mlat_from_user() {
129+
local user="$1"
130+
case "$user" in
131+
0|disable)
132+
MLAT_USER_OUT=""
133+
MLAT_ENABLED_OUT="false"
134+
;;
135+
'')
136+
# Empty USER → "Anonymous" so the daemon's strict-fail on
137+
# empty MLAT_USER + MLAT_ENABLED=true never fires. Mirrors
138+
# the writer-side defaults in feed/configure.sh and
139+
# feed/scripts/apl-feed/mlat.sh.
140+
MLAT_USER_OUT="Anonymous"
141+
MLAT_ENABLED_OUT="true"
142+
;;
143+
*)
144+
MLAT_USER_OUT="$user"
145+
MLAT_ENABLED_OUT="true"
146+
;;
147+
esac
148+
}
149+
150+
# Run the migration on $path. Caller is responsible for the lock.
151+
do_migrate() {
152+
local path="$1"
153+
local backup_path="${path}.pre-mlat-split"
154+
155+
local user_value="" user_present=0 mlat_user_present=0 rc
156+
157+
if user_value="$(extract_key "$path" USER)"; then
158+
user_present=1
159+
else
160+
rc=$?
161+
case "$rc" in
162+
1) user_present=0 ;;
163+
2) exit 2 ;;
164+
*) exit "$rc" ;;
165+
esac
166+
fi
167+
168+
if extract_key "$path" MLAT_USER >/dev/null; then
169+
mlat_user_present=1
170+
else
171+
rc=$?
172+
case "$rc" in
173+
1) mlat_user_present=0 ;;
174+
2) exit 2 ;;
175+
*) exit "$rc" ;;
176+
esac
177+
fi
178+
179+
# MLAT_ENABLED malformed is also a hard fail, even though we don't
180+
# use its value here (it gets re-derived).
181+
if extract_key "$path" MLAT_ENABLED >/dev/null; then
182+
:
183+
else
184+
rc=$?
185+
case "$rc" in
186+
1) : ;;
187+
2) exit 2 ;;
188+
*) exit "$rc" ;;
189+
esac
190+
fi
191+
192+
# USER absent → file is already on new schema (or has no schema).
193+
# Don't touch it. mtime stays unchanged.
194+
if (( ! user_present )); then
195+
return 0
196+
fi
197+
198+
# USER present: migrate. USER is authoritative; any stale MLAT_USER
199+
# / MLAT_ENABLED is stripped and re-derived.
200+
local MLAT_USER_OUT="" MLAT_ENABLED_OUT=""
201+
derive_mlat_from_user "$user_value"
202+
203+
local user_escaped mlat_user_escaped
204+
user_escaped="$(escape_for_double_quoted "$user_value")"
205+
mlat_user_escaped="$(escape_for_double_quoted "$MLAT_USER_OUT")"
206+
207+
# Backup once on first transition out of pure-legacy (USER present,
208+
# MLAT_USER absent). Never overwritten.
209+
if [[ ! -f "$backup_path" ]] && (( ! mlat_user_present )); then
210+
cp -fp "$path" "$backup_path"
211+
fi
212+
213+
# Render desired output to an adjacent temp file. The first USER=
214+
# line becomes the anchor: re-emitted in normalized form, immediately
215+
# followed by MLAT_USER and MLAT_ENABLED. Subsequent USER= lines and
216+
# any existing MLAT_USER= / MLAT_ENABLED= lines are dropped.
217+
local tmp
218+
tmp="$(mktemp "${path}.XXXXXX")"
219+
# shellcheck disable=SC2064
220+
trap "rm -f '$tmp'" EXIT
221+
222+
{
223+
local line trimmed user_seen=0
224+
while IFS= read -r line || [[ -n "$line" ]]; do
225+
trimmed="${line%$'\r'}"
226+
case "$trimmed" in
227+
'USER='*)
228+
if (( ! user_seen )); then
229+
printf 'USER="%s"\n' "$user_escaped"
230+
printf 'MLAT_USER="%s"\n' "$mlat_user_escaped"
231+
printf 'MLAT_ENABLED=%s\n' "$MLAT_ENABLED_OUT"
232+
user_seen=1
233+
fi
234+
;;
235+
'MLAT_USER='*|'MLAT_ENABLED='*)
236+
: # drop; will be re-emitted by the USER anchor
237+
;;
238+
*)
239+
printf '%s\n' "$line"
240+
;;
241+
esac
242+
done < "$path"
243+
} > "$tmp"
244+
245+
# Preserve mode and ownership. On vfat /boot ownership is meaningless,
246+
# so chown failure is tolerated; chmod falls back to 0644.
247+
chmod --reference="$path" "$tmp" 2>/dev/null || chmod 0644 "$tmp"
248+
chown --reference="$path" "$tmp" 2>/dev/null || true
249+
250+
# Byte-compare. No-op when output is identical to input so mtime is
251+
# preserved across idempotent re-runs.
252+
if cmp -s "$path" "$tmp"; then
253+
rm -f "$tmp"
254+
trap - EXIT
255+
return 0
256+
fi
257+
258+
mv -f "$tmp" "$path"
259+
trap - EXIT
260+
}
261+
262+
main() {
263+
if [[ "${1:-}" == "--version" ]]; then
264+
printf 'migrate-config.sh version=%d\n' "$MIGRATOR_VERSION"
265+
return 0
266+
fi
267+
268+
local path="${1:-}"
269+
if [[ -z "$path" ]]; then
270+
echo "usage: migrate-config.sh <path>" >&2
271+
return 2
272+
fi
273+
274+
if [[ ! -f "$path" ]]; then
275+
# Nothing to migrate. Don't fail — could be a fresh install
276+
# where airplanes-config.txt hasn't been created yet.
277+
return 0
278+
fi
279+
280+
if [[ "${AIRPLANES_CONFIG_LOCK_HELD:-}" == "1" ]]; then
281+
do_migrate "$path"
282+
return
283+
fi
284+
285+
mkdir -p "$(dirname "$LOCK_FILE")"
286+
exec 9>"$LOCK_FILE"
287+
flock 9
288+
do_migrate "$path"
289+
}
290+
291+
main "$@"

helpers/restart-services.sh

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
#!/bin/bash
2+
# Restarts services that consume /boot/airplanes-config.txt. Held under the
3+
# same flock as install-adsbconfig.sh so a restart can never fire while a
4+
# save is mid-cp-or-mid-migrate.
25

3-
restartIfEnabled() {
4-
# check if enabled
5-
if systemctl is-enabled "$1" &>/dev/null; then
6-
systemctl restart "$1"
7-
fi
8-
}
6+
set -euo pipefail
97

10-
systemctl restart webconfig
8+
exec flock /var/lock/airplanes-config.lock bash -c '
9+
restartIfEnabled() {
10+
if systemctl is-enabled "$1" &>/dev/null; then
11+
systemctl restart "$1"
12+
fi
13+
}
1114
12-
airplanes-first-run
15+
systemctl restart webconfig
1316
14-
services="readsb dump978-fa airplanes-978 airplanes-feed airplanes-mlat webconfig leds"
15-
for service in $services; do
16-
restartIfEnabled $service
17-
done
17+
airplanes-first-run
1818
19+
services="readsb dump978-fa airplanes-978 airplanes-feed airplanes-mlat webconfig leds"
20+
for service in $services; do
21+
restartIfEnabled "$service"
22+
done
23+
'

install.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,15 @@ if [[ "$1" != "dont_reset_config" ]]; then
7373
fi
7474
popd >/dev/null || exit
7575

76+
# One-time migration of /boot/airplanes-config.txt to the new MLAT_USER
77+
# schema. Idempotent (byte-compares before rewriting). Runs AFTER the
78+
# boot-configs/* copy above so it sees the freshly-placed template if
79+
# one was just dropped. Safe on dont_reset_config installs too: the
80+
# existing airplanes-config.txt is what needs migrating.
81+
if [[ -f /boot/airplanes-config.txt ]]; then
82+
/airplanes/webconfig/helpers/migrate-config.sh /boot/airplanes-config.txt
83+
fi
84+
7685
# We do not use hostapd. Setup network is open.
7786
systemctl disable hostapd &>/dev/null || true
7887
systemctl enable webconfig

0 commit comments

Comments
 (0)