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.
FreeDMR/hdstack_runtime.py

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())

Powered by TurnKey Linux.