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.
814 lines
27 KiB
814 lines
27 KiB
###############################################################################
|
|
# Copyright (C) 2026 Simon Adlem, G7RZU <g7rzu@gb7fr.org.uk>
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
###############################################################################
|
|
|
|
"""Generate private FreeDMR HDStack process configuration."""
|
|
|
|
import argparse
|
|
import configparser
|
|
import json
|
|
import shlex
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
|
|
MIN_INSTANCES = 2
|
|
MAX_INSTANCES = 5
|
|
MAX_SERVER_ID = 0xFFFFFFFF
|
|
SUPPORT_SECTIONS = ('GLOBAL', 'REPORTS', 'LOGGER', 'ALIASES', 'ALLSTAR')
|
|
INTERNAL_WORKER_PORT_BASE = 7000
|
|
INTERNAL_AGGREGATOR_PORT_BASE = 7100
|
|
INTERNAL_AGGREGATOR_BRIDGE_PORT = 7200
|
|
INTERNAL_BRIDGE_PORT = 7201
|
|
LORO_PORT_STRIDE = 2
|
|
|
|
|
|
class HDStackConfigurationError(ValueError):
|
|
"""Raised when an HDStack deployment cannot be generated safely."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class HDStackLayout:
|
|
"""Paths and derived addresses for one generated deployment."""
|
|
|
|
instances: int
|
|
base_server_id: int
|
|
aggregator_config: Path
|
|
worker_configs: tuple
|
|
loro_configs: tuple
|
|
bridge_config: Optional[Path]
|
|
supervisor_config: Path
|
|
backend_ranges: tuple
|
|
worker_report_ports: tuple
|
|
|
|
|
|
def generate_hdstack(
|
|
main_config,
|
|
bridge_config,
|
|
bridge_rules,
|
|
output_dir,
|
|
instances,
|
|
base_server_id,
|
|
bridge_enabled=True,
|
|
loro_config=None,
|
|
):
|
|
"""Generate configs for an aggregator, workers, optional bridge, proxy and MUX."""
|
|
instances = _validated_instances(instances)
|
|
base_server_id = _validated_base_id(base_server_id, instances)
|
|
bridge_enabled = _validated_bridge_enabled(bridge_enabled)
|
|
main_path = _required_file(main_config, 'main FreeDMR configuration')
|
|
source = _read_config(main_path)
|
|
echo_enabled = _echo_enabled(source)
|
|
|
|
loro_path = None
|
|
loro_source = None
|
|
if echo_enabled:
|
|
if loro_config is None:
|
|
loro_config = Path(__file__).resolve().with_name('loro.cfg')
|
|
loro_path = _required_file(loro_config, 'Loro configuration')
|
|
loro_source = _read_config(loro_path)
|
|
_validate_loro_config(source, loro_source)
|
|
|
|
bridge_path = None
|
|
rules_path = None
|
|
bridge_source = None
|
|
bridge_server_id = None
|
|
if bridge_enabled:
|
|
bridge_path = _required_file(bridge_config, 'bridge configuration')
|
|
rules_path = _required_file(bridge_rules, 'bridge rules')
|
|
bridge_source = _read_config(bridge_path)
|
|
bridge_server_id = _section_integer(
|
|
bridge_source,
|
|
'GLOBAL',
|
|
'SERVER_ID',
|
|
0,
|
|
)
|
|
_validate_bridge_server_id(
|
|
bridge_server_id,
|
|
base_server_id,
|
|
instances,
|
|
)
|
|
output_path = Path(output_dir)
|
|
system = _system_template(source)
|
|
|
|
hbp_port = _integer(system, 'PORT', 54000, 'SYSTEM PORT')
|
|
generator_size = _integer(system, 'GENERATOR', 100, 'SYSTEM GENERATOR')
|
|
if generator_size < 1:
|
|
raise HDStackConfigurationError('SYSTEM GENERATOR must be positive')
|
|
|
|
backend_ranges = tuple(
|
|
(
|
|
hbp_port + (number - 1) * generator_size,
|
|
hbp_port + number * generator_size - 1,
|
|
)
|
|
for number in range(1, instances + 1)
|
|
)
|
|
for start, end in backend_ranges:
|
|
_validate_port(start, 'generated HBP range start')
|
|
_validate_port(end, 'generated HBP range end')
|
|
|
|
loro_port_pairs = ()
|
|
if echo_enabled:
|
|
echo_peer_port = _section_integer(source, 'ECHO', 'PORT', 54916)
|
|
echo_master_port = _section_integer(source, 'ECHO', 'MASTER_PORT', 54915)
|
|
loro_port_pairs = tuple(
|
|
(
|
|
echo_peer_port + (number - 1) * LORO_PORT_STRIDE,
|
|
echo_master_port + (number - 1) * LORO_PORT_STRIDE,
|
|
)
|
|
for number in range(1, instances + 1)
|
|
)
|
|
|
|
loro_report_ports = ()
|
|
if echo_enabled:
|
|
loro_report_port = _section_integer(loro_source, 'REPORTS', 'REPORT_PORT', 4821)
|
|
loro_report_ports = tuple(
|
|
loro_report_port + number - 1 for number in range(1, instances + 1)
|
|
)
|
|
|
|
report_enabled = _boolean(source, 'REPORTS', 'REPORT', True)
|
|
report_port = _section_integer(source, 'REPORTS', 'REPORT_PORT', 4321)
|
|
_validate_port(report_port, 'report MUX port')
|
|
worker_report_ports = tuple(
|
|
report_port + number for number in range(1, instances + 1)
|
|
)
|
|
if report_enabled:
|
|
for port in worker_report_ports:
|
|
_validate_port(port, 'worker report port')
|
|
|
|
data_gateway_enabled, data_gateway_ports, data_gateway_target_ports = (
|
|
_data_gateway_layout(source, instances)
|
|
)
|
|
|
|
_validate_generated_port_collisions(
|
|
source,
|
|
bridge_source,
|
|
backend_ranges,
|
|
report_enabled,
|
|
report_port,
|
|
worker_report_ports,
|
|
instances,
|
|
bridge_enabled,
|
|
loro_port_pairs,
|
|
loro_report_ports,
|
|
data_gateway_ports,
|
|
data_gateway_target_ports,
|
|
)
|
|
|
|
aggregator = _support_config(source)
|
|
aggregator['GLOBAL']['SERVER_ID'] = str(base_server_id)
|
|
aggregator['GLOBAL']['HDSTACK_INSTANCES'] = str(instances)
|
|
aggregator['GLOBAL']['HDSTACK_BASE_SERVER_ID'] = str(base_server_id)
|
|
aggregator['GLOBAL']['GEN_STAT_BRIDGES'] = 'True'
|
|
aggregator['REPORTS']['REPORT'] = 'False'
|
|
aggregator['GLOBAL']['DATA_GATEWAY'] = 'False'
|
|
for section in source.sections():
|
|
if section != 'DATA-GATEWAY' and _mode(source, section) == 'OPENBRIDGE':
|
|
_copy_section(source, aggregator, section)
|
|
for number in range(1, instances + 1):
|
|
name = 'OBP-HDSTACK-WORKER-{}'.format(number)
|
|
if source.has_section(name):
|
|
raise HDStackConfigurationError(
|
|
'main configuration already contains reserved section {}'.format(name)
|
|
)
|
|
_add_internal_openbridge(
|
|
aggregator,
|
|
name,
|
|
INTERNAL_AGGREGATOR_PORT_BASE + number,
|
|
INTERNAL_WORKER_PORT_BASE + number,
|
|
base_server_id + number,
|
|
)
|
|
|
|
generated_bridge = None
|
|
if bridge_enabled:
|
|
aggregator_name = 'OBP-HDSTACK-BRIDGE'
|
|
bridge_name = 'OBP-HDSTACK-AGGREGATOR'
|
|
for config_source, name in (
|
|
(source, aggregator_name),
|
|
(bridge_source, bridge_name),
|
|
):
|
|
if config_source.has_section(name):
|
|
raise HDStackConfigurationError(
|
|
'configuration already contains reserved section {}'.format(name)
|
|
)
|
|
_add_internal_openbridge(
|
|
aggregator,
|
|
aggregator_name,
|
|
INTERNAL_AGGREGATOR_BRIDGE_PORT,
|
|
INTERNAL_BRIDGE_PORT,
|
|
bridge_server_id,
|
|
)
|
|
generated_bridge = _clone_config(bridge_source)
|
|
_add_internal_openbridge(
|
|
generated_bridge,
|
|
bridge_name,
|
|
INTERNAL_BRIDGE_PORT,
|
|
INTERNAL_AGGREGATOR_BRIDGE_PORT,
|
|
base_server_id,
|
|
)
|
|
|
|
workers = []
|
|
loros = []
|
|
for number, (start, _end) in enumerate(backend_ranges, 1):
|
|
worker = _support_config(source)
|
|
worker['GLOBAL']['SERVER_ID'] = str(base_server_id + number)
|
|
worker['GLOBAL']['HDSTACK_INSTANCES'] = str(instances)
|
|
worker['GLOBAL']['HDSTACK_BASE_SERVER_ID'] = str(base_server_id)
|
|
worker['GLOBAL']['GEN_STAT_BRIDGES'] = 'False'
|
|
worker['GLOBAL']['ENABLE_API'] = 'False'
|
|
worker['GLOBAL']['DATA_GATEWAY'] = str(data_gateway_enabled)
|
|
worker['ALIASES']['TRY_DOWNLOAD'] = 'False'
|
|
_number_mutable_alias_file(worker, 'SUB_MAP_FILE', number)
|
|
_number_mutable_alias_file(worker, 'KEYS_FILE', number)
|
|
worker['REPORTS']['REPORT'] = str(report_enabled)
|
|
worker['REPORTS']['REPORT_PORT'] = str(worker_report_ports[number - 1])
|
|
worker['REPORTS']['REPORT_CLIENTS'] = '127.0.0.1'
|
|
_copy_section(source, worker, 'SYSTEM')
|
|
worker['SYSTEM']['PORT'] = str(start)
|
|
if data_gateway_enabled:
|
|
_copy_section(source, worker, 'DATA-GATEWAY')
|
|
worker['DATA-GATEWAY']['PORT'] = str(data_gateway_ports[number - 1])
|
|
worker['DATA-GATEWAY']['TARGET_PORT'] = str(data_gateway_target_ports[number - 1])
|
|
|
|
name = 'OBP-HDSTACK-AGGREGATOR'
|
|
if echo_enabled:
|
|
echo_peer_port, echo_master_port = loro_port_pairs[number - 1]
|
|
_copy_section(source, worker, 'ECHO')
|
|
worker['ECHO']['IP'] = '127.0.0.1'
|
|
worker['ECHO']['PORT'] = str(echo_peer_port)
|
|
worker['ECHO']['MASTER_IP'] = '127.0.0.1'
|
|
worker['ECHO']['MASTER_PORT'] = str(echo_master_port)
|
|
|
|
loro = _clone_config(loro_source)
|
|
loro['GLOBAL']['SERVER_ID'] = '9990'
|
|
loro['REPORTS']['REPORT'] = 'False'
|
|
loro['REPORTS']['REPORT_PORT'] = str(loro_report_ports[number - 1])
|
|
loro['LOGGER']['LOG_NAME'] = 'ECHO-{}'.format(number)
|
|
loro['ALIASES']['TRY_DOWNLOAD'] = 'False'
|
|
loro['PARROT']['IP'] = '127.0.0.1'
|
|
loro['PARROT']['PORT'] = str(echo_master_port)
|
|
loro['PARROT']['MAX_PEERS'] = '1'
|
|
loro['PARROT']['PASSPHRASE'] = worker['ECHO']['PASSPHRASE']
|
|
loros.append(loro)
|
|
if source.has_section(name):
|
|
raise HDStackConfigurationError(
|
|
'main configuration already contains reserved section {}'.format(name)
|
|
)
|
|
_add_internal_openbridge(
|
|
worker,
|
|
name,
|
|
INTERNAL_WORKER_PORT_BASE + number,
|
|
INTERNAL_AGGREGATOR_PORT_BASE + number,
|
|
base_server_id,
|
|
)
|
|
workers.append(worker)
|
|
|
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
aggregator_path = output_path / 'aggregator.cfg'
|
|
worker_paths = tuple(
|
|
output_path / 'worker-{}.cfg'.format(number)
|
|
for number in range(1, instances + 1)
|
|
)
|
|
supervisor_path = output_path / 'supervisord.conf'
|
|
|
|
loro_paths = tuple(
|
|
output_path / 'loro-{}.cfg'.format(number)
|
|
for number in range(1, instances + 1)
|
|
) if echo_enabled else ()
|
|
generated_bridge_path = (
|
|
output_path / 'bridge.cfg'
|
|
if bridge_enabled
|
|
else None
|
|
)
|
|
_write_config(aggregator, aggregator_path)
|
|
for worker, path in zip(workers, worker_paths):
|
|
_write_config(worker, path)
|
|
for loro, path in zip(loros, loro_paths):
|
|
_write_config(loro, path)
|
|
if generated_bridge_path is not None:
|
|
_write_config(generated_bridge, generated_bridge_path)
|
|
supervisor_path.write_text(
|
|
_supervisor_config(
|
|
aggregator_path,
|
|
worker_paths,
|
|
loro_paths,
|
|
generated_bridge_path,
|
|
rules_path,
|
|
backend_ranges,
|
|
report_enabled,
|
|
report_port,
|
|
worker_report_ports,
|
|
_report_clients(source),
|
|
bridge_enabled,
|
|
),
|
|
encoding='utf-8',
|
|
)
|
|
|
|
return HDStackLayout(
|
|
instances=instances,
|
|
base_server_id=base_server_id,
|
|
aggregator_config=aggregator_path,
|
|
worker_configs=worker_paths,
|
|
loro_configs=loro_paths,
|
|
bridge_config=generated_bridge_path,
|
|
supervisor_config=supervisor_path,
|
|
backend_ranges=backend_ranges,
|
|
worker_report_ports=worker_report_ports,
|
|
)
|
|
|
|
|
|
def _validated_instances(value):
|
|
if isinstance(value, bool) or not isinstance(value, int):
|
|
raise HDStackConfigurationError('HDSTACK must be an integer')
|
|
if value < MIN_INSTANCES or value > MAX_INSTANCES:
|
|
raise HDStackConfigurationError(
|
|
'generated HDStack requires HDSTACK in {}..{}'.format(
|
|
MIN_INSTANCES,
|
|
MAX_INSTANCES,
|
|
)
|
|
)
|
|
return value
|
|
|
|
|
|
def _validated_base_id(value, instances):
|
|
if isinstance(value, bool) or not isinstance(value, int):
|
|
raise HDStackConfigurationError('HDSTACK_BASEID must be an integer')
|
|
if value < 1 or value + instances > MAX_SERVER_ID:
|
|
raise HDStackConfigurationError(
|
|
'HDSTACK_BASEID and worker IDs must fit an unsigned 32-bit ID'
|
|
)
|
|
return value
|
|
|
|
|
|
def _validate_bridge_server_id(value, base_server_id, instances):
|
|
if value < 1 or value > MAX_SERVER_ID:
|
|
raise HDStackConfigurationError(
|
|
'bridge GLOBAL SERVER_ID must fit an unsigned 32-bit ID'
|
|
)
|
|
if base_server_id <= value <= base_server_id + instances:
|
|
raise HDStackConfigurationError(
|
|
'bridge GLOBAL SERVER_ID conflicts with an HDStack server ID'
|
|
)
|
|
|
|
|
|
def _echo_enabled(source):
|
|
if not source.has_section('ECHO'):
|
|
return False
|
|
if not _boolean(source, 'ECHO', 'ENABLED', True):
|
|
return False
|
|
if _mode(source, 'ECHO') != 'PEER':
|
|
raise HDStackConfigurationError('enabled [ECHO] must use MODE PEER')
|
|
return True
|
|
|
|
|
|
def _validate_loro_config(source, loro_source):
|
|
if not loro_source.has_section('PARROT'):
|
|
raise HDStackConfigurationError('Loro configuration requires [PARROT]')
|
|
if _mode(loro_source, 'PARROT') != 'MASTER':
|
|
raise HDStackConfigurationError('Loro [PARROT] must use MODE MASTER')
|
|
if not _boolean(loro_source, 'PARROT', 'ENABLED', True):
|
|
raise HDStackConfigurationError('Loro [PARROT] must be enabled')
|
|
_section_integer(source, 'ECHO', 'PORT', 54916)
|
|
_section_integer(source, 'ECHO', 'MASTER_PORT', 54915)
|
|
|
|
|
|
def _validated_bridge_enabled(value):
|
|
if not isinstance(value, bool):
|
|
raise HDStackConfigurationError('HDSTACK_BRIDGE must be 0 or 1')
|
|
return value
|
|
|
|
|
|
def _required_file(value, description):
|
|
path = Path(value)
|
|
if not path.is_file():
|
|
raise HDStackConfigurationError('{} not found: {}'.format(description, path))
|
|
return path
|
|
|
|
|
|
def _read_config(path):
|
|
parser = configparser.ConfigParser(interpolation=None)
|
|
try:
|
|
loaded = parser.read(path)
|
|
except configparser.Error as err:
|
|
raise HDStackConfigurationError(
|
|
'invalid configuration {}: {}'.format(path, err)
|
|
) from err
|
|
if not loaded:
|
|
raise HDStackConfigurationError('could not read configuration {}'.format(path))
|
|
return parser
|
|
|
|
|
|
def _system_template(source):
|
|
if not source.has_section('SYSTEM'):
|
|
raise HDStackConfigurationError('main configuration requires [SYSTEM]')
|
|
if _mode(source, 'SYSTEM') != 'MASTER':
|
|
raise HDStackConfigurationError('[SYSTEM] must use MODE MASTER')
|
|
if not _boolean(source, 'SYSTEM', 'ENABLED', True):
|
|
raise HDStackConfigurationError('[SYSTEM] must be enabled')
|
|
return source['SYSTEM']
|
|
|
|
|
|
def _support_config(source):
|
|
result = configparser.ConfigParser(interpolation=None)
|
|
for section in SUPPORT_SECTIONS:
|
|
result.add_section(section)
|
|
if source.has_section(section):
|
|
for key, value in source.items(section, raw=True):
|
|
result[section][key] = value
|
|
return result
|
|
|
|
|
|
def _copy_section(source, destination, section):
|
|
if destination.has_section(section):
|
|
destination.remove_section(section)
|
|
destination.add_section(section)
|
|
for key, value in source.items(section, raw=True):
|
|
destination[section][key] = value
|
|
|
|
|
|
def _clone_config(source):
|
|
result = configparser.ConfigParser(interpolation=None)
|
|
for section in source.sections():
|
|
_copy_section(source, result, section)
|
|
return result
|
|
|
|
|
|
def _mode(source, section):
|
|
return source.get(section, 'MODE', fallback='').strip().upper()
|
|
|
|
|
|
def _boolean(source, section, option, fallback):
|
|
if not source.has_section(section):
|
|
return fallback
|
|
try:
|
|
return source.getboolean(section, option, fallback=fallback)
|
|
except ValueError as err:
|
|
raise HDStackConfigurationError(
|
|
'{} {} must be boolean'.format(section, option)
|
|
) from err
|
|
|
|
|
|
def _integer(section, option, fallback, description):
|
|
try:
|
|
return section.getint(option, fallback=fallback)
|
|
except ValueError as err:
|
|
raise HDStackConfigurationError('{} must be an integer'.format(description)) from err
|
|
|
|
|
|
def _section_integer(source, section, option, fallback):
|
|
if not source.has_section(section):
|
|
return fallback
|
|
try:
|
|
return source.getint(section, option, fallback=fallback)
|
|
except ValueError as err:
|
|
raise HDStackConfigurationError(
|
|
'{} {} must be an integer'.format(section, option)
|
|
) from err
|
|
|
|
|
|
def _validate_port(port, description):
|
|
if port < 1 or port > 65535:
|
|
raise HDStackConfigurationError('{} must be within 1..65535'.format(description))
|
|
|
|
|
|
def _enabled_bind_ports(source, excluded_sections=()):
|
|
excluded = set(excluded_sections)
|
|
ports = {}
|
|
for section in source.sections():
|
|
if section in excluded or not _mode(source, section):
|
|
continue
|
|
if not _boolean(source, section, 'ENABLED', True):
|
|
continue
|
|
if source.has_option(section, 'PORT'):
|
|
port = _section_integer(source, section, 'PORT', 0)
|
|
_validate_port(port, '{} PORT'.format(section))
|
|
ports.setdefault(port, []).append(section)
|
|
return ports
|
|
|
|
|
|
def _data_gateway_layout(source, instances):
|
|
if not _boolean(source, 'GLOBAL', 'DATA_GATEWAY', False):
|
|
return False, (), ()
|
|
|
|
if (
|
|
not source.has_section('DATA-GATEWAY')
|
|
or not _boolean(source, 'DATA-GATEWAY', 'ENABLED', True)
|
|
):
|
|
return False, (), ()
|
|
|
|
if _mode(source, 'DATA-GATEWAY') != 'OPENBRIDGE':
|
|
raise HDStackConfigurationError('[DATA-GATEWAY] must use MODE OPENBRIDGE')
|
|
if _section_integer(source, 'DATA-GATEWAY', 'PROTO_VER', 5) != 5:
|
|
raise HDStackConfigurationError('[DATA-GATEWAY] must use PROTO_VER 5')
|
|
if not _boolean(source, 'DATA-GATEWAY', 'ENHANCED_OBP', True):
|
|
raise HDStackConfigurationError('[DATA-GATEWAY] must enable FBCP')
|
|
|
|
bind_port = _section_integer(source, 'DATA-GATEWAY', 'PORT', 0)
|
|
target_port = _section_integer(source, 'DATA-GATEWAY', 'TARGET_PORT', 0)
|
|
bind_ports = tuple(
|
|
bind_port + number - 1 for number in range(1, instances + 1)
|
|
)
|
|
target_ports = tuple(
|
|
target_port + number - 1 for number in range(1, instances + 1)
|
|
)
|
|
return True, bind_ports, target_ports
|
|
|
|
|
|
def _validate_generated_port_collisions(
|
|
source,
|
|
bridge_source,
|
|
backend_ranges,
|
|
report_enabled,
|
|
report_port,
|
|
worker_report_ports,
|
|
instances,
|
|
bridge_enabled,
|
|
loro_port_pairs,
|
|
loro_report_ports,
|
|
data_gateway_ports,
|
|
data_gateway_target_ports,
|
|
):
|
|
generated = {}
|
|
|
|
def reserve(port, owner):
|
|
_validate_port(port, owner)
|
|
if port in generated:
|
|
raise HDStackConfigurationError(
|
|
'generated port {} is used by both {} and {}'.format(
|
|
port,
|
|
generated[port],
|
|
owner,
|
|
)
|
|
)
|
|
generated[port] = owner
|
|
|
|
for number, (start, end) in enumerate(backend_ranges, 1):
|
|
for port in range(start, end + 1):
|
|
reserve(port, 'worker {} HBP'.format(number))
|
|
for number in range(1, instances + 1):
|
|
reserve(INTERNAL_WORKER_PORT_BASE + number, 'worker {} FBP'.format(number))
|
|
reserve(
|
|
INTERNAL_AGGREGATOR_PORT_BASE + number,
|
|
'aggregator worker {} FBP'.format(number),
|
|
)
|
|
for number, (port, target_port) in enumerate(
|
|
zip(data_gateway_ports, data_gateway_target_ports),
|
|
1,
|
|
):
|
|
reserve(port, 'worker {} DATA-GATEWAY'.format(number))
|
|
_validate_port(target_port, 'worker {} DATA-GATEWAY target'.format(number))
|
|
if bridge_enabled:
|
|
reserve(INTERNAL_AGGREGATOR_BRIDGE_PORT, 'aggregator bridge FBP')
|
|
reserve(INTERNAL_BRIDGE_PORT, 'bridge aggregator FBP')
|
|
for number, (peer_port, master_port) in enumerate(loro_port_pairs, 1):
|
|
reserve(peer_port, 'worker {} ECHO peer'.format(number))
|
|
reserve(master_port, 'worker {} Loro master'.format(number))
|
|
for number, port in enumerate(loro_report_ports, 1):
|
|
reserve(port, 'worker {} Loro reporting'.format(number))
|
|
if report_enabled:
|
|
reserve(report_port, 'report MUX')
|
|
for number, port in enumerate(worker_report_ports, 1):
|
|
reserve(port, 'worker {} reporting'.format(number))
|
|
|
|
configured = _enabled_bind_ports(
|
|
source,
|
|
excluded_sections=('SYSTEM', 'ECHO', 'DATA-GATEWAY'),
|
|
)
|
|
bridge_ports = {}
|
|
if bridge_enabled:
|
|
bridge_ports = _enabled_bind_ports(bridge_source)
|
|
for port, sections in configured.items():
|
|
if port in generated:
|
|
raise HDStackConfigurationError(
|
|
'main section {} conflicts with {} on port {}'.format(
|
|
', '.join(sections),
|
|
generated[port],
|
|
port,
|
|
)
|
|
)
|
|
for port, sections in bridge_ports.items():
|
|
if port in generated:
|
|
raise HDStackConfigurationError(
|
|
'bridge section {} conflicts with {} on port {}'.format(
|
|
', '.join(sections),
|
|
generated[port],
|
|
port,
|
|
)
|
|
)
|
|
|
|
if bridge_enabled and _boolean(bridge_source, 'REPORTS', 'REPORT', True):
|
|
bridge_report_port = _section_integer(
|
|
bridge_source,
|
|
'REPORTS',
|
|
'REPORT_PORT',
|
|
4321,
|
|
)
|
|
_validate_port(bridge_report_port, 'bridge report port')
|
|
if bridge_report_port in generated:
|
|
raise HDStackConfigurationError(
|
|
'bridge reporting conflicts with {} on port {}'.format(
|
|
generated[bridge_report_port],
|
|
bridge_report_port,
|
|
)
|
|
)
|
|
|
|
|
|
def _add_internal_openbridge(
|
|
config,
|
|
name,
|
|
local_port,
|
|
target_port,
|
|
expected_network_id,
|
|
):
|
|
config.add_section(name)
|
|
config[name].update({
|
|
'MODE': 'OPENBRIDGE',
|
|
'ENABLED': 'True',
|
|
'IP': '127.0.0.1',
|
|
'PORT': str(local_port),
|
|
'NETWORK_ID': str(expected_network_id),
|
|
'PASSPHRASE': 'internal',
|
|
'TARGET_IP': '127.0.0.1',
|
|
'TARGET_PORT': str(target_port),
|
|
'USE_ACL': 'False',
|
|
'SUB_ACL': 'PERMIT:ALL',
|
|
'TGID_ACL': 'PERMIT:ALL',
|
|
'RELAX_CHECKS': 'False',
|
|
'ENHANCED_OBP': 'True',
|
|
'PROTO_VER': '5',
|
|
})
|
|
|
|
|
|
def _number_mutable_alias_file(config, option, number):
|
|
value = config['ALIASES'].get(option, '').strip()
|
|
if not value:
|
|
return
|
|
path = Path(value)
|
|
suffix = ''.join(path.suffixes)
|
|
if suffix:
|
|
stem = path.name[:-len(suffix)]
|
|
else:
|
|
stem = path.name
|
|
name = '{}-hdstack-{}{}'.format(stem, number, suffix)
|
|
config['ALIASES'][option] = str(path.with_name(name))
|
|
|
|
|
|
def _report_clients(source):
|
|
if not source.has_section('REPORTS'):
|
|
return ('*',)
|
|
value = source.get('REPORTS', 'REPORT_CLIENTS', fallback='*')
|
|
clients = tuple(item.strip() for item in value.split(',') if item.strip())
|
|
return clients or ('*',)
|
|
|
|
|
|
def _write_config(config, path):
|
|
with path.open('w', encoding='utf-8') as config_file:
|
|
config.write(config_file)
|
|
|
|
|
|
def _program(name, command, priority, environment=None):
|
|
lines = [
|
|
'[program:{}]'.format(name),
|
|
'directory=/opt/freedmr',
|
|
'stdout_logfile=/dev/fd/1',
|
|
'stdout_logfile_maxbytes=0',
|
|
'redirect_stderr=true',
|
|
'command={}'.format(command),
|
|
'stopwaitsecs=30',
|
|
'autorestart=true',
|
|
'priority={}'.format(priority),
|
|
]
|
|
if environment:
|
|
lines.append('environment={}'.format(environment))
|
|
return '\n'.join(lines)
|
|
|
|
|
|
def _supervisor_config(
|
|
aggregator_path,
|
|
worker_paths,
|
|
loro_paths,
|
|
bridge_path,
|
|
rules_path,
|
|
backend_ranges,
|
|
report_enabled,
|
|
report_port,
|
|
worker_report_ports,
|
|
report_clients,
|
|
bridge_enabled,
|
|
):
|
|
parts = [
|
|
'[supervisord]\n'
|
|
'nodaemon=true\n'
|
|
'logfile=/dev/null\n'
|
|
'logfile_maxbytes=0\n'
|
|
'pidfile={}'.format(aggregator_path.parent / 'supervisord.pid'),
|
|
_program(
|
|
'hdstack-aggregator',
|
|
'python /opt/freedmr/bridge_master.py -c {}'.format(
|
|
shlex.quote(str(aggregator_path))
|
|
),
|
|
10,
|
|
),
|
|
]
|
|
for number, path in enumerate(worker_paths, 1):
|
|
parts.append(
|
|
_program(
|
|
'hdstack-worker-{}'.format(number),
|
|
'python /opt/freedmr/bridge_master.py -c {}'.format(
|
|
shlex.quote(str(path))
|
|
),
|
|
20,
|
|
)
|
|
)
|
|
for number, path in enumerate(loro_paths, 1):
|
|
parts.append(
|
|
_program(
|
|
'playback-{}'.format(number),
|
|
'/opt/freedmr/playback.py -c {}'.format(
|
|
shlex.quote(str(path))
|
|
),
|
|
30,
|
|
)
|
|
)
|
|
if bridge_enabled:
|
|
parts.append(
|
|
_program(
|
|
'bridge',
|
|
'python /opt/freedmr/bridge.py -c {} -r {}'.format(
|
|
shlex.quote(str(bridge_path)),
|
|
shlex.quote(str(rules_path)),
|
|
),
|
|
30,
|
|
)
|
|
)
|
|
parts.extend([
|
|
_program(
|
|
'proxy',
|
|
'python /opt/freedmr/hotspot_proxy_v2.py',
|
|
40,
|
|
'FDPROXY_BACKEND_PORT_RANGES="{}"'.format(
|
|
json.dumps(backend_ranges, separators=(',', ':'))
|
|
),
|
|
),
|
|
])
|
|
if report_enabled:
|
|
command = [
|
|
'python',
|
|
'/opt/freedmr/report_mux.py',
|
|
'--listen-interface',
|
|
'0.0.0.0',
|
|
'--listen-port',
|
|
str(report_port),
|
|
]
|
|
for client in report_clients:
|
|
command.extend(('--client', client))
|
|
for number, port in enumerate(worker_report_ports, 1):
|
|
command.extend(('--backend', '{},127.0.0.1,{}'.format(number, port)))
|
|
parts.append(
|
|
_program(
|
|
'report-mux',
|
|
' '.join(shlex.quote(item) for item in command),
|
|
50,
|
|
)
|
|
)
|
|
return '\n\n'.join(parts) + '\n'
|
|
|
|
|
|
def main(argv=None):
|
|
parser = argparse.ArgumentParser(
|
|
description='Generate an HDStack FreeDMR container configuration',
|
|
)
|
|
parser.add_argument('--main-config', required=True)
|
|
parser.add_argument('--bridge-config', required=True)
|
|
parser.add_argument('--bridge-rules', required=True)
|
|
parser.add_argument('--output-dir', required=True)
|
|
parser.add_argument('--loro-config', default='/opt/freedmr/loro.cfg')
|
|
parser.add_argument('--instances', required=True, type=int)
|
|
parser.add_argument('--base-id', required=True, type=int)
|
|
parser.add_argument('--bridge-enabled', choices=('0', '1'), default='1')
|
|
args = parser.parse_args(argv)
|
|
try:
|
|
generate_hdstack(
|
|
args.main_config,
|
|
args.bridge_config,
|
|
args.bridge_rules,
|
|
args.output_dir,
|
|
args.instances,
|
|
args.base_id,
|
|
bridge_enabled=args.bridge_enabled == '1',
|
|
loro_config=args.loro_config,
|
|
)
|
|
except HDStackConfigurationError as err:
|
|
parser.error(str(err))
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|