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.
195 lines
7.1 KiB
195 lines
7.1 KiB
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-2.0-only
|
|
#/**
|
|
#* Digital Voice Modem - Host Configuration Generator
|
|
#* GPLv2 Open Source. Use is subject to license terms.
|
|
#* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
#*
|
|
#*/
|
|
#/*
|
|
#* Copyright (C) 2026 by Bryan Biedenkapp N2PLL
|
|
#*
|
|
#* 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 2 of the License, or
|
|
#* (at your option) any later version.
|
|
#*
|
|
#* This program is distributed in the hope that it will be useful,
|
|
#* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
#* GNU General Public License for more details.
|
|
#*
|
|
#* You should have received a copy of the GNU General Public License
|
|
#* along with this program; if not, write to the Free Software
|
|
#* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
#*/
|
|
"""
|
|
answers_loader.py - Load and validate wizard answers from YAML files
|
|
|
|
This module handles loading answers files that can be used to pre-populate
|
|
wizard defaults, enabling batch configuration generation and automation.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Optional
|
|
import yaml
|
|
from rich.console import Console
|
|
|
|
console = Console()
|
|
|
|
|
|
class AnswersLoader:
|
|
"""Load and validate wizard answers from YAML files"""
|
|
|
|
# All supported answer keys for ConfigWizard
|
|
CONFIG_WIZARD_KEYS = {
|
|
'template', 'config_dir', 'system_identity', 'network_peer_id',
|
|
'configure_logging', 'log_path', 'activity_log_path', 'log_root',
|
|
'use_syslog', 'disable_non_auth_logging', 'modem_type', 'modem_mode',
|
|
'serial_port', 'rx_level', 'tx_level', 'rpc_config', 'generate_rpc_password',
|
|
'rest_enable', 'generate_rest_password', 'update_lookups', 'save_lookups',
|
|
'allow_activity_transfer', 'allow_diagnostic_transfer', 'allow_status_transfer',
|
|
'radio_id_file', 'radio_id_time', 'radio_id_acl', 'talkgroup_id_file',
|
|
'talkgroup_id_time', 'talkgroup_id_acl', 'dmr_color_code', 'dmr_network_id',
|
|
'dmr_site_id', 'dmr_site_model', 'p25_nac', 'p25_network_id', 'p25_system_id',
|
|
'p25_rfss_id', 'p25_site_id', 'nxdn_ran', 'nxdn_location_id', 'site_id',
|
|
'latitude', 'longitude', 'location', 'tx_power', 'tx_freq', 'band'
|
|
}
|
|
|
|
# All supported answer keys for TrunkingWizard
|
|
TRUNKING_WIZARD_KEYS = {
|
|
'system_name', 'base_dir', 'identity', 'protocol', 'vc_count',
|
|
'configure_logging', 'log_path', 'activity_log_path', 'log_root',
|
|
'use_syslog', 'disable_non_auth_logging', 'fne_address', 'fne_port',
|
|
'fne_password', 'base_peer_id', 'base_rpc_port', 'modem_type',
|
|
'rpc_password', 'generate_rpc_password', 'base_rest_port', 'rest_enable',
|
|
'rest_password', 'generate_rest_password', 'update_lookups', 'save_lookups',
|
|
'allow_activity_transfer', 'allow_diagnostic_transfer', 'allow_status_transfer',
|
|
'radio_id_file', 'radio_id_time', 'radio_id_acl', 'talkgroup_id_file',
|
|
'talkgroup_id_time', 'talkgroup_id_acl', 'dmr_color_code', 'dmr_network_id',
|
|
'dmr_site_id', 'p25_nac', 'p25_network_id', 'p25_system_id', 'p25_rfss_id',
|
|
'p25_site_id', 'site_id', 'cc_tx_freq', 'cc_band', 'vc_tx_freqs', 'vc_bands'
|
|
}
|
|
|
|
ALL_KEYS = CONFIG_WIZARD_KEYS | TRUNKING_WIZARD_KEYS
|
|
|
|
@staticmethod
|
|
def load_answers(filepath: Path) -> Dict[str, Any]:
|
|
"""
|
|
Load answers from YAML file
|
|
|
|
Args:
|
|
filepath: Path to YAML answers file
|
|
|
|
Returns:
|
|
Dictionary of answers
|
|
|
|
Raises:
|
|
FileNotFoundError: If file doesn't exist
|
|
yaml.YAMLError: If file is invalid YAML
|
|
"""
|
|
try:
|
|
with open(filepath, 'r') as f:
|
|
answers = yaml.safe_load(f)
|
|
|
|
if answers is None:
|
|
return {}
|
|
|
|
if not isinstance(answers, dict):
|
|
raise ValueError("Answers file must contain a YAML dictionary")
|
|
|
|
return answers
|
|
except FileNotFoundError:
|
|
console.print(f"[red]Error: Answers file not found: {filepath}[/red]")
|
|
raise
|
|
except yaml.YAMLError as e:
|
|
console.print(f"[red]Error: Invalid YAML in answers file: {e}[/red]")
|
|
raise
|
|
|
|
@staticmethod
|
|
def validate_answers(answers: Dict[str, Any], strict: bool = False) -> bool:
|
|
"""
|
|
Validate answers file has recognized keys
|
|
|
|
Args:
|
|
answers: Dictionary of answers to validate
|
|
strict: If True, fail on unrecognized keys; if False, warn only
|
|
|
|
Returns:
|
|
True if valid, False if invalid (in strict mode)
|
|
"""
|
|
invalid_keys = set(answers.keys()) - AnswersLoader.ALL_KEYS
|
|
|
|
if invalid_keys:
|
|
msg = f"Unrecognized keys in answers file: {', '.join(sorted(invalid_keys))}"
|
|
if strict:
|
|
console.print(f"[red]{msg}[/red]")
|
|
return False
|
|
else:
|
|
console.print(f"[yellow]Warning: {msg}[/yellow]")
|
|
|
|
return True
|
|
|
|
@staticmethod
|
|
def save_answers(answers: Dict[str, Any], filepath: Path) -> None:
|
|
"""
|
|
Save answers to YAML file
|
|
|
|
Args:
|
|
answers: Dictionary of answers to save
|
|
filepath: Path to save answers to
|
|
"""
|
|
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(filepath, 'w') as f:
|
|
yaml.dump(answers, f, default_flow_style=False, sort_keys=False)
|
|
|
|
@staticmethod
|
|
def merge_answers(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Merge two answers dictionaries (override takes precedence)
|
|
|
|
Args:
|
|
base: Base answers dictionary
|
|
override: Override answers dictionary
|
|
|
|
Returns:
|
|
Merged dictionary
|
|
"""
|
|
merged = base.copy()
|
|
merged.update(override)
|
|
return merged
|
|
|
|
@staticmethod
|
|
def create_template() -> Dict[str, Any]:
|
|
"""
|
|
Create a template answers dictionary with common fields
|
|
|
|
Returns:
|
|
Template dictionary with helpful comments
|
|
"""
|
|
return {
|
|
# Wizard type
|
|
'template': 'enhanced',
|
|
|
|
# Basic configuration
|
|
'config_dir': '.',
|
|
'system_identity': 'SITE001',
|
|
'network_peer_id': 100000,
|
|
|
|
# Logging configuration
|
|
'configure_logging': True,
|
|
'log_path': '/var/log/dvm',
|
|
'activity_log_path': '/var/log/dvm/activity',
|
|
'log_root': 'DVM',
|
|
'use_syslog': False,
|
|
'disable_non_auth_logging': False,
|
|
|
|
# Modem configuration
|
|
'modem_type': 'uart',
|
|
'modem_mode': 'air',
|
|
'serial_port': '/dev/ttyUSB0',
|
|
'rx_level': 50,
|
|
'tx_level': 50,
|
|
}
|