add support to dvmcfggen for logging configuration; add support to dvmcfggen for user supplied answers files to automate skipping certain wizard questions;
parent
2c61748bbd
commit
24bbfb6d25
@ -0,0 +1,378 @@
|
|||||||
|
# DVMCfgGen Answers File Feature
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The answers file feature allows you to pre-populate wizard prompts using a YAML file. This enables:
|
||||||
|
|
||||||
|
- **Batch Configuration** - Generate multiple configurations with shared defaults
|
||||||
|
- **Automation** - Integrate with deployment scripts and CI/CD pipelines
|
||||||
|
- **Reproducibility** - Save and reuse configuration templates
|
||||||
|
- **Learning** - Understand what each prompt expects
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Use Example Files
|
||||||
|
|
||||||
|
Example answers files are provided in the `examples/` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For conventional systems
|
||||||
|
dvmcfg wizard -a examples/conventional-answers.yml
|
||||||
|
|
||||||
|
# For trunked systems
|
||||||
|
dvmcfg wizard --type trunk -a examples/trunked-answers.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Your Own
|
||||||
|
|
||||||
|
Copy an example file and customize it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp examples/conventional-answers.yml my-config-answers.yml
|
||||||
|
vim my-config-answers.yml
|
||||||
|
dvmcfg wizard -a my-config-answers.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Run Without Answers File (Default Behavior)
|
||||||
|
|
||||||
|
The answers file is completely optional. The wizard works normally without it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dvmcfg wizard
|
||||||
|
```
|
||||||
|
|
||||||
|
## Answers File Format
|
||||||
|
|
||||||
|
Answers files are YAML format with one key-value pair per configuration option. All fields are optional.
|
||||||
|
|
||||||
|
### Conventional System Template
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Step 2: Basic Configuration
|
||||||
|
template: enhanced
|
||||||
|
config_dir: .
|
||||||
|
system_identity: MYSITE001
|
||||||
|
network_peer_id: 100001
|
||||||
|
|
||||||
|
# Step 3: 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
|
||||||
|
|
||||||
|
# Step 4: Modem Configuration
|
||||||
|
modem_type: uart
|
||||||
|
modem_mode: air
|
||||||
|
serial_port: /dev/ttyUSB0
|
||||||
|
rx_level: 50
|
||||||
|
tx_level: 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trunked System Template
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Step 1: System Configuration
|
||||||
|
system_name: trunked_test
|
||||||
|
base_dir: .
|
||||||
|
identity: TRUNKED001
|
||||||
|
protocol: p25
|
||||||
|
vc_count: 2
|
||||||
|
|
||||||
|
# Step 2: Logging Configuration
|
||||||
|
configure_logging: true
|
||||||
|
log_path: /var/log/dvm
|
||||||
|
activity_log_path: /var/log/dvm/activity
|
||||||
|
log_root: TRUNKED
|
||||||
|
use_syslog: false
|
||||||
|
disable_non_auth_logging: false
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Answer Keys
|
||||||
|
|
||||||
|
### ConfigWizard (Conventional Systems)
|
||||||
|
|
||||||
|
**Basic Configuration:**
|
||||||
|
- `template` - Template name (conventional, enhanced)
|
||||||
|
- `config_dir` - Configuration directory
|
||||||
|
- `system_identity` - System identity/callsign
|
||||||
|
- `network_peer_id` - Network peer ID (integer)
|
||||||
|
|
||||||
|
**Logging Configuration:**
|
||||||
|
- `configure_logging` - Enable logging (true/false)
|
||||||
|
- `log_path` - Log file directory
|
||||||
|
- `activity_log_path` - Activity log directory
|
||||||
|
- `log_root` - Log filename prefix
|
||||||
|
- `use_syslog` - Enable syslog (true/false)
|
||||||
|
- `disable_non_auth_logging` - Disable non-authoritative logging (true/false)
|
||||||
|
|
||||||
|
**Modem Configuration:**
|
||||||
|
- `modem_type` - Modem type (uart, null)
|
||||||
|
- `modem_mode` - Modem mode (air, dfsi)
|
||||||
|
- `serial_port` - Serial port path
|
||||||
|
- `rx_level` - RX level (0-100)
|
||||||
|
- `tx_level` - TX level (0-100)
|
||||||
|
|
||||||
|
**Optional Settings:**
|
||||||
|
- `rpc_config` - Configure RPC (true/false)
|
||||||
|
- `generate_rpc_password` - Generate RPC password (true/false)
|
||||||
|
- `rest_enable` - Enable REST API (true/false)
|
||||||
|
- `generate_rest_password` - Generate REST password (true/false)
|
||||||
|
- `update_lookups` - Update lookups (true/false)
|
||||||
|
- `save_lookups` - Save lookups (true/false)
|
||||||
|
- `allow_activity_transfer` - Allow activity transfer (true/false)
|
||||||
|
- `allow_diagnostic_transfer` - Allow diagnostic transfer (true/false)
|
||||||
|
- `allow_status_transfer` - Allow status transfer (true/false)
|
||||||
|
|
||||||
|
**Protocol Configuration (for Step 8/9):**
|
||||||
|
- `dmr_color_code` - DMR color code (integer)
|
||||||
|
- `dmr_network_id` - DMR network ID (integer)
|
||||||
|
- `dmr_site_id` - DMR site ID (integer)
|
||||||
|
- `dmr_site_model` - DMR site model (small, tiny, large, huge)
|
||||||
|
- `p25_nac` - P25 NAC code (hex string)
|
||||||
|
- `p25_network_id` - P25 network ID (integer)
|
||||||
|
- `p25_system_id` - P25 system ID (integer)
|
||||||
|
- `p25_rfss_id` - P25 RFSS ID (integer)
|
||||||
|
- `p25_site_id` - P25 site ID (integer)
|
||||||
|
|
||||||
|
### TrunkingWizard (Trunked Systems)
|
||||||
|
|
||||||
|
**System Configuration:**
|
||||||
|
- `system_name` - System name
|
||||||
|
- `base_dir` - Base directory
|
||||||
|
- `identity` - System identity
|
||||||
|
- `protocol` - Protocol (p25, dmr)
|
||||||
|
- `vc_count` - Number of voice channels (integer)
|
||||||
|
|
||||||
|
**Logging Configuration:**
|
||||||
|
- `configure_logging` - Enable logging (true/false)
|
||||||
|
- `log_path` - Log file directory
|
||||||
|
- `activity_log_path` - Activity log directory
|
||||||
|
- `log_root` - Log filename prefix
|
||||||
|
- `use_syslog` - Enable syslog (true/false)
|
||||||
|
- `disable_non_auth_logging` - Disable non-authoritative logging (true/false)
|
||||||
|
|
||||||
|
**Network Settings:**
|
||||||
|
- `fne_address` - FNE address
|
||||||
|
- `fne_port` - FNE port (integer)
|
||||||
|
- `fne_password` - FNE password
|
||||||
|
|
||||||
|
**Optional Settings:**
|
||||||
|
- `base_peer_id` - Base peer ID (integer)
|
||||||
|
- `base_rpc_port` - Base RPC port (integer)
|
||||||
|
- `modem_type` - Modem type (uart, null)
|
||||||
|
- `generate_rpc_password` - Generate RPC password (true/false)
|
||||||
|
- `base_rest_port` - Base REST port (integer)
|
||||||
|
- `rest_enable` - Enable REST API (true/false)
|
||||||
|
- `generate_rest_password` - Generate REST password (true/false)
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Example 1: Single Configuration with All Defaults
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# hotspot-answers.yml
|
||||||
|
template: enhanced
|
||||||
|
config_dir: /etc/dvm/hotspot
|
||||||
|
system_identity: MY_HOTSPOT
|
||||||
|
network_peer_id: 100001
|
||||||
|
configure_logging: true
|
||||||
|
log_path: /var/log/dvm
|
||||||
|
log_root: HOTSPOT
|
||||||
|
modem_type: uart
|
||||||
|
modem_mode: air
|
||||||
|
serial_port: /dev/ttyUSB0
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```bash
|
||||||
|
dvmcfg wizard -a hotspot-answers.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Trunked System
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# trunk-p25-answers.yml
|
||||||
|
system_name: trunk_p25
|
||||||
|
base_dir: /etc/dvm/trunk
|
||||||
|
identity: TRUNK_P25
|
||||||
|
protocol: p25
|
||||||
|
vc_count: 4
|
||||||
|
configure_logging: true
|
||||||
|
log_path: /var/log/dvm
|
||||||
|
log_root: TRUNK_P25
|
||||||
|
p25_nac: 0x293
|
||||||
|
p25_network_id: 1
|
||||||
|
p25_system_id: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```bash
|
||||||
|
dvmcfg wizard --type trunk -a trunk-p25-answers.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Batch Generation
|
||||||
|
|
||||||
|
Create multiple configs programmatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
for SITE_NUM in {1..5}; do
|
||||||
|
SITE_ID=$((100000 + SITE_NUM))
|
||||||
|
SITE_NAME="SITE$(printf '%03d' $SITE_NUM)"
|
||||||
|
|
||||||
|
cat > /tmp/site-${SITE_NUM}-answers.yml << EOF
|
||||||
|
template: enhanced
|
||||||
|
config_dir: ./site-${SITE_NUM}
|
||||||
|
system_identity: ${SITE_NAME}
|
||||||
|
network_peer_id: ${SITE_ID}
|
||||||
|
configure_logging: true
|
||||||
|
log_root: ${SITE_NAME}
|
||||||
|
modem_type: uart
|
||||||
|
modem_mode: air
|
||||||
|
EOF
|
||||||
|
|
||||||
|
dvmcfg wizard -a /tmp/site-${SITE_NUM}-answers.yml
|
||||||
|
echo "Generated config for ${SITE_NAME}"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 4: CI/CD Pipeline
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/generate-test-configs.yml
|
||||||
|
name: Generate Test Configs
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
|
||||||
|
- name: Generate conventional config
|
||||||
|
run: |
|
||||||
|
python3 dvmhost/tools/dvmcfggen/dvmcfg.py wizard \
|
||||||
|
-a examples/conventional-answers.yml
|
||||||
|
|
||||||
|
- name: Generate trunked config
|
||||||
|
run: |
|
||||||
|
python3 dvmhost/tools/dvmcfggen/dvmcfg.py wizard \
|
||||||
|
--type trunk \
|
||||||
|
-a examples/trunked-answers.yml
|
||||||
|
|
||||||
|
- name: Validate configs
|
||||||
|
run: |
|
||||||
|
python3 dvmhost/tools/dvmcfggen/dvmcfg.py validate -c config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Partial Answers Files
|
||||||
|
|
||||||
|
You only need to specify the fields you want to pre-populate. Missing fields will be prompted for interactively:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# minimal-answers.yml
|
||||||
|
system_identity: MYSITE
|
||||||
|
network_peer_id: 100001
|
||||||
|
# User will be prompted for other values
|
||||||
|
```
|
||||||
|
|
||||||
|
### Saving Generated Answers
|
||||||
|
|
||||||
|
After creating a configuration interactively, you can extract the answers:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from answers_loader import AnswersLoader
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# After wizard completes
|
||||||
|
answers = {
|
||||||
|
'template': 'enhanced',
|
||||||
|
'system_identity': 'MYSITE001',
|
||||||
|
'network_peer_id': 100001,
|
||||||
|
# ... etc
|
||||||
|
}
|
||||||
|
|
||||||
|
AnswersLoader.save_answers(answers, Path('my-site-answers.yml'))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
Answers files are validated automatically, but you can check them manually:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from answers_loader import AnswersLoader
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
answers = AnswersLoader.load_answers(Path('my-answers.yml'))
|
||||||
|
|
||||||
|
# Non-strict validation (warns about unknown keys)
|
||||||
|
is_valid = AnswersLoader.validate_answers(answers, strict=False)
|
||||||
|
|
||||||
|
# Strict validation (fails on unknown keys)
|
||||||
|
is_valid = AnswersLoader.validate_answers(answers, strict=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### ✅ Backward Compatible
|
||||||
|
- Existing wizard usage works unchanged
|
||||||
|
- Answers file is completely optional
|
||||||
|
- No breaking changes
|
||||||
|
|
||||||
|
### ✅ Flexible
|
||||||
|
- Answer any or all questions
|
||||||
|
- Interactive prompts for missing answers
|
||||||
|
- Easy to create custom templates
|
||||||
|
|
||||||
|
### ✅ Easy to Use
|
||||||
|
- Simple YAML format
|
||||||
|
- Clear comments in example files
|
||||||
|
- Error messages for invalid files
|
||||||
|
|
||||||
|
### ✅ Reproducible
|
||||||
|
- Save and version control answers files
|
||||||
|
- Generate identical configs across systems
|
||||||
|
- Document configuration decisions
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Unrecognized keys in answers file"
|
||||||
|
|
||||||
|
This is a warning (not an error) if your answers file contains unknown keys. It won't stop the wizard from running. To suppress this, ensure you're only using valid keys from the lists above.
|
||||||
|
|
||||||
|
### "Error loading answers file"
|
||||||
|
|
||||||
|
Check that:
|
||||||
|
1. The file path is correct
|
||||||
|
2. The file is valid YAML (use `python3 -c "import yaml; yaml.safe_load(open('file.yml'))"`
|
||||||
|
3. The file has proper indentation (2 spaces per level)
|
||||||
|
|
||||||
|
### Answers not being used
|
||||||
|
|
||||||
|
Verify that:
|
||||||
|
1. The key name matches exactly (case-sensitive)
|
||||||
|
2. The file is being passed with `-a` or `--answers-file`
|
||||||
|
3. The value type is correct (string, integer, boolean)
|
||||||
|
|
||||||
|
## File Reference
|
||||||
|
|
||||||
|
### Core Files
|
||||||
|
- `answers_loader.py` - Utility for loading/validating answers
|
||||||
|
- `wizard.py` - ConfigWizard and TrunkingWizard classes (refactored)
|
||||||
|
- `dvmcfg.py` - CLI interface (updated with `--answers-file` support)
|
||||||
|
|
||||||
|
### Example Files
|
||||||
|
- `examples/conventional-answers.yml` - Example for conventional systems
|
||||||
|
- `examples/trunked-answers.yml` - Example for trunked systems
|
||||||
|
|
||||||
@ -0,0 +1,194 @@
|
|||||||
|
#!/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,
|
||||||
|
}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
#
|
||||||
|
# Digital Voice Modem - Host Configuration Generator
|
||||||
|
#
|
||||||
|
# Example answers file for conventional system configuration
|
||||||
|
# This file can be used with: dvmcfg wizard -a conventional-answers.yml
|
||||||
|
# All fields are optional; prompts will appear for missing values
|
||||||
|
|
||||||
|
# Conventional system with enhanced template
|
||||||
|
template: enhanced
|
||||||
|
|
||||||
|
# Basic Configuration (Step 2)
|
||||||
|
config_dir: .
|
||||||
|
system_identity: MYSITE001
|
||||||
|
network_peer_id: 100001
|
||||||
|
|
||||||
|
# Logging Configuration (Step 3)
|
||||||
|
configure_logging: true
|
||||||
|
log_path: /var/log/dvm
|
||||||
|
activity_log_path: /var/log/dvm/activity
|
||||||
|
log_root: MYSITE
|
||||||
|
use_syslog: false
|
||||||
|
disable_non_auth_logging: false
|
||||||
|
|
||||||
|
# Modem Configuration (Step 4)
|
||||||
|
modem_type: uart
|
||||||
|
modem_mode: air
|
||||||
|
serial_port: /dev/ttyUSB0
|
||||||
|
rx_level: 50
|
||||||
|
tx_level: 50
|
||||||
|
|
||||||
|
# Uncomment and modify the sections below as needed:
|
||||||
|
|
||||||
|
# RPC Configuration (Step 5a) - optional
|
||||||
|
# rpc_config: true
|
||||||
|
# generate_rpc_password: true
|
||||||
|
|
||||||
|
# REST API Configuration (Step 5b) - optional
|
||||||
|
# rest_enable: true
|
||||||
|
# generate_rest_password: true
|
||||||
|
|
||||||
|
# Network Lookups (Step 5c) - optional
|
||||||
|
# update_lookups: true
|
||||||
|
# save_lookups: true
|
||||||
|
|
||||||
|
# File Transfer Settings (Step 5d) - optional
|
||||||
|
# allow_activity_transfer: true
|
||||||
|
# allow_diagnostic_transfer: true
|
||||||
|
# allow_status_transfer: true
|
||||||
|
|
||||||
|
# Radio ID File Settings (Step 6) - optional
|
||||||
|
# radio_id_file: /path/to/rid.csv
|
||||||
|
# radio_id_time: 3600
|
||||||
|
# radio_id_acl: blacklist
|
||||||
|
|
||||||
|
# Talkgroup ID File Settings (Step 7) - optional
|
||||||
|
# talkgroup_id_file: /path/to/talkgroups.csv
|
||||||
|
# talkgroup_id_time: 3600
|
||||||
|
# talkgroup_id_acl: whitelist
|
||||||
|
|
||||||
|
# Protocol Configuration (Step 8) - optional
|
||||||
|
# For DMR:
|
||||||
|
# dmr_color_code: 1
|
||||||
|
# dmr_network_id: 1
|
||||||
|
# dmr_site_id: 1
|
||||||
|
# dmr_site_model: small
|
||||||
|
|
||||||
|
# For P25:
|
||||||
|
# p25_nac: 0x293
|
||||||
|
# p25_network_id: 1
|
||||||
|
# p25_system_id: 1
|
||||||
|
# p25_rfss_id: 1
|
||||||
|
# p25_site_id: 1
|
||||||
|
|
||||||
|
# For NXDN:
|
||||||
|
# nxdn_ran: 1
|
||||||
|
# nxdn_location_id: 1
|
||||||
|
|
||||||
|
# Frequency Configuration (Step 9) - optional
|
||||||
|
# site_id: 1
|
||||||
|
# tx_power: 50
|
||||||
|
# tx_freq: 453.0125
|
||||||
|
# band: uhf
|
||||||
|
|
||||||
|
# Location Information (Step 10) - optional
|
||||||
|
# latitude: 40.7128
|
||||||
|
# longitude: -74.0060
|
||||||
|
# location: "New York, NY"
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
#
|
||||||
|
# Digital Voice Modem - Host Configuration Generator
|
||||||
|
#
|
||||||
|
# Example answers file for trunked system configuration
|
||||||
|
# This file can be used with: dvmcfg wizard --type trunk -a trunked-answers.yml
|
||||||
|
# All fields are optional; prompts will appear for missing values
|
||||||
|
|
||||||
|
# System Configuration (Step 1)
|
||||||
|
system_name: trunked_test
|
||||||
|
base_dir: .
|
||||||
|
identity: TRUNKED001
|
||||||
|
protocol: p25
|
||||||
|
vc_count: 2
|
||||||
|
|
||||||
|
# Logging Configuration (Step 2)
|
||||||
|
configure_logging: true
|
||||||
|
log_path: /var/log/dvm
|
||||||
|
activity_log_path: /var/log/dvm/activity
|
||||||
|
log_root: TRUNKED
|
||||||
|
use_syslog: false
|
||||||
|
disable_non_auth_logging: false
|
||||||
|
|
||||||
|
# Network Settings (Step 3) - optional
|
||||||
|
# fne_address: 127.0.0.1
|
||||||
|
# fne_port: 62031
|
||||||
|
# fne_password: PASSWORD
|
||||||
|
|
||||||
|
# Control Channel Settings (Step 4) - optional
|
||||||
|
# base_peer_id: 100000
|
||||||
|
# base_rpc_port: 9000
|
||||||
|
# modem_type: uart
|
||||||
|
|
||||||
|
# Voice Channel Settings (Step 5) - optional
|
||||||
|
# cc_tx_freq: 453.0125
|
||||||
|
# cc_band: uhf
|
||||||
|
# vc_tx_freqs: "453.0375,453.0625"
|
||||||
|
# vc_bands: "uhf,uhf"
|
||||||
|
|
||||||
|
# Network/System IDs (Step 6) - optional
|
||||||
|
# For P25:
|
||||||
|
# p25_nac: 0x293
|
||||||
|
# p25_network_id: 1
|
||||||
|
# p25_system_id: 1
|
||||||
|
# p25_rfss_id: 1
|
||||||
|
# p25_site_id: 1
|
||||||
|
|
||||||
|
# For DMR:
|
||||||
|
# dmr_color_code: 1
|
||||||
|
# dmr_network_id: 1
|
||||||
|
# dmr_site_id: 1
|
||||||
|
|
||||||
|
# RPC Configuration (Step 6a) - optional
|
||||||
|
# generate_rpc_password: true
|
||||||
|
|
||||||
|
# Network Lookups (Step 6b) - optional
|
||||||
|
# update_lookups: true
|
||||||
|
# save_lookups: true
|
||||||
|
|
||||||
|
# File Transfer Settings (Step 6c) - optional
|
||||||
|
# allow_activity_transfer: true
|
||||||
|
# allow_diagnostic_transfer: true
|
||||||
|
# allow_status_transfer: true
|
||||||
|
|
||||||
|
# Radio ID File Settings (Step 7) - optional
|
||||||
|
# radio_id_file: /path/to/rid.csv
|
||||||
|
# radio_id_time: 3600
|
||||||
|
# radio_id_acl: blacklist
|
||||||
|
|
||||||
|
# Talkgroup ID File Settings (Step 8) - optional
|
||||||
|
# talkgroup_id_file: /path/to/talkgroups.csv
|
||||||
|
# talkgroup_id_time: 3600
|
||||||
|
# talkgroup_id_acl: whitelist
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue