import configparser import tempfile import unittest from pathlib import Path import config import hdstack_runtime ROOT = Path(__file__).resolve().parents[1] DOCKER_CONFIGS = ROOT / 'docker-configs' class HDStackRuntimeTests(unittest.TestCase): def setUp(self): self.temporary = tempfile.TemporaryDirectory() self.addCleanup(self.temporary.cleanup) self.root = Path(self.temporary.name) self.main_config = self.root / 'freedmr.cfg' self.bridge_config = self.root / 'freedmr-bridge.cfg' self.bridge_rules = self.root / 'rules.py' self.main_config.write_text(MAIN_CONFIG, encoding='utf-8') self.bridge_config.write_text(BRIDGE_CONFIG, encoding='utf-8') self.bridge_rules.write_text('BRIDGES = {}\n', encoding='utf-8') def generate(self, instances=2, base_id=23400, bridge_enabled=True): return hdstack_runtime.generate_hdstack( self.main_config, self.bridge_config, self.bridge_rules, self.root / 'generated', instances, base_id, bridge_enabled=bridge_enabled, ) def test_two_worker_layout_uses_base_id_plus_one_and_disjoint_ranges(self): layout = self.generate() aggregator = _read_config(layout.aggregator_config) workers = [_read_config(path) for path in layout.worker_configs] self.assertEqual(layout.backend_ranges, ((54000, 54099), (54100, 54199))) self.assertEqual(aggregator.getint('GLOBAL', 'SERVER_ID'), 23400) self.assertEqual( [worker.getint('GLOBAL', 'SERVER_ID') for worker in workers], [23401, 23402], ) self.assertEqual(aggregator.getint('GLOBAL', 'HDSTACK_INSTANCES'), 2) self.assertEqual( aggregator.getint('GLOBAL', 'HDSTACK_BASE_SERVER_ID'), 23400, ) self.assertTrue(all( worker.getint('GLOBAL', 'HDSTACK_INSTANCES') == 2 and worker.getint('GLOBAL', 'HDSTACK_BASE_SERVER_ID') == 23400 for worker in workers )) self.assertEqual( [worker.getint('SYSTEM', 'PORT') for worker in workers], [54000, 54100], ) self.assertEqual( [worker.getint('SYSTEM', 'GENERATOR') for worker in workers], [100, 100], ) def test_five_worker_layout_is_derived_without_hard_coded_second_worker(self): layout = self.generate(instances=5) self.assertEqual( layout.backend_ranges, ( (54000, 54099), (54100, 54199), (54200, 54299), (54300, 54399), (54400, 54499), ), ) self.assertEqual(len(layout.worker_configs), 5) self.assertEqual(layout.worker_report_ports, (4322, 4323, 4324, 4325, 4326)) def test_aggregator_has_only_external_and_internal_openbridge_systems(self): layout = self.generate() aggregator = _read_config(layout.aggregator_config) self.assertFalse(aggregator.getboolean('REPORTS', 'REPORT')) self.assertTrue(aggregator.getboolean('GLOBAL', 'GEN_STAT_BRIDGES')) self.assertNotIn('SYSTEM', aggregator.sections()) self.assertNotIn('ECHO', aggregator.sections()) self.assertIn('EXTERNAL-FBP', aggregator.sections()) self.assertEqual(aggregator.getint('EXTERNAL-FBP', 'PORT'), 62044) protocol_sections = [ section for section in aggregator.sections() if aggregator.has_option(section, 'MODE') ] self.assertTrue(protocol_sections) self.assertTrue(all(aggregator.get(section, 'MODE') == 'OPENBRIDGE' for section in protocol_sections)) def test_workers_copy_system_policy_and_only_add_aggregator_link(self): source = _read_config(self.main_config) layout = self.generate() workers = [_read_config(path) for path in layout.worker_configs] for number, worker in enumerate(workers, 1): expected = dict(source.items('SYSTEM')) expected['port'] = str(54000 + (number - 1) * 100) self.assertEqual(dict(worker.items('SYSTEM')), expected) self.assertNotIn('EXTERNAL-FBP', worker.sections()) self.assertEqual( [ section for section in worker.sections() if worker.has_option(section, 'MODE') ], ['SYSTEM', 'ECHO', 'OBP-HDSTACK-AGGREGATOR'], ) self.assertEqual( worker.getint('OBP-HDSTACK-AGGREGATOR', 'NETWORK_ID'), 23400, ) self.assertFalse(worker.getboolean('GLOBAL', 'ENABLE_API')) self.assertFalse(worker.getboolean('GLOBAL', 'DATA_GATEWAY')) def test_data_gateway_is_connected_directly_to_each_worker(self): self.main_config.write_text( MAIN_CONFIG + DATA_GATEWAY_CONFIG, encoding='utf-8', ) layout = self.generate() aggregator = _read_config(layout.aggregator_config) workers = [_read_config(path) for path in layout.worker_configs] self.assertFalse(aggregator.getboolean('GLOBAL', 'DATA_GATEWAY')) self.assertNotIn('DATA-GATEWAY', aggregator.sections()) for number, worker in enumerate(workers, 1): self.assertTrue(worker.getboolean('GLOBAL', 'DATA_GATEWAY')) self.assertIn('DATA-GATEWAY', worker.sections()) self.assertEqual( worker.getint('DATA-GATEWAY', 'PORT'), 62040 + number, ) self.assertEqual( worker.getint('DATA-GATEWAY', 'TARGET_PORT'), 62030 + number, ) self.assertEqual( worker.get('DATA-GATEWAY', 'TARGET_IP'), 'data-gateway', ) self.assertEqual(worker.getint('DATA-GATEWAY', 'PROTO_VER'), 5) self.assertTrue(worker.getboolean('DATA-GATEWAY', 'ENHANCED_OBP')) self.assertNotIn('EXTERNAL-FBP', worker.sections()) def test_data_gateway_requires_fbp5_fbcp_and_valid_derived_ports(self): invalid_profiles = ( DATA_GATEWAY_CONFIG.replace("MODE: OPENBRIDGE", "MODE: MASTER"), DATA_GATEWAY_CONFIG.replace("PROTO_VER: 5", "PROTO_VER: 1"), DATA_GATEWAY_CONFIG.replace("ENHANCED_OBP: True", "ENHANCED_OBP: False"), ) for data_gateway_config in invalid_profiles: with self.subTest(config=data_gateway_config): self.main_config.write_text( MAIN_CONFIG + data_gateway_config, encoding="utf-8", ) with self.assertRaises(hdstack_runtime.HDStackConfigurationError): self.generate() for data_gateway_config in ( DATA_GATEWAY_CONFIG.replace("PORT: 62041", "PORT: 54000"), DATA_GATEWAY_CONFIG.replace("TARGET_PORT: 62031", "TARGET_PORT: 65535"), ): with self.subTest(config=data_gateway_config): self.main_config.write_text( MAIN_CONFIG + data_gateway_config, encoding="utf-8", ) with self.assertRaises(hdstack_runtime.HDStackConfigurationError): self.generate() def test_internal_fbp_links_are_reciprocal_and_use_expected_ids(self): layout = self.generate() aggregator = _read_config(layout.aggregator_config) workers = [_read_config(path) for path in layout.worker_configs] for number, worker in enumerate(workers, 1): aggregator_link = 'OBP-HDSTACK-WORKER-{}'.format(number) self.assertEqual( ( aggregator.getint(aggregator_link, 'PORT'), aggregator.getint(aggregator_link, 'TARGET_PORT'), aggregator.getint(aggregator_link, 'NETWORK_ID'), ), (7100 + number, 7000 + number, 23400 + number), ) self.assertEqual( ( worker.getint('OBP-HDSTACK-AGGREGATOR', 'PORT'), worker.getint('OBP-HDSTACK-AGGREGATOR', 'TARGET_PORT'), worker.getint('OBP-HDSTACK-AGGREGATOR', 'NETWORK_ID'), ), (7000 + number, 7100 + number, 23400), ) self.assertEqual( aggregator.getint(aggregator_link, 'PROTO_VER'), 5, ) self.assertTrue( aggregator.getboolean(aggregator_link, 'ENHANCED_OBP') ) self.assertEqual(worker.getint('OBP-HDSTACK-AGGREGATOR', 'PROTO_VER'), 5) self.assertTrue( worker.getboolean('OBP-HDSTACK-AGGREGATOR', 'ENHANCED_OBP') ) self.assertFalse( aggregator.getboolean(aggregator_link, 'RELAX_CHECKS') ) self.assertFalse( worker.getboolean('OBP-HDSTACK-AGGREGATOR', 'RELAX_CHECKS') ) def test_bridge_link_is_reciprocal_fbp_v5_with_expected_ids(self): layout = self.generate() aggregator = _read_config(layout.aggregator_config) bridge = _read_config(layout.bridge_config) self.assertEqual( ( aggregator.getint('OBP-HDSTACK-BRIDGE', 'PORT'), aggregator.getint('OBP-HDSTACK-BRIDGE', 'TARGET_PORT'), aggregator.getint('OBP-HDSTACK-BRIDGE', 'NETWORK_ID'), ), (7200, 7201, 23500), ) self.assertEqual( ( bridge.getint('OBP-HDSTACK-AGGREGATOR', 'PORT'), bridge.getint('OBP-HDSTACK-AGGREGATOR', 'TARGET_PORT'), bridge.getint('OBP-HDSTACK-AGGREGATOR', 'NETWORK_ID'), ), (7201, 7200, 23400), ) for parser, section in ( (aggregator, 'OBP-HDSTACK-BRIDGE'), (bridge, 'OBP-HDSTACK-AGGREGATOR'), ): self.assertEqual(parser.get(section, 'MODE'), 'OPENBRIDGE') self.assertEqual(parser.getint(section, 'PROTO_VER'), 5) self.assertTrue(parser.getboolean(section, 'ENHANCED_OBP')) self.assertFalse(parser.getboolean(section, 'RELAX_CHECKS')) self.assertEqual(parser.get(section, 'PASSPHRASE'), 'internal') def test_each_worker_has_an_isolated_loro_peer(self): layout = self.generate(instances=3) workers = [_read_config(path) for path in layout.worker_configs] loros = [_read_config(path) for path in layout.loro_configs] self.assertEqual(len(loros), 3) for number, (worker, loro) in enumerate(zip(workers, loros), 1): expected_peer_port = 54916 + (number - 1) * 2 expected_master_port = 54915 + (number - 1) * 2 self.assertEqual(worker.get('ECHO', 'IP'), '127.0.0.1') self.assertEqual(worker.getint('ECHO', 'PORT'), expected_peer_port) self.assertEqual(worker.get('ECHO', 'MASTER_IP'), '127.0.0.1') self.assertEqual( worker.getint('ECHO', 'MASTER_PORT'), expected_master_port, ) self.assertEqual(loro.get('PARROT', 'IP'), '127.0.0.1') self.assertEqual(loro.getint('PARROT', 'PORT'), expected_master_port) self.assertEqual(loro.getint('PARROT', 'MAX_PEERS'), 1) self.assertEqual( loro.get('PARROT', 'PASSPHRASE'), worker.get('ECHO', 'PASSPHRASE'), ) self.assertFalse(loro.getboolean('REPORTS', 'REPORT')) self.assertEqual( loro.getint('REPORTS', 'REPORT_PORT'), 4821 + number - 1, ) self.assertEqual(loro.get('LOGGER', 'LOG_NAME'), 'ECHO-{}'.format(number)) def test_reporting_mux_uses_workers_only_and_preserves_client_acl(self): layout = self.generate() supervisor = layout.supervisor_config.read_text(encoding='utf-8') workers = [_read_config(path) for path in layout.worker_configs] self.assertIn('--listen-port 4321', supervisor) self.assertIn('--client 127.0.0.1', supervisor) self.assertIn('--client 192.0.2.20', supervisor) self.assertIn('--backend 1,127.0.0.1,4322', supervisor) self.assertIn('--backend 2,127.0.0.1,4323', supervisor) self.assertNotIn('--backend 0,', supervisor) self.assertNotIn('--backend 3,', supervisor) self.assertEqual( [worker.getint('REPORTS', 'REPORT_PORT') for worker in workers], [4322, 4323], ) self.assertTrue(all( worker.get('REPORTS', 'REPORT_CLIENTS') == '127.0.0.1' for worker in workers )) def test_supervisor_runs_aggregator_workers_bridge_proxy_mux_and_playback(self): layout = self.generate() supervisor = layout.supervisor_config.read_text(encoding='utf-8') for program in ( 'hdstack-aggregator', 'hdstack-worker-1', 'hdstack-worker-2', 'bridge', 'playback-1', 'playback-2', 'proxy', 'report-mux', ): self.assertIn('[program:{}]'.format(program), supervisor) self.assertNotIn('[program:playback]', supervisor) self.assertIn( 'bridge.py -c {} -r {}'.format(layout.bridge_config, self.bridge_rules), supervisor, ) self.assertIn( 'FDPROXY_BACKEND_PORT_RANGES="[[54000,54099],[54100,54199]]"', supervisor, ) def test_bridge_can_be_disabled_without_bridge_files(self): self.bridge_config.unlink() self.bridge_rules.unlink() layout = self.generate(bridge_enabled=False) supervisor = layout.supervisor_config.read_text(encoding='utf-8') self.assertNotIn('[program:bridge]', supervisor) for program in ( 'hdstack-aggregator', 'hdstack-worker-1', 'hdstack-worker-2', 'playback-1', 'playback-2', 'proxy', 'report-mux', ): self.assertIn('[program:{}]'.format(program), supervisor) self.assertIsNone(layout.bridge_config) def test_reporting_disabled_omits_mux_and_worker_reporting(self): self.main_config.write_text( MAIN_CONFIG.replace('REPORT: True', 'REPORT: False'), encoding='utf-8', ) layout = self.generate() supervisor = layout.supervisor_config.read_text(encoding='utf-8') self.assertNotIn('[program:report-mux]', supervisor) for path in layout.worker_configs: self.assertFalse(_read_config(path).getboolean('REPORTS', 'REPORT')) def test_generated_configs_are_accepted_by_current_freedmr_parser(self): layout = self.generate() generated = ( (layout.aggregator_config,) + layout.worker_configs + layout.loro_configs + (layout.bridge_config,) ) for path in generated: parsed = config.build_config(str(path)) self.assertIn('GLOBAL', parsed) self.assertIn('SYSTEMS', parsed) def test_mutable_worker_state_files_are_distinct(self): layout = self.generate(instances=3) workers = [_read_config(path) for path in layout.worker_configs] self.assertEqual( [worker.get('ALIASES', 'SUB_MAP_FILE') for worker in workers], ['sub_map-hdstack-1.pkl', 'sub_map-hdstack-2.pkl', 'sub_map-hdstack-3.pkl'], ) self.assertEqual( [worker.get('ALIASES', 'KEYS_FILE') for worker in workers], ['keys-hdstack-1.json', 'keys-hdstack-2.json', 'keys-hdstack-3.json'], ) self.assertTrue(all( not worker.getboolean('ALIASES', 'TRY_DOWNLOAD') for worker in workers )) def test_generation_does_not_modify_supplied_configuration_or_rules(self): before = ( self.main_config.read_bytes(), self.bridge_config.read_bytes(), self.bridge_rules.read_bytes(), ) self.generate() self.assertEqual( before, ( self.main_config.read_bytes(), self.bridge_config.read_bytes(), self.bridge_rules.read_bytes(), ), ) def test_invalid_instance_count_base_id_and_system_template_are_rejected(self): for instances in (1, 6): with self.subTest(instances=instances): with self.assertRaises(hdstack_runtime.HDStackConfigurationError): self.generate(instances=instances) with self.assertRaises(hdstack_runtime.HDStackConfigurationError): self.generate(base_id=0xFFFFFFFF - 1) self.main_config.write_text( MAIN_CONFIG.replace('MODE: MASTER', 'MODE: PEER'), encoding='utf-8', ) with self.assertRaises(hdstack_runtime.HDStackConfigurationError): self.generate() with self.assertRaises(hdstack_runtime.HDStackConfigurationError): self.generate(bridge_enabled=1) def test_enabled_bridge_requires_a_distinct_nonzero_server_id(self): for bridge_id in (0, 23400, 23401, 23402): with self.subTest(bridge_id=bridge_id): self.bridge_config.write_text( BRIDGE_CONFIG.replace( 'SERVER_ID: 23500', 'SERVER_ID: {}'.format(bridge_id), ), encoding='utf-8', ) with self.assertRaises(hdstack_runtime.HDStackConfigurationError): self.generate() self.bridge_config.write_text( BRIDGE_CONFIG.replace('SERVER_ID: 23500', 'SERVER_ID: 23399'), encoding='utf-8', ) self.assertIsNotNone(self.generate().bridge_config) def test_missing_bridge_files_and_port_collisions_fail_before_generation(self): self.bridge_rules.unlink() with self.assertRaises(hdstack_runtime.HDStackConfigurationError): self.generate() self.bridge_rules.write_text('BRIDGES = {}\n', encoding='utf-8') self.main_config.write_text( MAIN_CONFIG.replace('PORT: 62044', 'PORT: 7001'), encoding='utf-8', ) with self.assertRaises(hdstack_runtime.HDStackConfigurationError): self.generate() class HDStackContainerTests(unittest.TestCase): def test_standard_entrypoint_derives_default_and_preserves_explicit_single_mode(self): entrypoint = (DOCKER_CONFIGS / 'entrypoint-proxy').read_text(encoding='utf-8') self.assertIn('HDSTACK_COUNT="${HDSTACK:-}"', entrypoint) self.assertIn('getconf _NPROCESSORS_ONLN', entrypoint) self.assertIn('HDSTACK_COUNT=$((CPU_COUNT - 1))', entrypoint) self.assertIn('HDSTACK_COUNT=5', entrypoint) self.assertIn('[ "$HDSTACK_COUNT" = 1 ]', entrypoint) self.assertIn('/etc/supervisor/conf.d/supervisord.conf', entrypoint) self.assertIn('[ "$HDSTACK_COUNT" -ge 2 ]', entrypoint) self.assertIn('[ "$HDSTACK_COUNT" -le 5 ]', entrypoint) self.assertIn('--base-id "$HDSTACK_BASEID"', entrypoint) self.assertIn('HDSTACK_BRIDGE="${HDSTACK_BRIDGE:-0}"', entrypoint) self.assertIn('--bridge-enabled "$HDSTACK_BRIDGE"', entrypoint) self.assertIn('--loro-config /opt/freedmr/loro.cfg', entrypoint) self.assertIn('HDSTACK_BRIDGE_CONFIG', entrypoint) self.assertIn('HDSTACK_BRIDGE_RULES', entrypoint) self.assertIn('/^\\[GLOBAL\\]/', entrypoint) def test_standard_compose_enables_cpu_derived_hdstack_and_skeleton_mounts(self): compose = (DOCKER_CONFIGS / 'docker-compose.yml').read_text(encoding='utf-8') self.assertIn("#- HDSTACK=5", compose) self.assertIn("#- HDSTACK_BASEID=23400", compose) self.assertIn("- HDSTACK_BRIDGE=0", compose) self.assertIn('freedmr:latest', compose) self.assertIn( '/etc/freedmr/freedmr-bridge.cfg:/opt/freedmr/freedmr-bridge.cfg:ro', compose, ) self.assertIn( '/etc/freedmr/rules-bridge.py:/opt/freedmr/rules.py:ro', compose, ) self.assertIn('#- HDSTACK_BRIDGE_CONFIG=', compose) self.assertIn('#- HDSTACK_BRIDGE_RULES=', compose) self.assertIn( ' tmpfs: /tmp\n read_only: "true"', compose, ) self.assertIn("62041-62045:62041-62045/udp", compose) self.assertIn("container_name: freedmr-data-gateway", compose) self.assertIn("freedmr-data-gateway:latest", compose) self.assertNotIn("freedmr-data-gateway:development-latest", compose) self.assertIn("FREEDMR_GATEWAY_INGRESS_MODE: fbp", compose) self.assertIn("FREEDMR_GATEWAY_FBP_RELATIONSHIPS:", compose) self.assertIn("/etc/freedmr/json/:/data/:ro", compose) def test_installer_records_guided_hdstack_and_bridge_choices(self): installer = ( DOCKER_CONFIGS / 'docker-compose_install.sh' ).read_text(encoding='utf-8') self.assertIn('Assigned Server ID, or your own DMR ID', installer) self.assertIn('HDStack allows FreeDMR to use more than one CPU core', installer) self.assertIn('DEFAULT_HDSTACK=$((CPU_COUNT - 1))', installer) self.assertIn("prompt_value 'Number of HDStack workers (1-5)'", installer) self.assertIn("prompt_value 'Enable the separate bridge server?", installer) self.assertIn("prompt_value 'Bridge server ID'", installer) self.assertIn('docker-configs/freedmr-bridge.cfg', installer) self.assertIn('docker-configs/rules-bridge.py', installer) self.assertIn('- HDSTACK=$HDSTACK_COUNT', installer) self.assertIn('- HDSTACK_BASEID=$HDSTACK_BASEID', installer) self.assertIn('- HDSTACK_BRIDGE=$HDSTACK_BRIDGE_VALUE', installer) self.assertIn("prompt_value 'APRS-IS login callsign'", installer) self.assertIn("prompt_value 'APRS-IS passcode'", installer) self.assertIn('build_data_gateway_relationships', installer) self.assertIn('\\"bind_port\\":${bind_port}', installer) self.assertIn('\\"peer_port\\":${peer_port}', installer) self.assertIn('DATA_GATEWAY: True', installer) self.assertIn('ENABLED: True', installer) self.assertIn('TARGET_IP: freedmr-data-gateway', installer) self.assertIn('s/^SERVER_ID:.*/SERVER_ID: $HDSTACK_BASEID/', installer) self.assertIn('s/^SERVER_ID:.*/SERVER_ID: $HDSTACK_BRIDGE_ID/', installer) self.assertNotIn('/etc/freedmr/.env', installer) def test_installer_uses_current_debian_docker_repository(self): installer = ( DOCKER_CONFIGS / 'docker-compose_install.sh' ).read_text(encoding='utf-8') self.assertNotIn('docker-engine', installer) self.assertIn("dpkg-query -W -f='${Status}'", installer) self.assertIn('/etc/apt/keyrings/docker.asc', installer) self.assertIn('/etc/apt/sources.list.d/docker.sources', installer) self.assertIn('$(dpkg --print-architecture)', installer) self.assertIn( 'docker-ce docker-ce-cli containerd.io ' 'docker-buildx-plugin docker-compose-plugin', installer, ) self.assertIn('docker compose up -d', installer) self.assertIn('Starting the FreeDMR container...', installer) self.assertNotIn('release-candidate container', installer) self.assertNotIn('apt-key', installer) self.assertNotIn('add-apt-repository', installer) def test_shipped_config_keeps_native_gateway_disabled_until_install(self): for path in ( ROOT / 'FreeDMR.cfg', ROOT / 'freedmr.cfg', DOCKER_CONFIGS / 'freedmr.cfg', ): with self.subTest(path=path): config = _read_config(path) self.assertFalse(config.getboolean('GLOBAL', 'DATA_GATEWAY')) self.assertFalse(config.getboolean('DATA-GATEWAY', 'ENABLED')) self.assertEqual(config.get('DATA-GATEWAY', 'MODE'), 'OPENBRIDGE') self.assertEqual(config.getint('DATA-GATEWAY', 'PROTO_VER'), 5) self.assertTrue(config.getboolean('DATA-GATEWAY', 'ENHANCED_OBP')) def test_bridge_templates_are_valid_and_idle(self): bridge = _read_config(DOCKER_CONFIGS / 'freedmr-bridge.cfg') rules_text = ( DOCKER_CONFIGS / 'rules-bridge.py' ).read_text(encoding='utf-8') rules = {} exec(compile(rules_text, 'rules-bridge.py', 'exec'), rules) self.assertEqual(bridge.getint('GLOBAL', 'SERVER_ID'), 0) self.assertFalse(bridge.getboolean('REPORTS', 'REPORT')) self.assertFalse(any( bridge.has_option(section, 'MODE') for section in bridge.sections() )) self.assertEqual(rules['BRIDGES'], {}) def test_image_has_no_obsolete_static_hdstack_supervisor(self): dockerfile = (DOCKER_CONFIGS / 'Dockerfile-ci').read_text(encoding='utf-8') self.assertNotIn('supervisord-hdstack.conf', dockerfile) self.assertFalse((DOCKER_CONFIGS / 'supervisord-hdstack.conf').exists()) self.assertFalse((DOCKER_CONFIGS / 'docker-compose-hdstack.yml').exists()) def _read_config(path): parser = configparser.ConfigParser(interpolation=None) loaded = parser.read(path) if not loaded: raise AssertionError('could not read {}'.format(path)) return parser MAIN_CONFIG = """ [GLOBAL] SERVER_ID: 9999 GEN_STAT_BRIDGES: False ENABLE_API: True DATA_GATEWAY: True VALIDATE_SERVER_IDS: False USE_ACL: True REG_ACL: PERMIT:ALL SUB_ACL: DENY:1 TGID_TS1_ACL: PERMIT:ALL TGID_TS2_ACL: PERMIT:ALL [REPORTS] REPORT: True REPORT_PORT: 4321 REPORT_CLIENTS: 127.0.0.1,192.0.2.20 [LOGGER] LOG_FILE: /dev/null LOG_HANDLERS: console-timed LOG_LEVEL: INFO LOG_NAME: FreeDMR [ALIASES] TRY_DOWNLOAD: True PATH: ./json/ SUB_MAP_FILE: sub_map.pkl KEYS_FILE: keys.json [ALLSTAR] ENABLED: False [EXTERNAL-FBP] MODE: OPENBRIDGE ENABLED: True IP: 127.0.0.1 PORT: 62044 NETWORK_ID: 23499 PASSPHRASE: external TARGET_IP: 127.0.0.1 TARGET_PORT: 62045 USE_ACL: True SUB_ACL: DENY:1 TGID_ACL: PERMIT:ALL RELAX_CHECKS: True ENHANCED_OBP: True PROTO_VER: 5 [SYSTEM] MODE: MASTER ENABLED: True REPEAT: True MAX_PEERS: 1 IP: 127.0.0.1 PORT: 54000 PASSPHRASE: GROUP_HANGTIME: 5 USE_ACL: True REG_ACL: DENY:1 SUB_ACL: DENY:1 TGID_TS1_ACL: PERMIT:ALL TGID_TS2_ACL: PERMIT:ALL DEFAULT_UA_TIMER: 10 SINGLE_MODE: True VOICE_IDENT: True DIAL_A_TG: True DYNAMIC_TG_ROUTING: True NETWORK_DIRECT_DIAL_SLOT: 1 TS1_STATIC: TS2_STATIC: 2350 GENERATOR: 100 ALLOW_UNREG_ID: True PROXY_CONTROL: True ANNOUNCEMENT_LANGUAGE: en_GB [ECHO] MODE: PEER ENABLED: True LOOSE: False EXPORT_AMBE: False IP: 127.0.0.1 PORT: 54916 MASTER_IP: 127.0.0.1 MASTER_PORT: 54915 PASSPHRASE: passw0rd CALLSIGN: TEST RADIO_ID: 2340210 RX_FREQ: 449000000 TX_FREQ: 444000000 TX_POWER: 25 COLORCODE: 1 SLOTS: 1 LATITUDE: 00.0000 LONGITUDE: 000.0000 HEIGHT: 0 LOCATION: Test DESCRIPTION: ECHO URL: example.invalid SOFTWARE_ID: test PACKAGE_ID: test GROUP_HANGTIME: 5 OPTIONS: USE_ACL: True SUB_ACL: DENY:1 TGID_TS1_ACL: PERMIT:ALL TGID_TS2_ACL: PERMIT:ALL ANNOUNCEMENT_LANGUAGE: en_GB """.lstrip() DATA_GATEWAY_CONFIG = """ [DATA-GATEWAY] MODE: OPENBRIDGE ENABLED: True IP: 0.0.0.0 PORT: 62041 NETWORK_ID: 0 PASSPHRASE: gateway-test TARGET_IP: data-gateway TARGET_PORT: 62031 USE_ACL: False SUB_ACL: PERMIT:ALL TGID_ACL: PERMIT:ALL RELAX_CHECKS: False ENHANCED_OBP: True PROTO_VER: 5 """.lstrip() BRIDGE_CONFIG = """ [GLOBAL] SERVER_ID: 23500 USE_ACL: True REG_ACL: PERMIT:ALL SUB_ACL: DENY:1 TGID_TS1_ACL: PERMIT:ALL TGID_TS2_ACL: PERMIT:ALL [REPORTS] REPORT: False REPORT_PORT: 4421 [LOGGER] [ALIASES] TRY_DOWNLOAD: False [ALLSTAR] ENABLED: False """.lstrip() if __name__ == '__main__': unittest.main()