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"
|
||||
@ -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…
Reference in new issue