diff --git a/docker-configs/upgrade-daprs-to-data-gateway.sh b/docker-configs/upgrade-daprs-to-data-gateway.sh new file mode 100755 index 0000000..49c3203 --- /dev/null +++ b/docker-configs/upgrade-daprs-to-data-gateway.sh @@ -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 &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" diff --git a/docs/hdstack.md b/docs/hdstack.md index 5007d51..97cf1ce 100644 --- a/docs/hdstack.md +++ b/docs/hdstack.md @@ -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 diff --git a/docs/release-notes-v1.5.0.md b/docs/release-notes-v1.5.0.md index 24e8d00..342da7a 100644 --- a/docs/release-notes-v1.5.0.md +++ b/docs/release-notes-v1.5.0.md @@ -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 diff --git a/tests/test_data_gateway_upgrade.py b/tests/test_data_gateway_upgrade.py new file mode 100644 index 0000000..657a20e --- /dev/null +++ b/tests/test_data_gateway_upgrade.py @@ -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()