config_validator.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936
  1. """
  2. Configuration Validation Module for Trixy Application
  3. This module provides comprehensive validation for configuration data across all
  4. deployment modes. It includes:
  5. - Schema validation
  6. - Cross-field validation
  7. - System resource validation
  8. - Path and file validation
  9. - Network port validation
  10. - Security validation
  11. Validation results include detailed error messages and warnings to help
  12. administrators correct configuration issues.
  13. """
  14. import os
  15. import socket
  16. import subprocess
  17. import platform
  18. from pathlib import Path
  19. from typing import Dict, List, Optional, Union, Any, Tuple
  20. import re
  21. import ipaddress
  22. from dataclasses import dataclass
  23. from .config_models import (
  24. ServerConfig, ClientConfig, StandaloneConfig, ConfigType,
  25. NetworkConfig, AudioConfig, MLConfig, PluginConfig,
  26. SatelliteConfig, SecurityConfig, LoggingConfig,
  27. DeploymentMode, LogLevel
  28. )
  29. @dataclass
  30. class ValidationResult:
  31. """Result of configuration validation."""
  32. is_valid: bool
  33. errors: List[str]
  34. warnings: List[str]
  35. info: List[str]
  36. def add_error(self, message: str) -> None:
  37. """Add an error message."""
  38. self.errors.append(message)
  39. self.is_valid = False
  40. def add_warning(self, message: str) -> None:
  41. """Add a warning message."""
  42. self.warnings.append(message)
  43. def add_info(self, message: str) -> None:
  44. """Add an info message."""
  45. self.info.append(message)
  46. def merge(self, other: 'ValidationResult') -> None:
  47. """Merge another validation result."""
  48. self.errors.extend(other.errors)
  49. self.warnings.extend(other.warnings)
  50. self.info.extend(other.info)
  51. if not other.is_valid:
  52. self.is_valid = False
  53. def has_issues(self) -> bool:
  54. """Check if there are any errors or warnings."""
  55. return bool(self.errors or self.warnings)
  56. def get_summary(self) -> str:
  57. """Get a summary of validation results."""
  58. parts = []
  59. if self.errors:
  60. parts.append(f"{len(self.errors)} errors")
  61. if self.warnings:
  62. parts.append(f"{len(self.warnings)} warnings")
  63. if self.info:
  64. parts.append(f"{len(self.info)} info messages")
  65. if not parts:
  66. return "Configuration valid"
  67. return f"Configuration validation: {', '.join(parts)}"
  68. class ConfigValidator:
  69. """Comprehensive configuration validator."""
  70. def __init__(self, strict_mode: bool = False):
  71. """
  72. Initialize the validator.
  73. Args:
  74. strict_mode: If True, warnings are treated as errors
  75. """
  76. self.strict_mode = strict_mode
  77. self._system_info = self._get_system_info()
  78. def _get_system_info(self) -> Dict[str, Any]:
  79. """Get system information for validation."""
  80. info = {
  81. "platform": platform.system().lower(),
  82. "available_ports": self._get_available_ports(),
  83. "has_gpu": self._check_gpu_availability(),
  84. "audio_devices": self._get_audio_devices(),
  85. }
  86. return info
  87. def _get_available_ports(self) -> List[int]:
  88. """Get list of available ports."""
  89. available = []
  90. for port in range(1024, 65536):
  91. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  92. try:
  93. sock.bind(('localhost', port))
  94. available.append(port)
  95. sock.close()
  96. except OSError:
  97. pass
  98. if len(available) >= 100: # Limit check for performance
  99. break
  100. return available
  101. def _check_gpu_availability(self) -> bool:
  102. """Check if GPU is available for ML processing."""
  103. try:
  104. # Check for NVIDIA GPU
  105. result = subprocess.run(['nvidia-smi'], capture_output=True, text=True)
  106. if result.returncode == 0:
  107. return True
  108. except FileNotFoundError:
  109. pass
  110. try:
  111. # Check for AMD GPU (ROCm)
  112. result = subprocess.run(['rocm-smi'], capture_output=True, text=True)
  113. if result.returncode == 0:
  114. return True
  115. except FileNotFoundError:
  116. pass
  117. return False
  118. def _get_audio_devices(self) -> Dict[str, List[str]]:
  119. """Get available audio devices."""
  120. devices = {"input": [], "output": []}
  121. # This is a placeholder - actual implementation would use system-specific
  122. # audio enumeration (ALSA, PulseAudio, WASAPI, CoreAudio)
  123. try:
  124. if self._system_info["platform"] == "linux":
  125. # Check ALSA devices
  126. devices["input"] = ["default", "hw:0,0"]
  127. devices["output"] = ["default", "hw:0,0"]
  128. elif self._system_info["platform"] == "windows":
  129. # Check WASAPI devices
  130. devices["input"] = ["default"]
  131. devices["output"] = ["default"]
  132. elif self._system_info["platform"] == "darwin":
  133. # Check CoreAudio devices
  134. devices["input"] = ["default"]
  135. devices["output"] = ["default"]
  136. except Exception:
  137. pass
  138. return devices
  139. def validate_config(self, config: ConfigType) -> ValidationResult:
  140. """Validate a complete configuration."""
  141. result = ValidationResult(True, [], [], [])
  142. # Validate based on config type
  143. if isinstance(config, ServerConfig):
  144. result.merge(self.validate_server_config(config))
  145. elif isinstance(config, ClientConfig):
  146. result.merge(self.validate_client_config(config))
  147. elif isinstance(config, StandaloneConfig):
  148. result.merge(self.validate_standalone_config(config))
  149. else:
  150. result.add_error(f"Unknown configuration type: {type(config)}")
  151. return result
  152. def validate_server_config(self, config: ServerConfig) -> ValidationResult:
  153. """Validate server configuration."""
  154. result = ValidationResult(True, [], [], [])
  155. # Validate base configuration
  156. result.merge(self._validate_base_config(config))
  157. # Validate component configurations
  158. result.merge(self.validate_network_config(config.network))
  159. result.merge(self.validate_audio_config(config.audio))
  160. result.merge(self.validate_ml_config(config.ml))
  161. result.merge(self.validate_plugin_config(config.plugins))
  162. result.merge(self.validate_security_config(config.security))
  163. result.merge(self.validate_logging_config(config.logging))
  164. # Server-specific validations
  165. if config.max_satellites <= 0:
  166. result.add_error("Max satellites must be positive")
  167. elif config.max_satellites > 1000:
  168. result.add_warning("Very high max satellites count may impact performance")
  169. if config.satellite_heartbeat_timeout <= 0:
  170. result.add_error("Satellite heartbeat timeout must be positive")
  171. elif config.satellite_heartbeat_timeout < 10:
  172. result.add_warning("Very low heartbeat timeout may cause unnecessary disconnections")
  173. if config.conversation_timeout <= 0:
  174. result.add_error("Conversation timeout must be positive")
  175. if config.max_concurrent_conversations <= 0:
  176. result.add_error("Max concurrent conversations must be positive")
  177. elif config.max_concurrent_conversations > config.max_satellites:
  178. result.add_warning("Max concurrent conversations exceeds max satellites")
  179. # Web interface validation
  180. if config.enable_web_interface:
  181. if not (1024 <= config.web_interface_port <= 65535):
  182. result.add_error("Web interface port must be between 1024 and 65535")
  183. else:
  184. # Check for port conflicts
  185. network_ports = [
  186. config.network.command_port,
  187. config.network.raw_audio_input_port,
  188. config.network.raw_audio_output_port,
  189. config.network.raw_music_output_port
  190. ]
  191. if config.web_interface_port in network_ports:
  192. result.add_error("Web interface port conflicts with network ports")
  193. return result
  194. def validate_client_config(self, config: ClientConfig) -> ValidationResult:
  195. """Validate client configuration."""
  196. result = ValidationResult(True, [], [], [])
  197. # Validate base configuration
  198. result.merge(self._validate_base_config(config))
  199. # Validate component configurations
  200. result.merge(self.validate_satellite_config(config.satellite))
  201. result.merge(self.validate_audio_config(config.audio))
  202. result.merge(self.validate_ml_config(config.ml))
  203. result.merge(self.validate_logging_config(config.logging))
  204. # Client-specific validations
  205. if config.local_model_cache_size <= 0:
  206. result.add_error("Local model cache size must be positive")
  207. if config.audio_buffer_size <= 0:
  208. result.add_error("Audio buffer size must be positive")
  209. elif not (config.audio_buffer_size & (config.audio_buffer_size - 1)) == 0:
  210. result.add_warning("Audio buffer size should be a power of 2 for optimal performance")
  211. if config.status_report_interval <= 0:
  212. result.add_error("Status report interval must be positive")
  213. elif config.status_report_interval < 5:
  214. result.add_warning("Very frequent status reports may impact network performance")
  215. if config.health_check_interval <= 0:
  216. result.add_error("Health check interval must be positive")
  217. elif config.health_check_interval > config.status_report_interval:
  218. result.add_warning("Health check interval should be less than or equal to status report interval")
  219. return result
  220. def validate_standalone_config(self, config: StandaloneConfig) -> ValidationResult:
  221. """Validate standalone configuration."""
  222. result = ValidationResult(True, [], [], [])
  223. # Validate base configuration
  224. result.merge(self._validate_base_config(config))
  225. # Validate component configurations (same as server)
  226. result.merge(self.validate_network_config(config.network))
  227. result.merge(self.validate_audio_config(config.audio))
  228. result.merge(self.validate_ml_config(config.ml))
  229. result.merge(self.validate_plugin_config(config.plugins))
  230. result.merge(self.validate_security_config(config.security))
  231. result.merge(self.validate_logging_config(config.logging))
  232. # Standalone-specific validations
  233. if config.enable_server_sync:
  234. if not config.sync_server_host:
  235. result.add_error("Sync server host required when server sync is enabled")
  236. else:
  237. result.merge(self._validate_host(config.sync_server_host, "sync_server_host"))
  238. if not config.sync_server_port:
  239. result.add_error("Sync server port required when server sync is enabled")
  240. elif not (1024 <= config.sync_server_port <= 65535):
  241. result.add_error("Sync server port must be between 1024 and 65535")
  242. if config.sync_interval <= 0:
  243. result.add_error("Sync interval must be positive")
  244. elif config.sync_interval < 60:
  245. result.add_warning("Very frequent sync intervals may impact performance")
  246. # Same server validations apply
  247. result.merge(self.validate_server_config(config))
  248. return result
  249. def _validate_base_config(self, config: ConfigType) -> ValidationResult:
  250. """Validate base configuration fields."""
  251. result = ValidationResult(True, [], [], [])
  252. # Validate data directory
  253. data_dir = Path(config.data_directory)
  254. if not data_dir.is_absolute():
  255. result.add_info(f"Using relative data directory: {config.data_directory}")
  256. if not data_dir.exists():
  257. result.add_warning(f"Data directory does not exist: {config.data_directory}")
  258. elif not data_dir.is_dir():
  259. result.add_error(f"Data directory is not a directory: {config.data_directory}")
  260. # Validate config file path
  261. if config.config_file:
  262. config_file = Path(config.config_file)
  263. if not config_file.parent.exists():
  264. result.add_warning(f"Config file parent directory does not exist: {config_file.parent}")
  265. return result
  266. def validate_network_config(self, config: NetworkConfig) -> ValidationResult:
  267. """Validate network configuration."""
  268. result = ValidationResult(True, [], [], [])
  269. # Port validation
  270. ports = [
  271. ("command_port", config.command_port),
  272. ("raw_audio_input_port", config.raw_audio_input_port),
  273. ("raw_audio_output_port", config.raw_audio_output_port),
  274. ("raw_music_output_port", config.raw_music_output_port),
  275. ]
  276. for port_name, port_value in ports:
  277. if not (1024 <= port_value <= 65535):
  278. result.add_error(f"{port_name} must be between 1024 and 65535, got {port_value}")
  279. # Check for port uniqueness
  280. port_values = [p[1] for p in ports]
  281. if len(set(port_values)) != len(port_values):
  282. result.add_error("All network ports must be unique")
  283. # Validate bind addresses
  284. for addr_name, addr_value in [
  285. ("command_bind_address", config.command_bind_address),
  286. ("audio_bind_address", config.audio_bind_address),
  287. ]:
  288. result.merge(self._validate_bind_address(addr_value, addr_name))
  289. # Timeout and buffer validations
  290. if config.command_timeout <= 0:
  291. result.add_error("Command timeout must be positive")
  292. elif config.command_timeout > 300:
  293. result.add_warning("Very high command timeout may cause poor user experience")
  294. if config.audio_timeout <= 0:
  295. result.add_error("Audio timeout must be positive")
  296. if config.command_max_connections <= 0:
  297. result.add_error("Max connections must be positive")
  298. elif config.command_max_connections > 1000:
  299. result.add_warning("Very high max connections may impact system resources")
  300. # Buffer size validation
  301. for buffer_name, buffer_size in [
  302. ("command_buffer_size", config.command_buffer_size),
  303. ("audio_buffer_size", config.audio_buffer_size),
  304. ]:
  305. if buffer_size <= 0:
  306. result.add_error(f"{buffer_name} must be positive")
  307. elif not (buffer_size & (buffer_size - 1)) == 0:
  308. result.add_warning(f"{buffer_name} should be a power of 2 for optimal performance")
  309. # Connection retry validation
  310. if config.max_retry_attempts < 0:
  311. result.add_error("Max retry attempts must be non-negative")
  312. elif config.max_retry_attempts > 100:
  313. result.add_warning("Very high retry attempts may cause long delays")
  314. if config.retry_delay <= 0:
  315. result.add_error("Retry delay must be positive")
  316. if config.connection_timeout <= 0:
  317. result.add_error("Connection timeout must be positive")
  318. if config.heartbeat_interval <= 0:
  319. result.add_error("Heartbeat interval must be positive")
  320. elif config.heartbeat_interval > 300:
  321. result.add_warning("Very long heartbeat intervals may delay disconnect detection")
  322. # Compression validation
  323. if config.enable_compression:
  324. if not (1 <= config.compression_level <= 9):
  325. result.add_error("Compression level must be between 1 and 9")
  326. # Encryption validation
  327. if config.enable_encryption and config.encryption_key_file:
  328. key_file = Path(config.encryption_key_file)
  329. if not key_file.exists():
  330. result.add_error(f"Encryption key file not found: {config.encryption_key_file}")
  331. elif not key_file.is_file():
  332. result.add_error(f"Encryption key file is not a file: {config.encryption_key_file}")
  333. return result
  334. def _validate_bind_address(self, address: str, field_name: str) -> ValidationResult:
  335. """Validate a bind address."""
  336. result = ValidationResult(True, [], [], [])
  337. if not address:
  338. result.add_error(f"{field_name} cannot be empty")
  339. return result
  340. # Special addresses
  341. if address in ["0.0.0.0", "::", "localhost", "127.0.0.1", "::1"]:
  342. return result
  343. # Try to parse as IP address
  344. try:
  345. ip = ipaddress.ip_address(address)
  346. if ip.is_private:
  347. result.add_info(f"{field_name} uses private IP address: {address}")
  348. elif ip.is_loopback:
  349. result.add_info(f"{field_name} uses loopback address: {address}")
  350. return result
  351. except ValueError:
  352. pass
  353. # Try as hostname
  354. try:
  355. socket.gethostbyname(address)
  356. result.add_info(f"{field_name} uses hostname: {address}")
  357. except socket.gaierror:
  358. result.add_warning(f"{field_name} hostname cannot be resolved: {address}")
  359. return result
  360. def _validate_host(self, host: str, field_name: str) -> ValidationResult:
  361. """Validate a host address."""
  362. result = ValidationResult(True, [], [], [])
  363. if not host:
  364. result.add_error(f"{field_name} cannot be empty")
  365. return result
  366. # Try to resolve hostname
  367. try:
  368. socket.gethostbyname(host)
  369. except socket.gaierror:
  370. result.add_warning(f"{field_name} hostname cannot be resolved: {host}")
  371. return result
  372. def validate_audio_config(self, config: AudioConfig) -> ValidationResult:
  373. """Validate audio configuration."""
  374. result = ValidationResult(True, [], [], [])
  375. # Sample rate validation
  376. valid_sample_rates = [8000, 16000, 22050, 44100, 48000]
  377. if config.sample_rate not in valid_sample_rates:
  378. result.add_error(f"Sample rate must be one of {valid_sample_rates}, got {config.sample_rate}")
  379. if config.music_sample_rate not in valid_sample_rates:
  380. result.add_error(f"Music sample rate must be one of {valid_sample_rates}, got {config.music_sample_rate}")
  381. # Channels validation
  382. if config.channels not in [1, 2]:
  383. result.add_error(f"Channels must be 1 or 2, got {config.channels}")
  384. if config.music_channels not in [1, 2]:
  385. result.add_error(f"Music channels must be 1 or 2, got {config.music_channels}")
  386. # Bit depth validation
  387. valid_bit_depths = [16, 24, 32]
  388. if config.bit_depth not in valid_bit_depths:
  389. result.add_error(f"Bit depth must be one of {valid_bit_depths}, got {config.bit_depth}")
  390. if config.music_bit_depth not in valid_bit_depths:
  391. result.add_error(f"Music bit depth must be one of {valid_bit_depths}, got {config.music_bit_depth}")
  392. # Frame size validation
  393. if config.frame_size <= 0:
  394. result.add_error("Frame size must be positive")
  395. elif not (config.frame_size & (config.frame_size - 1)) == 0:
  396. result.add_warning("Frame size should be a power of 2 for optimal performance")
  397. # Recording duration validation
  398. if config.max_recording_duration <= 0:
  399. result.add_error("Max recording duration must be positive")
  400. elif config.max_recording_duration > 600: # 10 minutes
  401. result.add_warning("Very long max recording duration may use excessive resources")
  402. # Threshold validation
  403. if not (0.0 <= config.silence_threshold <= 1.0):
  404. result.add_error(f"Silence threshold must be between 0.0 and 1.0, got {config.silence_threshold}")
  405. if config.silence_duration <= 0:
  406. result.add_error("Silence duration must be positive")
  407. elif config.silence_duration > 10:
  408. result.add_warning("Very long silence duration may cause poor user experience")
  409. if config.pre_recording_buffer < 0:
  410. result.add_error("Pre-recording buffer must be non-negative")
  411. elif config.pre_recording_buffer > 5:
  412. result.add_warning("Very long pre-recording buffer may use excessive memory")
  413. # Gain validation
  414. if config.gain_level < 0:
  415. result.add_error("Gain level must be non-negative")
  416. elif config.gain_level > 10:
  417. result.add_warning("Very high gain level may cause audio distortion")
  418. # VAD validation
  419. if config.vad_enabled:
  420. if not (0 <= config.vad_aggressiveness <= 3):
  421. result.add_error(f"VAD aggressiveness must be between 0 and 3, got {config.vad_aggressiveness}")
  422. if config.vad_frame_duration not in [10, 20, 30]:
  423. result.add_error(f"VAD frame duration must be 10, 20, or 30 ms, got {config.vad_frame_duration}")
  424. return result
  425. def validate_ml_config(self, config: MLConfig) -> ValidationResult:
  426. """Validate ML configuration."""
  427. result = ValidationResult(True, [], [], [])
  428. # Model paths validation
  429. # Wakeword model is required
  430. if not config.wakeword_model_path:
  431. result.add_error("wakeword_model_path cannot be empty")
  432. else:
  433. path = Path(config.wakeword_model_path)
  434. if not path.exists():
  435. result.add_warning(f"Model file not found: {config.wakeword_model_path}")
  436. elif not path.is_file():
  437. result.add_error(f"Model path is not a file: {config.wakeword_model_path}")
  438. elif path.suffix not in ['.pth', '.pt', '.onnx']:
  439. result.add_warning(f"Unexpected model file extension: {path.suffix}")
  440. # Voice recognition model is optional - only validate if provided
  441. if config.voice_recognition_model_path and config.voice_recognition_model_path.strip():
  442. path = Path(config.voice_recognition_model_path)
  443. if not path.exists():
  444. result.add_warning(f"Model file not found: {config.voice_recognition_model_path}")
  445. elif not path.is_file():
  446. result.add_error(f"Model path is not a file: {config.voice_recognition_model_path}")
  447. elif path.suffix not in ['.pth', '.pt', '.onnx']:
  448. result.add_warning(f"Unexpected model file extension: {path.suffix}")
  449. # Threshold validation
  450. if not (0.0 <= config.wakeword_threshold <= 1.0):
  451. result.add_error(f"Wakeword threshold must be between 0.0 and 1.0, got {config.wakeword_threshold}")
  452. if not (0.0 <= config.voice_recognition_threshold <= 1.0):
  453. result.add_error(f"Voice recognition threshold must be between 0.0 and 1.0, got {config.voice_recognition_threshold}")
  454. # Cache and performance validation
  455. if config.model_cache_size <= 0:
  456. result.add_error("Model cache size must be positive")
  457. elif config.model_cache_size > 100:
  458. result.add_warning("Very large model cache may use excessive memory")
  459. if config.batch_size <= 0:
  460. result.add_error("Batch size must be positive")
  461. elif config.batch_size > 64:
  462. result.add_warning("Very large batch size may use excessive memory")
  463. if config.max_inference_time <= 0:
  464. result.add_error("Max inference time must be positive")
  465. elif config.max_inference_time > 30:
  466. result.add_warning("Very long max inference time may cause poor user experience")
  467. # GPU validation
  468. if config.enable_gpu:
  469. if not self._system_info.get("has_gpu", False):
  470. result.add_warning("GPU acceleration requested but no GPU detected")
  471. if config.gpu_device_id < 0:
  472. result.add_error("GPU device ID must be non-negative")
  473. # Training configuration validation
  474. for path_name, path_value in [
  475. ("training_data_path", config.training_data_path),
  476. ("model_output_path", config.model_output_path),
  477. ]:
  478. if not path_value:
  479. result.add_error(f"{path_name} cannot be empty")
  480. continue
  481. path = Path(path_value)
  482. if path.exists() and not path.is_dir():
  483. result.add_error(f"{path_name} exists but is not a directory: {path_value}")
  484. if config.training_batch_size <= 0:
  485. result.add_error("Training batch size must be positive")
  486. elif config.training_batch_size > 512:
  487. result.add_warning("Very large training batch size may use excessive memory")
  488. if config.training_epochs <= 0:
  489. result.add_error("Training epochs must be positive")
  490. elif config.training_epochs > 10000:
  491. result.add_warning("Very high training epochs count may take excessive time")
  492. if config.learning_rate <= 0:
  493. result.add_error("Learning rate must be positive")
  494. elif config.learning_rate > 1.0:
  495. result.add_warning("Very high learning rate may prevent model convergence")
  496. elif config.learning_rate < 1e-6:
  497. result.add_warning("Very low learning rate may slow training significantly")
  498. return result
  499. def validate_plugin_config(self, config: PluginConfig) -> ValidationResult:
  500. """Validate plugin configuration."""
  501. result = ValidationResult(True, [], [], [])
  502. # Plugin directories validation
  503. if not config.plugin_directories:
  504. result.add_error("At least one plugin directory must be specified")
  505. else:
  506. for plugin_dir in config.plugin_directories:
  507. path = Path(plugin_dir)
  508. if not path.exists():
  509. result.add_warning(f"Plugin directory does not exist: {plugin_dir}")
  510. elif not path.is_dir():
  511. result.add_error(f"Plugin directory is not a directory: {plugin_dir}")
  512. # Timeout and resource validation
  513. if config.plugin_timeout <= 0:
  514. result.add_error("Plugin timeout must be positive")
  515. elif config.plugin_timeout > 300:
  516. result.add_warning("Very high plugin timeout may cause poor user experience")
  517. if config.max_plugin_memory <= 0:
  518. result.add_error("Max plugin memory must be positive")
  519. elif config.max_plugin_memory > 8192: # 8GB
  520. result.add_warning("Very high plugin memory limit may use excessive resources")
  521. # Hot reload validation
  522. if config.enable_hot_reload:
  523. if config.reload_check_interval <= 0:
  524. result.add_error("Reload check interval must be positive")
  525. elif config.reload_check_interval < 1:
  526. result.add_warning("Very frequent reload checks may impact performance")
  527. if config.max_reload_attempts <= 0:
  528. result.add_error("Max reload attempts must be positive")
  529. elif config.max_reload_attempts > 10:
  530. result.add_warning("Very high max reload attempts may cause long delays on failures")
  531. # API validation
  532. if not config.allowed_plugin_apis:
  533. result.add_warning("No plugin APIs are allowed - plugins may not function properly")
  534. return result
  535. def validate_satellite_config(self, config: SatelliteConfig) -> ValidationResult:
  536. """Validate satellite configuration."""
  537. result = ValidationResult(True, [], [], [])
  538. # Server connection validation
  539. if not config.server_host:
  540. result.add_error("Server host cannot be empty")
  541. else:
  542. result.merge(self._validate_host(config.server_host, "server_host"))
  543. if not (1024 <= config.server_port <= 65535):
  544. result.add_error(f"Server port must be between 1024 and 65535, got {config.server_port}")
  545. if config.reconnect_interval <= 0:
  546. result.add_error("Reconnect interval must be positive")
  547. elif config.reconnect_interval < 1:
  548. result.add_warning("Very frequent reconnect attempts may impact server performance")
  549. # Identification validation
  550. if not config.room_name:
  551. result.add_error("Room name cannot be empty")
  552. elif not re.match(r'^[a-zA-Z0-9_-]+$', config.room_name):
  553. result.add_warning("Room name should contain only alphanumeric characters, underscores, and hyphens")
  554. if not config.alias_name:
  555. result.add_error("Alias name cannot be empty")
  556. elif not re.match(r'^[a-zA-Z0-9_-]+$', config.alias_name):
  557. result.add_warning("Alias name should contain only alphanumeric characters, underscores, and hyphens")
  558. # MAC address validation (if provided)
  559. if config.mac_address:
  560. mac_pattern = r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$'
  561. if not re.match(mac_pattern, config.mac_address):
  562. result.add_error(f"Invalid MAC address format: {config.mac_address}")
  563. # Stream quality validation
  564. if config.stream_quality not in ["low", "medium", "high"]:
  565. result.add_error(f"Stream quality must be 'low', 'medium', or 'high', got {config.stream_quality}")
  566. return result
  567. def validate_security_config(self, config: SecurityConfig) -> ValidationResult:
  568. """Validate security configuration."""
  569. result = ValidationResult(True, [], [], [])
  570. # Encryption validation
  571. if config.enable_encryption:
  572. if config.encryption_algorithm not in ["AES-256-GCM", "AES-256-CBC", "ChaCha20-Poly1305"]:
  573. result.add_warning(f"Unrecognized encryption algorithm: {config.encryption_algorithm}")
  574. if config.key_rotation_interval <= 0:
  575. result.add_error("Key rotation interval must be positive")
  576. elif config.key_rotation_interval < 3600: # 1 hour
  577. result.add_warning("Very frequent key rotation may impact performance")
  578. # Authentication validation
  579. if config.require_authentication:
  580. if config.auth_token_expiry <= 0:
  581. result.add_error("Auth token expiry must be positive")
  582. elif config.auth_token_expiry < 300: # 5 minutes
  583. result.add_warning("Very short auth token expiry may cause frequent re-authentication")
  584. if config.max_auth_attempts <= 0:
  585. result.add_error("Max auth attempts must be positive")
  586. elif config.max_auth_attempts > 10:
  587. result.add_warning("Very high max auth attempts may aid brute force attacks")
  588. if config.auth_lockout_duration <= 0:
  589. result.add_error("Auth lockout duration must be positive")
  590. # Access control validation
  591. if config.max_connections_per_client <= 0:
  592. result.add_error("Max connections per client must be positive")
  593. elif config.max_connections_per_client > 100:
  594. result.add_warning("Very high max connections per client may enable resource exhaustion")
  595. # Client list validation
  596. allowed_set = set(config.allowed_clients)
  597. blocked_set = set(config.blocked_clients)
  598. conflicts = allowed_set & blocked_set
  599. if conflicts:
  600. result.add_error(f"Clients cannot be both allowed and blocked: {', '.join(conflicts)}")
  601. # Audit logging validation
  602. if config.enable_audit_logging:
  603. if not config.audit_log_file:
  604. result.add_error("Audit log file path cannot be empty when audit logging is enabled")
  605. else:
  606. log_path = Path(config.audit_log_file)
  607. if not log_path.parent.exists():
  608. result.add_warning(f"Audit log directory does not exist: {log_path.parent}")
  609. return result
  610. def validate_logging_config(self, config: LoggingConfig) -> ValidationResult:
  611. """Validate logging configuration."""
  612. result = ValidationResult(True, [], [], [])
  613. # Log level validation is handled by enum
  614. # File logging validation
  615. if config.enable_file_logging:
  616. if not config.log_file_path:
  617. result.add_error("Log file path cannot be empty when file logging is enabled")
  618. else:
  619. log_path = Path(config.log_file_path)
  620. if not log_path.parent.exists():
  621. result.add_warning(f"Log file directory does not exist: {log_path.parent}")
  622. if config.max_log_file_size <= 0:
  623. result.add_error("Max log file size must be positive")
  624. elif config.max_log_file_size < 1024: # 1KB
  625. result.add_warning("Very small max log file size may cause excessive rotation")
  626. elif config.max_log_file_size > 1073741824: # 1GB
  627. result.add_warning("Very large max log file size may use excessive disk space")
  628. if config.log_file_backup_count < 0:
  629. result.add_error("Log file backup count must be non-negative")
  630. elif config.log_file_backup_count > 100:
  631. result.add_warning("Very high backup count may use excessive disk space")
  632. # Specialized logging validation
  633. if config.enable_event_logging:
  634. if not config.event_log_file:
  635. result.add_error("Event log file path cannot be empty when event logging is enabled")
  636. else:
  637. event_log_path = Path(config.event_log_file)
  638. if not event_log_path.parent.exists():
  639. result.add_warning(f"Event log directory does not exist: {event_log_path.parent}")
  640. if config.enable_performance_logging:
  641. if not config.performance_log_file:
  642. result.add_error("Performance log file path cannot be empty when performance logging is enabled")
  643. else:
  644. perf_log_path = Path(config.performance_log_file)
  645. if not perf_log_path.parent.exists():
  646. result.add_warning(f"Performance log directory does not exist: {perf_log_path.parent}")
  647. return result
  648. def validate_file_permissions(self, config: ConfigType) -> ValidationResult:
  649. """Validate file and directory permissions."""
  650. result = ValidationResult(True, [], [], [])
  651. # Check data directory permissions
  652. data_path = Path(config.data_directory)
  653. if data_path.exists():
  654. if not os.access(data_path, os.R_OK | os.W_OK):
  655. result.add_error(f"Insufficient permissions for data directory: {config.data_directory}")
  656. # Check log directory permissions (if applicable)
  657. if hasattr(config, 'logging'):
  658. log_path = Path(config.logging.log_file_path).parent
  659. if log_path.exists():
  660. if not os.access(log_path, os.R_OK | os.W_OK):
  661. result.add_error(f"Insufficient permissions for log directory: {log_path}")
  662. return result
  663. def validate_system_resources(self, config: ConfigType) -> ValidationResult:
  664. """Validate system resource requirements."""
  665. result = ValidationResult(True, [], [], [])
  666. # Check available disk space
  667. try:
  668. data_path = Path(config.data_directory)
  669. if data_path.exists():
  670. stat = os.statvfs(data_path)
  671. available_space = stat.f_bavail * stat.f_frsize # Available space in bytes
  672. if available_space < 1073741824: # Less than 1GB
  673. result.add_warning(f"Low disk space available: {available_space / 1073741824:.1f} GB")
  674. elif available_space < 104857600: # Less than 100MB
  675. result.add_error(f"Critically low disk space: {available_space / 1048576:.1f} MB")
  676. except (OSError, AttributeError):
  677. # os.statvfs not available on Windows
  678. pass
  679. # Check memory usage estimates
  680. if isinstance(config, (ServerConfig, StandaloneConfig)):
  681. estimated_memory = self._estimate_memory_usage(config)
  682. result.add_info(f"Estimated memory usage: {estimated_memory:.1f} MB")
  683. if estimated_memory > 8192: # 8GB
  684. result.add_warning("Configuration may require significant memory resources")
  685. return result
  686. def _estimate_memory_usage(self, config: Union[ServerConfig, StandaloneConfig]) -> float:
  687. """Estimate memory usage for server/standalone configurations."""
  688. # Base memory usage
  689. base_memory = 256 # MB
  690. # Satellite connections
  691. satellite_memory = config.max_satellites * 5 # ~5MB per satellite
  692. # Audio processing
  693. audio_memory = config.audio.frame_size * config.audio.channels * 4 * 10 # ~10 frames in memory
  694. audio_memory = audio_memory / (1024 * 1024) # Convert to MB
  695. # Model caching
  696. if hasattr(config, 'ml'):
  697. model_memory = config.ml.model_cache_size * 50 # ~50MB per cached model
  698. else:
  699. model_memory = 0
  700. # Plugin memory
  701. if hasattr(config, 'plugins'):
  702. plugin_memory = len(config.plugins.plugin_directories) * config.plugins.max_plugin_memory
  703. else:
  704. plugin_memory = 0
  705. return base_memory + satellite_memory + audio_memory + model_memory + plugin_memory
  706. def validate_config_dict(config_dict: Dict[str, Any], mode: DeploymentMode) -> ValidationResult:
  707. """Validate a configuration dictionary before creating config object."""
  708. result = ValidationResult(True, [], [], [])
  709. # Import here to avoid circular imports
  710. from .config_models import CONFIG_CLASSES
  711. try:
  712. config_class = CONFIG_CLASSES[mode]
  713. # Try to create the config object to catch any data validation errors
  714. config = config_class.from_dict(config_dict)
  715. # Validate the created config
  716. validator = ConfigValidator()
  717. result.merge(validator.validate_config(config))
  718. except Exception as e:
  719. result.add_error(f"Failed to create {mode.value} configuration: {str(e)}")
  720. return result
  721. def quick_validate(config_file: str) -> ValidationResult:
  722. """Quickly validate a configuration file."""
  723. result = ValidationResult(True, [], [], [])
  724. config_path = Path(config_file)
  725. if not config_path.exists():
  726. result.add_error(f"Configuration file not found: {config_file}")
  727. return result
  728. if not config_path.is_file():
  729. result.add_error(f"Configuration path is not a file: {config_file}")
  730. return result
  731. try:
  732. import json
  733. with open(config_path, 'r') as f:
  734. config_data = json.load(f)
  735. # Determine mode from filename or config data
  736. filename = config_path.name.lower()
  737. if 'server' in filename:
  738. mode = DeploymentMode.SERVER
  739. elif 'client' in filename:
  740. mode = DeploymentMode.CLIENT
  741. elif 'standalone' in filename:
  742. mode = DeploymentMode.STANDALONE
  743. elif 'mode' in config_data:
  744. mode = DeploymentMode(config_data['mode'])
  745. else:
  746. result.add_error("Cannot determine deployment mode from filename or config data")
  747. return result
  748. result.merge(validate_config_dict(config_data, mode))
  749. except json.JSONDecodeError as e:
  750. result.add_error(f"Invalid JSON in configuration file: {str(e)}")
  751. except Exception as e:
  752. result.add_error(f"Error validating configuration file: {str(e)}")
  753. return result