You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
314 lines
9.8 KiB
314 lines
9.8 KiB
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["PATH"] = (
|
|
str(bin_dir) + os.pathsep + self.environment["PATH"]
|
|
)
|
|
|
|
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=self.environment,
|
|
)
|
|
|
|
def test_migrates_using_values_from_config_and_compose(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", "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)
|
|
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["peer_network_id"] for item in relationships],
|
|
[23401, 23402],
|
|
)
|
|
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):
|
|
self.compose.write_text(
|
|
COMPOSE.replace("- HDSTACK=2", "- HDSTACK=1"),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
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), 1)
|
|
self.assertEqual(relationships[0]["peer_network_id"], 23400)
|
|
self.assertEqual(relationships[0]["direct_source_server_id"], 23400)
|
|
|
|
def test_reads_three_worker_base_id_from_compose(self):
|
|
self.compose.write_text(
|
|
COMPOSE.replace("- HDSTACK=2", "- HDSTACK=3").replace(
|
|
"- HDSTACK_BASEID=23400",
|
|
"- HDSTACK_BASEID=24500",
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
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), 3)
|
|
self.assertEqual(
|
|
[item["peer_network_id"] for item in relationships],
|
|
[24501, 24502, 24503],
|
|
)
|
|
|
|
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")
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
updated = self.config.read_text(encoding="utf-8")
|
|
self.assertEqual(updated.count("[DATA-GATEWAY]"), 1)
|
|
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()
|
|
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
|
|
PORT: 63041
|
|
PASSPHRASE: old-key
|
|
TARGET_PORT: 63031
|
|
PROTO_VER: 5
|
|
|
|
[D-APRS]
|
|
MODE: MASTER
|
|
ENABLED: True
|
|
|
|
[SYSTEM]
|
|
MODE: MASTER
|
|
"""
|
|
|
|
COMPOSE = """\
|
|
version: '2.4'
|
|
services:
|
|
freedmr:
|
|
container_name: freedmr
|
|
image: freedmr:test
|
|
environment:
|
|
- HDSTACK=2
|
|
- HDSTACK_BASEID=23400
|
|
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()
|