Add D-APRS Data Gateway migration helper

master
Simon 2 weeks ago
parent 7f6ddddf54
commit 4e7dd22630

@ -0,0 +1,452 @@
#!/bin/bash
# Migrate a recognised legacy D-APRS Docker deployment to FreeDMR Data Gateway.
set -eu
CONFIG_DIR=/etc/freedmr
DRY_RUN=0
ASSUME_YES=0
usage()
{
cat <<'USAGE'
Usage: upgrade-daprs-to-data-gateway.sh [--config-dir DIR] [--dry-run] [--yes]
Migrates a recognised legacy D-APRS service in docker-compose.yml to the native
FreeDMR Data Gateway and enables its FBP v5 relationship in freedmr.cfg.
Environment values may be supplied instead of answering prompts:
HDSTACK worker count, 1 through 5
HDSTACK_BASEID main/aggregator server ID
FREEDMR_GATEWAY_APRS_LOGIN_CALL APRS-IS login callsign
FREEDMR_GATEWAY_APRS_PASSCODE APRS-IS passcode
FREEDMR_GATEWAY_FBP_KEY private FBP key, at most 20 characters
USAGE
}
fail()
{
echo "Upgrade aborted: $*" >&2
exit 2
}
prompt_value()
{
prompt_text="$1"
default_value="$2"
if [ ! -r /dev/tty ]
then
fail "interactive input is required; supply the documented environment values"
fi
if [ -n "$default_value" ]
then
printf '%s [%s]: ' "$prompt_text" "$default_value" >/dev/tty
else
printf '%s: ' "$prompt_text" >/dev/tty
fi
read -r entered_value </dev/tty
if [ -n "$entered_value" ]
then
printf '%s' "$entered_value"
else
printf '%s' "$default_value"
fi
}
is_unsigned_integer()
{
case "$1" in
''|*[!0-9]*) return 1 ;;
*) return 0 ;;
esac
}
config_value()
{
section_name="$1"
option_name="$2"
awk -v wanted_section="$section_name" -v wanted_option="$option_name" '
$0 == "[" wanted_section "]" { in_section = 1; next }
in_section && /^\[/ { exit }
in_section {
split($0, parts, ":")
option = parts[1]
gsub(/^[[:space:]]+|[[:space:]]+$/, "", option)
if (option == wanted_option) {
sub(/^[^:]*:[[:space:]]*/, "", $0)
sub(/[[:space:]]*([#;].*)?$/, "", $0)
print $0
exit
}
}
' "$FREEDMR_CONFIG"
}
compose_environment_value()
{
wanted="$1"
awk -v wanted="$wanted" '
{
line = $0
sub(/^[[:space:]]*-[[:space:]]*/, "", line)
sub(/^[[:space:]]*/, "", line)
if (index(line, wanted "=") == 1) {
sub("^" wanted "=", "", line)
gsub(/^[\047\042]|[\047\042][[:space:]]*$/, "", line)
print line
exit
}
if (line ~ ("^" wanted ":[[:space:]]*")) {
sub("^" wanted ":[[:space:]]*", "", line)
gsub(/^[\047\042]|[\047\042][[:space:]]*$/, "", line)
print line
exit
}
}
' "$COMPOSE_FILE"
}
build_relationships()
{
count="$1"
base_id="$2"
key="$3"
relationships='['
number=1
while [ "$number" -le "$count" ]
do
if [ "$count" -eq 1 ]
then
server_id="$base_id"
else
server_id=$((base_id + number))
fi
bind_port=$((62030 + number))
peer_port=$((62040 + number))
if [ "$number" -gt 1 ]
then
relationships="${relationships},"
fi
relationships="${relationships}{\"link_id\":\"hdstack-${number}\",\"bind_host\":\"0.0.0.0\",\"bind_port\":${bind_port},\"peer_host\":\"freedmr\",\"peer_port\":${peer_port},\"peer_network_id\":${server_id},\"direct_source_server_id\":${server_id},\"key\":\"${key}\"}"
number=$((number + 1))
done
printf '%s]' "$relationships"
}
while [ "$#" -gt 0 ]
do
case "$1" in
--config-dir)
[ "$#" -ge 2 ] || fail "--config-dir requires a path"
CONFIG_DIR="$2"
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
--yes)
ASSUME_YES=1
shift
;;
--help|-h)
usage
exit 0
;;
*)
usage >&2
fail "unknown argument: $1"
;;
esac
done
FREEDMR_CONFIG="$CONFIG_DIR/freedmr.cfg"
COMPOSE_FILE="$CONFIG_DIR/docker-compose.yml"
[ -f "$FREEDMR_CONFIG" ] || fail "$FREEDMR_CONFIG was not found"
[ -f "$COMPOSE_FILE" ] || fail "$COMPOSE_FILE was not found"
[ -r "$FREEDMR_CONFIG" ] || fail "$FREEDMR_CONFIG is not readable"
[ -r "$COMPOSE_FILE" ] || fail "$COMPOSE_FILE is not readable"
[ -w "$FREEDMR_CONFIG" ] || fail "$FREEDMR_CONFIG is not writable"
[ -w "$COMPOSE_FILE" ] || fail "$COMPOSE_FILE is not writable"
grep -q '^\[GLOBAL\][[:space:]]*$' "$FREEDMR_CONFIG" || \
fail "$FREEDMR_CONFIG has no [GLOBAL] section"
grep -q '^services:[[:space:]]*$' "$COMPOSE_FILE" || \
fail "$COMPOSE_FILE has no top-level services mapping"
grep -q '^ freedmr:[[:space:]]*$' "$COMPOSE_FILE" || \
fail "$COMPOSE_FILE has no recognised freedmr service"
legacy_count=$(grep -Ec '^ D-APRS:[[:space:]]*$' "$COMPOSE_FILE" || true)
native_count=$(grep -Ec '^ data-gateway:[[:space:]]*$' "$COMPOSE_FILE" || true)
[ "$native_count" -eq 0 ] || \
fail "a native data-gateway service already exists; no files were changed"
[ "$legacy_count" -eq 1 ] || \
fail "expected exactly one legacy D-APRS service, found $legacy_count"
legacy_block=$(awk '
/^ D-APRS:[[:space:]]*$/ { active = 1 }
active && NR > 1 && (/^ [A-Za-z0-9_.-]+:[[:space:]]*$/ || /^[^[:space:]#][^:]*:[[:space:]]*$/) && $0 !~ /^ D-APRS:/ { exit }
active { print }
' "$COMPOSE_FILE")
printf '%s\n' "$legacy_block" | grep -q 'container_name:[[:space:]]*D-APRS' || \
fail "the D-APRS service is not the recognised legacy container shape"
printf '%s\n' "$legacy_block" | grep -q 'APRS_CALL' || \
fail "the D-APRS service has no recognised APRS_CALL setting"
printf '%s\n' "$legacy_block" | grep -q 'APRS_PASSCODE' || \
fail "the D-APRS service has no recognised APRS_PASSCODE setting"
HDSTACK_COUNT="${HDSTACK:-}"
if [ -z "$HDSTACK_COUNT" ]
then
HDSTACK_COUNT=$(compose_environment_value HDSTACK)
fi
if [ -z "$HDSTACK_COUNT" ]
then
HDSTACK_COUNT=$(prompt_value 'Number of active HDStack workers (1-5)' '1')
fi
if ! is_unsigned_integer "$HDSTACK_COUNT" || \
[ "$HDSTACK_COUNT" -lt 1 ] || [ "$HDSTACK_COUNT" -gt 5 ]
then
fail "HDSTACK must be an integer from 1 to 5"
fi
BASE_ID="${HDSTACK_BASEID:-}"
if [ -z "$BASE_ID" ]
then
BASE_ID=$(compose_environment_value HDSTACK_BASEID)
fi
if [ -z "$BASE_ID" ]
then
BASE_ID=$(config_value GLOBAL SERVER_ID)
fi
if ! is_unsigned_integer "$BASE_ID" || \
[ "$BASE_ID" -lt 1 ] || [ "$BASE_ID" -gt 4294967295 ]
then
fail "HDSTACK_BASEID or [GLOBAL] SERVER_ID must be a non-zero server ID"
fi
if [ $((BASE_ID + HDSTACK_COUNT)) -gt 4294967295 ]
then
fail "the base server ID plus worker IDs exceeds the supported range"
fi
APRS_CALL="${FREEDMR_GATEWAY_APRS_LOGIN_CALL:-}"
if [ -z "$APRS_CALL" ]
then
APRS_CALL=$(compose_environment_value APRS_CALL)
fi
if [ -z "$APRS_CALL" ]
then
APRS_CALL=$(prompt_value 'APRS-IS login callsign' '')
fi
case "$APRS_CALL" in
''|*[!A-Za-z0-9-]*) fail "APRS-IS login callsign contains invalid characters" ;;
esac
[ "${#APRS_CALL}" -le 9 ] || fail "APRS-IS login callsign must not exceed 9 characters"
APRS_PASSCODE="${FREEDMR_GATEWAY_APRS_PASSCODE:-}"
if [ -z "$APRS_PASSCODE" ]
then
APRS_PASSCODE=$(compose_environment_value APRS_PASSCODE)
fi
if [ -z "$APRS_PASSCODE" ]
then
APRS_PASSCODE=$(prompt_value 'APRS-IS passcode' '')
fi
is_unsigned_integer "$APRS_PASSCODE" || fail "APRS-IS passcode must be numeric"
FBP_KEY="${FREEDMR_GATEWAY_FBP_KEY:-}"
if [ -z "$FBP_KEY" ]
then
FBP_KEY=$(od -An -N10 -tx1 /dev/urandom | tr -d ' \n')
fi
case "$FBP_KEY" in
''|*[!A-Za-z0-9._-]*) fail "the Data Gateway FBP key contains invalid characters" ;;
esac
[ "${#FBP_KEY}" -le 20 ] || fail "the Data Gateway FBP key must not exceed 20 characters"
RELATIONSHIPS=$(build_relationships "$HDSTACK_COUNT" "$BASE_ID" "$FBP_KEY")
WORK_DIR=$(mktemp -d "$CONFIG_DIR/.data-gateway-upgrade.XXXXXX")
trap 'rm -rf "$WORK_DIR"' EXIT
PROPOSED_CONFIG="$WORK_DIR/freedmr.cfg"
PROPOSED_COMPOSE="$WORK_DIR/docker-compose.yml"
awk -v gateway_key="$FBP_KEY" '
function finish_section() {
if (section == "GLOBAL" && !global_value_seen)
print "DATA_GATEWAY: True"
if (section == "D-APRS" && !legacy_enabled_seen)
print "ENABLED: False"
}
function print_gateway() {
print "[DATA-GATEWAY]"
print "MODE: OPENBRIDGE"
print "ENABLED: True"
print "IP: 0.0.0.0"
print "PORT: 62041"
print "NETWORK_ID: 0"
print "PASSPHRASE: " gateway_key
print "TARGET_IP: freedmr-data-gateway"
print "TARGET_PORT: 62031"
print "USE_ACL: False"
print "SUB_ACL: PERMIT:ALL"
print "TGID_ACL: PERMIT:ALL"
print "RELAX_CHECKS: False"
print "ENHANCED_OBP: True"
print "PROTO_VER: 5"
gateway_seen = 1
}
/^\[[^]]+\][[:space:]]*$/ {
finish_section()
if ($0 ~ /^\[DATA-GATEWAY\]/) {
if (!gateway_seen)
print_gateway()
section = "DATA-GATEWAY-SKIP"
next
}
section = $0
gsub(/^\[|\][[:space:]]*$/, "", section)
global_value_seen = 0
legacy_enabled_seen = 0
print
next
}
section == "DATA-GATEWAY-SKIP" { next }
section == "GLOBAL" && /^[[:space:]]*DATA_GATEWAY[[:space:]]*:/ {
print "DATA_GATEWAY: True"
global_value_seen = 1
next
}
section == "D-APRS" && /^[[:space:]]*ENABLED[[:space:]]*:/ {
print "ENABLED: False"
legacy_enabled_seen = 1
next
}
{ print }
END {
finish_section()
if (!gateway_seen) {
print ""
print_gateway()
}
}
' "$FREEDMR_CONFIG" > "$PROPOSED_CONFIG"
awk -v relationships="$RELATIONSHIPS" -v aprs_call="$APRS_CALL" -v aprs_passcode="$APRS_PASSCODE" '
function print_gateway() {
print " data-gateway:"
print " container_name: freedmr-data-gateway"
print " depends_on:"
print " - freedmr"
print " image: \047gitlab.hacknix.net:5050/freedmr/freedmr-data-gateway:latest\047"
print " restart: \042unless-stopped\042"
print " read_only: true"
print " tmpfs:"
print " - /tmp:size=16m,mode=1777"
print " cap_drop:"
print " - ALL"
print " security_opt:"
print " - no-new-privileges:true"
print " environment:"
print " FREEDMR_GATEWAY_INGRESS_MODE: fbp"
print " FREEDMR_GATEWAY_FBP_RELATIONSHIPS: \047" relationships "\047"
print " FREEDMR_GATEWAY_RADIOID_PATH: /data/subscriber_ids.json"
print " FREEDMR_GATEWAY_APRS_HOST: rotate.aprs2.net"
print " FREEDMR_GATEWAY_APRS_PORT: \04714580\047"
print " FREEDMR_GATEWAY_APRS_LOGIN_CALL: \047" aprs_call "\047"
print " FREEDMR_GATEWAY_APRS_PASSCODE: \047" aprs_passcode "\047"
print " FREEDMR_GATEWAY_LOG_LEVEL: INFO"
print " volumes:"
print " - \047/etc/freedmr/json/:/data/:ro\047"
print " networks:"
print " app_net:"
print " ipv4_address: 172.16.238.40"
print ""
}
{
if (skip_legacy) {
if ($0 ~ /^ [A-Za-z0-9_.-]+:[[:space:]]*$/ ||
$0 ~ /^[^[:space:]#][^:]*:[[:space:]]*$/) {
skip_legacy = 0
} else {
next
}
}
if ($0 ~ /^ D-APRS:[[:space:]]*$/) {
print_gateway()
skip_legacy = 1
next
}
print
}
' "$COMPOSE_FILE" > "$PROPOSED_COMPOSE"
for required in \
'DATA_GATEWAY: True' \
'[DATA-GATEWAY]' \
'PROTO_VER: 5' \
'PASSPHRASE:'
do
grep -Fq "$required" "$PROPOSED_CONFIG" || fail "generated FreeDMR configuration is incomplete"
done
grep -q '^ data-gateway:[[:space:]]*$' "$PROPOSED_COMPOSE" || \
fail "generated Compose file has no data-gateway service"
if grep -q '^ D-APRS:[[:space:]]*$' "$PROPOSED_COMPOSE"
then
fail "generated Compose file still contains the legacy D-APRS service"
fi
if grep -q '__FREEDMR_GATEWAY_' "$PROPOSED_COMPOSE"
then
fail "generated Compose file contains an unreplaced gateway placeholder"
fi
if ! docker compose -f "$PROPOSED_COMPOSE" config --quiet >/dev/null
then
fail "Docker Compose rejected the generated configuration"
fi
if [ "$DRY_RUN" -eq 1 ]
then
echo "Dry run successful; no files were changed."
echo "Configuration: $FREEDMR_CONFIG"
echo "Compose file: $COMPOSE_FILE"
echo "Workers: $HDSTACK_COUNT"
echo "Base ID: $BASE_ID"
exit 0
fi
if [ "$ASSUME_YES" -ne 1 ]
then
answer=$(prompt_value 'Back up both files and apply this migration? (y/N)' 'N')
case "$answer" in
y|Y|yes|YES) ;;
*) fail "migration was not confirmed" ;;
esac
fi
stamp=$(date -u +%Y%m%dT%H%M%SZ)
CONFIG_BACKUP="$FREEDMR_CONFIG.before-data-gateway.$stamp"
COMPOSE_BACKUP="$COMPOSE_FILE.before-data-gateway.$stamp"
[ ! -e "$CONFIG_BACKUP" ] || fail "backup already exists: $CONFIG_BACKUP"
[ ! -e "$COMPOSE_BACKUP" ] || fail "backup already exists: $COMPOSE_BACKUP"
cp -p "$FREEDMR_CONFIG" "$CONFIG_BACKUP"
cp -p "$COMPOSE_FILE" "$COMPOSE_BACKUP"
chmod --reference="$FREEDMR_CONFIG" "$PROPOSED_CONFIG"
chmod --reference="$COMPOSE_FILE" "$PROPOSED_COMPOSE"
if [ "$(id -u)" -eq 0 ]
then
chown --reference="$FREEDMR_CONFIG" "$PROPOSED_CONFIG"
chown --reference="$COMPOSE_FILE" "$PROPOSED_COMPOSE"
fi
mv "$PROPOSED_CONFIG" "$FREEDMR_CONFIG"
mv "$PROPOSED_COMPOSE" "$COMPOSE_FILE"
echo "FreeDMR Data Gateway migration complete."
echo "Backups:"
echo " $CONFIG_BACKUP"
echo " $COMPOSE_BACKUP"
echo "Review the files, then run:"
echo " cd $CONFIG_DIR"
echo " docker compose pull freedmr data-gateway"
echo " docker compose up -d --force-recreate freedmr data-gateway"

@ -176,6 +176,35 @@ peer IDs and keys, then removing or disabling the local Compose service. The
gateway endpoint must be reachable from the FreeDMR container; `localhost`
would address the FreeDMR container itself, not a separate gateway container.
### Migrating the legacy D-APRS container
`upgrade-daprs-to-data-gateway.sh` migrates the documented legacy `D-APRS`
Compose service to the native Data Gateway without replacing unrelated
FreeDMR configuration or Compose services. Download it, inspect it, and run a
dry run first:
```sh
curl -fsSL \
https://gitlab.hacknix.net/hacknix/FreeDMR/-/raw/master/docker-configs/upgrade-daprs-to-data-gateway.sh \
-o /tmp/upgrade-daprs-to-data-gateway.sh
chmod 755 /tmp/upgrade-daprs-to-data-gateway.sh
sudo /tmp/upgrade-daprs-to-data-gateway.sh --dry-run
sudo /tmp/upgrade-daprs-to-data-gateway.sh
```
The script reuses the legacy `APRS_CALL` and `APRS_PASSCODE`, prompts for any
missing values, generates a private FBP key, enables the FreeDMR
`[DATA-GATEWAY]` relationship, disables the old `[D-APRS]` listener, and
replaces only the recognised legacy Compose service. If `HDSTACK` is not set
in the Compose file, it asks for the active worker count and defaults to one.
Before changing either file, the script validates the proposed Compose
configuration and asks for confirmation. It creates timestamped backups beside
`/etc/freedmr/freedmr.cfg` and `/etc/freedmr/docker-compose.yml`. It refuses an
unrecognised layout or an existing native `data-gateway` service rather than
guessing. The script does not pull images or restart containers; follow the
commands it prints after reviewing the generated files.
## Session assignment
The proxy rotates new DMR IDs through the configured worker ranges. With two

@ -51,6 +51,10 @@ The installer requests the APRS-IS login and passcode and writes the matching
worker relationships. The gateway remains a separate container and is not a
fallback when its native links are unavailable.
The separately downloadable `upgrade-daprs-to-data-gateway.sh` helper can
migrate the documented legacy D-APRS Compose service while preserving and
backing up the surrounding FreeDMR configuration.
## Other operator-visible changes
- Network direct dial is available and defaults to local TG9 on TS1. Set

@ -0,0 +1,272 @@
import configparser
import json
import os
import re
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "docker-configs" / "upgrade-daprs-to-data-gateway.sh"
class DataGatewayUpgradeTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(dir=ROOT / "tests")
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
self.config_dir = self.root / "freedmr"
self.config_dir.mkdir()
self.config = self.config_dir / "freedmr.cfg"
self.compose = self.config_dir / "docker-compose.yml"
self.config.write_text(CONFIG, encoding="utf-8")
self.compose.write_text(COMPOSE, encoding="utf-8")
bin_dir = self.root / "bin"
bin_dir.mkdir()
docker = bin_dir / "docker"
docker.write_text(
"#!/bin/sh\n"
"[ \"$1\" = compose ] || exit 1\n"
"exit 0\n",
encoding="utf-8",
)
docker.chmod(0o755)
self.environment = os.environ.copy()
self.environment.update(
{
"PATH": str(bin_dir) + os.pathsep + self.environment["PATH"],
"HDSTACK": "2",
"HDSTACK_BASEID": "23400",
"FREEDMR_GATEWAY_APRS_LOGIN_CALL": "M0ABC",
"FREEDMR_GATEWAY_APRS_PASSCODE": "12345",
"FREEDMR_GATEWAY_FBP_KEY": "private-key",
}
)
def run_upgrade(self, *arguments, environment=None):
return subprocess.run(
[str(SCRIPT), "--config-dir", str(self.config_dir), *arguments],
check=False,
capture_output=True,
text=True,
env=environment or self.environment,
)
def test_migrates_legacy_service_and_builds_hdstack_relationships(self):
result = self.run_upgrade("--yes")
self.assertEqual(result.returncode, 0, result.stderr)
parser = configparser.ConfigParser()
parser.read(self.config)
self.assertTrue(parser.getboolean("GLOBAL", "DATA_GATEWAY"))
self.assertTrue(parser.getboolean("DATA-GATEWAY", "ENABLED"))
self.assertEqual(parser.get("DATA-GATEWAY", "MODE"), "OPENBRIDGE")
self.assertEqual(parser.getint("DATA-GATEWAY", "PROTO_VER"), 5)
self.assertEqual(parser.get("DATA-GATEWAY", "PASSPHRASE"), "private-key")
self.assertEqual(
parser.get("DATA-GATEWAY", "TARGET_IP"),
"freedmr-data-gateway",
)
self.assertFalse(parser.getboolean("D-APRS", "ENABLED"))
compose = self.compose.read_text(encoding="utf-8")
self.assertNotIn(" D-APRS:\n", compose)
self.assertIn(" data-gateway:\n", compose)
self.assertIn(" freedmrmonitor2:\n", compose)
relationships = self._relationships(compose)
self.assertEqual([item["link_id"] for item in relationships], ["hdstack-1", "hdstack-2"])
self.assertEqual([item["bind_port"] for item in relationships], [62031, 62032])
self.assertEqual([item["peer_port"] for item in relationships], [62041, 62042])
self.assertEqual(
[item["peer_network_id"] for item in relationships],
[23401, 23402],
)
self.assertTrue(all(item["key"] == "private-key" for item in relationships))
self.assertEqual(len(list(self.config_dir.glob("freedmr.cfg.before-data-gateway.*"))), 1)
self.assertEqual(len(list(self.config_dir.glob("docker-compose.yml.before-data-gateway.*"))), 1)
def test_single_worker_uses_main_server_id(self):
environment = self.environment.copy()
environment["HDSTACK"] = "1"
result = self.run_upgrade("--yes", environment=environment)
self.assertEqual(result.returncode, 0, result.stderr)
relationships = self._relationships(self.compose.read_text(encoding="utf-8"))
self.assertEqual(len(relationships), 1)
self.assertEqual(relationships[0]["peer_network_id"], 23400)
self.assertEqual(relationships[0]["direct_source_server_id"], 23400)
def test_reuses_legacy_credentials_and_explicit_compose_hdstack(self):
self.compose.write_text(
COMPOSE.replace(
" image: freedmr:test\n",
" image: freedmr:test\n"
" environment:\n"
" - HDSTACK=3\n"
" - HDSTACK_BASEID=24500\n",
),
encoding="utf-8",
)
environment = self.environment.copy()
for name in (
"HDSTACK",
"HDSTACK_BASEID",
"FREEDMR_GATEWAY_APRS_LOGIN_CALL",
"FREEDMR_GATEWAY_APRS_PASSCODE",
):
environment.pop(name)
result = self.run_upgrade("--yes", environment=environment)
self.assertEqual(result.returncode, 0, result.stderr)
compose = self.compose.read_text(encoding="utf-8")
self.assertIn("FREEDMR_GATEWAY_APRS_LOGIN_CALL: 'M0OLD'", compose)
self.assertIn("FREEDMR_GATEWAY_APRS_PASSCODE: '54321'", compose)
relationships = self._relationships(compose)
self.assertEqual(len(relationships), 3)
self.assertEqual(
[item["peer_network_id"] for item in relationships],
[24501, 24502, 24503],
)
def test_replaces_existing_disabled_gateway_section_once(self):
self.config.write_text(CONFIG_WITH_GATEWAY, encoding="utf-8")
result = self.run_upgrade("--yes")
self.assertEqual(result.returncode, 0, result.stderr)
updated = self.config.read_text(encoding="utf-8")
self.assertEqual(updated.count("[DATA-GATEWAY]"), 1)
self.assertNotIn("PASSPHRASE: old-key", updated)
self.assertIn("PASSPHRASE: private-key", updated)
self.assertIn("[SYSTEM]\nMODE: MASTER", updated)
def test_dry_run_validates_without_modifying_or_backing_up(self):
original_config = self.config.read_bytes()
original_compose = self.compose.read_bytes()
result = self.run_upgrade("--dry-run")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("Dry run successful", result.stdout)
self.assertEqual(self.config.read_bytes(), original_config)
self.assertEqual(self.compose.read_bytes(), original_compose)
self.assertEqual(list(self.config_dir.glob("*.before-data-gateway.*")), [])
def test_refuses_existing_native_service_without_changes(self):
self.compose.write_text(
COMPOSE.replace(" D-APRS:\n", " data-gateway:\n"),
encoding="utf-8",
)
original_config = self.config.read_bytes()
original_compose = self.compose.read_bytes()
result = self.run_upgrade("--yes")
self.assertEqual(result.returncode, 2)
self.assertIn("already exists", result.stderr)
self.assertEqual(self.config.read_bytes(), original_config)
self.assertEqual(self.compose.read_bytes(), original_compose)
def test_refuses_unrecognised_legacy_service_without_changes(self):
self.compose.write_text(
COMPOSE.replace("container_name: D-APRS", "container_name: unrelated"),
encoding="utf-8",
)
original_config = self.config.read_bytes()
original_compose = self.compose.read_bytes()
result = self.run_upgrade("--yes")
self.assertEqual(result.returncode, 2)
self.assertIn("not the recognised legacy container shape", result.stderr)
self.assertEqual(self.config.read_bytes(), original_config)
self.assertEqual(self.compose.read_bytes(), original_compose)
@staticmethod
def _relationships(compose):
match = re.search(
r"FREEDMR_GATEWAY_FBP_RELATIONSHIPS: '([^']+)'",
compose,
)
if match is None:
raise AssertionError("relationship JSON was not found")
return json.loads(match.group(1))
CONFIG = """\
[GLOBAL]
SERVER_ID: 23400
DATA_GATEWAY: False
[D-APRS]
MODE: MASTER
ENABLED: True
PORT: 52555
PASSPHRASE:
[SYSTEM]
MODE: MASTER
ENABLED: True
PORT: 54000
"""
CONFIG_WITH_GATEWAY = """\
[GLOBAL]
SERVER_ID: 23400
DATA_GATEWAY: False
[DATA-GATEWAY]
MODE: OPENBRIDGE
ENABLED: False
PASSPHRASE: old-key
PROTO_VER: 5
[D-APRS]
MODE: MASTER
ENABLED: True
[SYSTEM]
MODE: MASTER
"""
COMPOSE = """\
version: '2.4'
services:
freedmr:
container_name: freedmr
image: freedmr:test
networks:
app_net:
ipv4_address: 172.16.238.10
D-APRS:
container_name: D-APRS
environment:
- APRS_CALL=M0OLD
- APRS_PASSCODE=54321
image: legacy-daprs:test
restart: "unless-stopped"
networks:
app_net:
ipv4_address: 172.16.238.40
freedmrmonitor2:
container_name: freedmrmonitor2
image: monitor:test
networks:
app_net:
ipv4_address: 172.16.238.20
networks:
app_net:
driver: bridge
"""
if __name__ == "__main__":
unittest.main()
Loading…
Cancel
Save

Powered by TurnKey Linux.