Read Data Gateway migration settings from deployment

master
Simon 2 weeks ago
parent 4e7dd22630
commit 0841d819e5

@ -15,12 +15,9 @@ 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
Existing server, HDStack and APRS values are read from freedmr.cfg and the
legacy D-APRS service in docker-compose.yml. A new private FBP key is generated
unless freedmr.cfg already contains a non-placeholder Data Gateway passphrase.
USAGE
}
@ -36,7 +33,8 @@ prompt_value()
default_value="$2"
if [ ! -r /dev/tty ]
then
fail "interactive input is required; supply the documented environment values"
fail \
"interactive confirmation is required; rerun with --yes for non-interactive use"
fi
if [ -n "$default_value" ]
then
@ -111,6 +109,8 @@ build_relationships()
count="$1"
base_id="$2"
key="$3"
bind_base_port="$4"
peer_base_port="$5"
relationships='['
number=1
while [ "$number" -le "$count" ]
@ -121,8 +121,8 @@ build_relationships()
else
server_id=$((base_id + number))
fi
bind_port=$((62030 + number))
peer_port=$((62040 + number))
bind_port=$((bind_base_port + number - 1))
peer_port=$((peer_base_port + number - 1))
if [ "$number" -gt 1 ]
then
relationships="${relationships},"
@ -196,14 +196,22 @@ printf '%s\n' "$legacy_block" | grep -q 'APRS_CALL' || \
printf '%s\n' "$legacy_block" | grep -q 'APRS_PASSCODE' || \
fail "the D-APRS service has no recognised APRS_PASSCODE setting"
HDSTACK_COUNT="${HDSTACK:-}"
HDSTACK_COUNT=$(compose_environment_value 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')
CPU_COUNT=$(getconf _NPROCESSORS_ONLN 2>/dev/null) || CPU_COUNT=2
if ! is_unsigned_integer "$CPU_COUNT" || [ "$CPU_COUNT" -lt 1 ]
then
CPU_COUNT=2
fi
HDSTACK_COUNT=$((CPU_COUNT - 1))
if [ "$HDSTACK_COUNT" -lt 1 ]
then
HDSTACK_COUNT=1
elif [ "$HDSTACK_COUNT" -gt 5 ]
then
HDSTACK_COUNT=5
fi
fi
if ! is_unsigned_integer "$HDSTACK_COUNT" || \
[ "$HDSTACK_COUNT" -lt 1 ] || [ "$HDSTACK_COUNT" -gt 5 ]
@ -211,11 +219,7 @@ 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
BASE_ID=$(compose_environment_value HDSTACK_BASEID)
if [ -z "$BASE_ID" ]
then
BASE_ID=$(config_value GLOBAL SERVER_ID)
@ -230,33 +234,20 @@ 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
APRS_CALL=$(compose_environment_value APRS_CALL)
[ -n "$APRS_CALL" ] || fail "the legacy D-APRS service has no usable APRS_CALL value"
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
APRS_PASSCODE=$(compose_environment_value APRS_PASSCODE)
[ -n "$APRS_PASSCODE" ] || \
fail "the legacy D-APRS service has no usable APRS_PASSCODE value"
is_unsigned_integer "$APRS_PASSCODE" || fail "APRS-IS passcode must be numeric"
FBP_KEY="${FREEDMR_GATEWAY_FBP_KEY:-}"
if [ -z "$FBP_KEY" ]
FBP_KEY=$(config_value DATA-GATEWAY PASSPHRASE)
if [ -z "$FBP_KEY" ] || [ "$FBP_KEY" = "change-this" ]
then
FBP_KEY=$(od -An -N10 -tx1 /dev/urandom | tr -d ' \n')
fi
@ -265,13 +256,34 @@ case "$FBP_KEY" in
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")
GATEWAY_PEER_PORT=$(config_value DATA-GATEWAY PORT)
[ -n "$GATEWAY_PEER_PORT" ] || GATEWAY_PEER_PORT=62041
GATEWAY_BIND_PORT=$(config_value DATA-GATEWAY TARGET_PORT)
[ -n "$GATEWAY_BIND_PORT" ] || GATEWAY_BIND_PORT=62031
for gateway_port in "$GATEWAY_PEER_PORT" "$GATEWAY_BIND_PORT"
do
if ! is_unsigned_integer "$gateway_port" || \
[ "$gateway_port" -lt 1 ] || [ "$gateway_port" -gt 65535 ]
then
fail "Data Gateway ports must be integers from 1 to 65535"
fi
if [ $((gateway_port + HDSTACK_COUNT - 1)) -gt 65535 ]
then
fail "the Data Gateway base port plus worker ports exceeds 65535"
fi
done
RELATIONSHIPS=$(build_relationships \
"$HDSTACK_COUNT" "$BASE_ID" "$FBP_KEY" \
"$GATEWAY_BIND_PORT" "$GATEWAY_PEER_PORT")
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" '
awk -v gateway_key="$FBP_KEY" \
-v gateway_port="$GATEWAY_PEER_PORT" \
-v gateway_target_port="$GATEWAY_BIND_PORT" '
function finish_section() {
if (section == "GLOBAL" && !global_value_seen)
print "DATA_GATEWAY: True"
@ -283,11 +295,11 @@ awk -v gateway_key="$FBP_KEY" '
print "MODE: OPENBRIDGE"
print "ENABLED: True"
print "IP: 0.0.0.0"
print "PORT: 62041"
print "PORT: " gateway_port
print "NETWORK_ID: 0"
print "PASSPHRASE: " gateway_key
print "TARGET_IP: freedmr-data-gateway"
print "TARGET_PORT: 62031"
print "TARGET_PORT: " gateway_target_port
print "USE_ACL: False"
print "SUB_ACL: PERMIT:ALL"
print "TGID_ACL: PERMIT:ALL"

@ -192,11 +192,14 @@ 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.
The script reads the server ID and any existing Data Gateway ports and
non-placeholder passphrase from `freedmr.cfg`. It reads the HDStack settings
and legacy `APRS_CALL` and `APRS_PASSCODE` from `docker-compose.yml`. If
`HDSTACK` is
omitted, it applies the container's CPU-count-minus-one default, clamped to one
through five. A private FBP key is generated only when no usable one exists.
Missing or invalid existing values cause the migration to stop rather than
prompt for replacement configuration.
Before changing either file, the script validates the proposed Compose
configuration and asks for confirmation. It creates timestamped backups beside

@ -35,27 +35,20 @@ class DataGatewayUpgradeTests(unittest.TestCase):
)
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",
}
self.environment["PATH"] = (
str(bin_dir) + os.pathsep + self.environment["PATH"]
)
def run_upgrade(self, *arguments, environment=None):
def run_upgrade(self, *arguments):
return subprocess.run(
[str(SCRIPT), "--config-dir", str(self.config_dir), *arguments],
check=False,
capture_output=True,
text=True,
env=environment or self.environment,
env=self.environment,
)
def test_migrates_legacy_service_and_builds_hdstack_relationships(self):
def test_migrates_using_values_from_config_and_compose(self):
result = self.run_upgrade("--yes")
self.assertEqual(result.returncode, 0, result.stderr)
@ -65,7 +58,6 @@ class DataGatewayUpgradeTests(unittest.TestCase):
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",
@ -76,23 +68,51 @@ class DataGatewayUpgradeTests(unittest.TestCase):
self.assertNotIn(" D-APRS:\n", compose)
self.assertIn(" data-gateway:\n", compose)
self.assertIn(" freedmrmonitor2:\n", compose)
self.assertIn("FREEDMR_GATEWAY_APRS_LOGIN_CALL: 'M0OLD'", compose)
self.assertIn("FREEDMR_GATEWAY_APRS_PASSCODE: '54321'", 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["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)
generated_key = parser.get("DATA-GATEWAY", "PASSPHRASE")
self.assertRegex(generated_key, r"^[0-9a-f]{20}$")
self.assertTrue(all(item["key"] == generated_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"
self.compose.write_text(
COMPOSE.replace("- HDSTACK=2", "- HDSTACK=1"),
encoding="utf-8",
)
result = self.run_upgrade("--yes", environment=environment)
result = self.run_upgrade("--yes")
self.assertEqual(result.returncode, 0, result.stderr)
relationships = self._relationships(self.compose.read_text(encoding="utf-8"))
@ -100,40 +120,45 @@ class DataGatewayUpgradeTests(unittest.TestCase):
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):
def test_reads_three_worker_base_id_from_compose(self):
self.compose.write_text(
COMPOSE.replace(
" image: freedmr:test\n",
" image: freedmr:test\n"
" environment:\n"
" - HDSTACK=3\n"
" - HDSTACK_BASEID=24500\n",
COMPOSE.replace("- HDSTACK=2", "- HDSTACK=3").replace(
"- HDSTACK_BASEID=23400",
"- HDSTACK_BASEID=24500",
),
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)
result = self.run_upgrade("--yes")
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)
relationships = self._relationships(self.compose.read_text(encoding="utf-8"))
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):
def test_omitted_hdstack_uses_current_cpu_derived_default(self):
self.compose.write_text(
COMPOSE.replace(" - HDSTACK=2\n", ""),
encoding="utf-8",
)
cpu_count = int(
subprocess.check_output(
["getconf", "_NPROCESSORS_ONLN"],
text=True,
)
)
expected = max(1, min(5, cpu_count - 1))
result = self.run_upgrade("--yes")
self.assertEqual(result.returncode, 0, result.stderr)
relationships = self._relationships(self.compose.read_text(encoding="utf-8"))
self.assertEqual(len(relationships), expected)
def test_reuses_existing_non_placeholder_gateway_key(self):
self.config.write_text(CONFIG_WITH_GATEWAY, encoding="utf-8")
result = self.run_upgrade("--yes")
@ -141,9 +166,20 @@ class DataGatewayUpgradeTests(unittest.TestCase):
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("PASSPHRASE: old-key", updated)
self.assertIn("PORT: 63041", updated)
self.assertIn("TARGET_PORT: 63031", updated)
self.assertIn("[SYSTEM]\nMODE: MASTER", updated)
relationships = self._relationships(self.compose.read_text(encoding="utf-8"))
self.assertTrue(all(item["key"] == "old-key" for item in relationships))
self.assertEqual(
[item["bind_port"] for item in relationships],
[63031, 63032],
)
self.assertEqual(
[item["peer_port"] for item in relationships],
[63041, 63042],
)
def test_dry_run_validates_without_modifying_or_backing_up(self):
original_config = self.config.read_bytes()
@ -223,7 +259,9 @@ DATA_GATEWAY: False
[DATA-GATEWAY]
MODE: OPENBRIDGE
ENABLED: False
PORT: 63041
PASSPHRASE: old-key
TARGET_PORT: 63031
PROTO_VER: 5
[D-APRS]
@ -240,6 +278,9 @@ services:
freedmr:
container_name: freedmr
image: freedmr:test
environment:
- HDSTACK=2
- HDSTACK_BASEID=23400
networks:
app_net:
ipv4_address: 172.16.238.10

Loading…
Cancel
Save

Powered by TurnKey Linux.