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.
dvmhost/tools/dvmcfggen/answers_loader.py

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,
}

Powered by TurnKey Linux.