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.
759 lines
28 KiB
759 lines
28 KiB
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()
|