""" Ansible Integration Module for Server_Monitorizare Provides functions to execute Ansible playbooks and commands on remote devices """ import os import json import subprocess import logging from datetime import datetime from pathlib import Path from typing import Dict, List, Any, Optional, Tuple logger = logging.getLogger(__name__) class AnsibleManager: """Manager class for Ansible operations""" def __init__(self, ansible_dir: str = None, inventory_file: str = None): """ Initialize Ansible manager Args: ansible_dir: Path to ansible directory inventory_file: Path to inventory file """ self.ansible_dir = ansible_dir or os.path.join( os.path.dirname(__file__), 'ansible' ) self.inventory_file = inventory_file or os.path.join( self.ansible_dir, 'inventory.ini' ) self.log_dir = os.path.join(self.ansible_dir, 'logs') # Create logs directory if it doesn't exist Path(self.log_dir).mkdir(exist_ok=True) def _verify_ansible_installed(self) -> bool: """Verify Ansible is installed""" try: result = subprocess.run( ['ansible', '--version'], capture_output=True, text=True, timeout=10 ) return result.returncode == 0 except Exception as e: logger.error(f"Ansible not found: {e}") return False def deploy_to_devices( self, target_hosts: str = 'prezenta_devices', git_branch: str = 'dev', backup: bool = True, restart: bool = True ) -> Dict[str, Any]: """ Deploy updates to devices using deploy.yml playbook Args: target_hosts: Target host group or specific host git_branch: Git branch to deploy backup: Whether to backup before deploy restart: Whether to restart service Returns: dict: Execution result with status and output """ if not self._verify_ansible_installed(): return { 'success': False, 'error': 'Ansible is not installed', 'timestamp': datetime.now().isoformat() } playbook_file = os.path.join(self.ansible_dir, 'deploy.yml') if not os.path.exists(playbook_file): return { 'success': False, 'error': f'Playbook not found: {playbook_file}', 'timestamp': datetime.now().isoformat() } # Prepare Ansible command cmd = [ 'ansible-playbook', '-i', self.inventory_file, playbook_file, '-e', f'git_branch={git_branch}', '-e', f'backup_before_deploy={str(backup).lower()}', '-e', f'restart_service={str(restart).lower()}', '--limit', target_hosts, '-v' ] logger.info(f"Executing deployment: {' '.join(cmd)}") try: log_file = os.path.join( self.log_dir, f'deploy_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log' ) with open(log_file, 'w') as log: result = subprocess.run( cmd, capture_output=True, text=True, timeout=600, # 10 minute timeout cwd=self.ansible_dir ) log.write(result.stdout) if result.stderr: log.write("\n--- STDERR ---\n") log.write(result.stderr) success = result.returncode == 0 return { 'success': success, 'returncode': result.returncode, 'output': result.stdout, 'error': result.stderr if not success else None, 'log_file': log_file, 'timestamp': datetime.now().isoformat(), 'target_hosts': target_hosts, 'git_branch': git_branch } except subprocess.TimeoutExpired: return { 'success': False, 'error': 'Deployment timed out after 10 minutes', 'timestamp': datetime.now().isoformat() } except Exception as e: logger.exception(f"Deployment failed: {e}") return { 'success': False, 'error': f'Deployment failed: {str(e)}', 'timestamp': datetime.now().isoformat() } def execute_command_on_device( self, device: str, command: str, user: str = 'pi' ) -> Dict[str, Any]: """ Execute command on specific device Args: device: Device hostname or IP command: Command to execute user: User to execute command as Returns: dict: Execution result """ if not self._verify_ansible_installed(): return { 'success': False, 'error': 'Ansible is not installed', 'timestamp': datetime.now().isoformat() } playbook_file = os.path.join(self.ansible_dir, 'commands.yml') if not os.path.exists(playbook_file): return { 'success': False, 'error': f'Playbook not found: {playbook_file}', 'timestamp': datetime.now().isoformat() } cmd = [ 'ansible-playbook', '-i', self.inventory_file, playbook_file, '--limit', device, '-e', f'command={command}', '-e', f'command_user={user}', '-v' ] logger.info(f"Executing command on {device}: {command}") try: log_file = os.path.join( self.log_dir, f'command_{device}_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log' ) with open(log_file, 'w') as log: result = subprocess.run( cmd, capture_output=True, text=True, timeout=60, cwd=self.ansible_dir ) log.write(result.stdout) if result.stderr: log.write("\n--- STDERR ---\n") log.write(result.stderr) success = result.returncode == 0 return { 'success': success, 'returncode': result.returncode, 'output': result.stdout, 'error': result.stderr if not success else None, 'log_file': log_file, 'timestamp': datetime.now().isoformat(), 'device': device, 'command': command } except subprocess.TimeoutExpired: return { 'success': False, 'error': 'Command execution timed out after 60 seconds', 'timestamp': datetime.now().isoformat() } except Exception as e: logger.exception(f"Command execution failed: {e}") return { 'success': False, 'error': f'Command execution failed: {str(e)}', 'timestamp': datetime.now().isoformat() } def system_update( self, target_hosts: str = 'prezenta_devices', update_os: bool = False, update_python: bool = True, health_check: bool = True ) -> Dict[str, Any]: """ Execute system update and maintenance Args: target_hosts: Target host group update_os: Whether to update OS packages update_python: Whether to update Python packages health_check: Whether to perform health check Returns: dict: Execution result """ if not self._verify_ansible_installed(): return { 'success': False, 'error': 'Ansible is not installed', 'timestamp': datetime.now().isoformat() } playbook_file = os.path.join(self.ansible_dir, 'system_update.yml') if not os.path.exists(playbook_file): return { 'success': False, 'error': f'Playbook not found: {playbook_file}', 'timestamp': datetime.now().isoformat() } cmd = [ 'ansible-playbook', '-i', self.inventory_file, playbook_file, '--limit', target_hosts, '-e', f'update_os_packages={str(update_os).lower()}', '-e', f'update_python_packages={str(update_python).lower()}', '-e', f'perform_health_check={str(health_check).lower()}', '-v' ] logger.info(f"Executing system update on {target_hosts}") try: log_file = os.path.join( self.log_dir, f'system_update_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log' ) with open(log_file, 'w') as log: result = subprocess.run( cmd, capture_output=True, text=True, timeout=1800, # 30 minute timeout cwd=self.ansible_dir ) log.write(result.stdout) if result.stderr: log.write("\n--- STDERR ---\n") log.write(result.stderr) success = result.returncode == 0 return { 'success': success, 'returncode': result.returncode, 'output': result.stdout, 'error': result.stderr if not success else None, 'log_file': log_file, 'timestamp': datetime.now().isoformat(), 'target_hosts': target_hosts } except subprocess.TimeoutExpired: return { 'success': False, 'error': 'System update timed out after 30 minutes', 'timestamp': datetime.now().isoformat() } except Exception as e: logger.exception(f"System update failed: {e}") return { 'success': False, 'error': f'System update failed: {str(e)}', 'timestamp': datetime.now().isoformat() } def get_device_facts(self, device: str) -> Dict[str, Any]: """ Get facts about a specific device Args: device: Device hostname or IP Returns: dict: Device facts and information """ if not self._verify_ansible_installed(): return { 'success': False, 'error': 'Ansible is not installed' } cmd = [ 'ansible', device, '-i', self.inventory_file, '-m', 'setup' ] try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=30, cwd=self.ansible_dir ) if result.returncode == 0: # Parse JSON output output_lines = result.stdout.split('\n') for line in output_lines: if line.startswith(device): try: data = json.loads(line.split(' => ', 1)[1]) return { 'success': True, 'device': device, 'facts': data.get('ansible_facts', {}), 'timestamp': datetime.now().isoformat() } except json.JSONDecodeError: pass return { 'success': False, 'error': f'Failed to get facts for {device}', 'timestamp': datetime.now().isoformat() } except Exception as e: logger.exception(f"Failed to get device facts: {e}") return { 'success': False, 'error': f'Failed to get device facts: {str(e)}', 'timestamp': datetime.now().isoformat() } def get_execution_logs(self, limit: int = 10) -> List[Dict[str, str]]: """Get recent execution logs""" logs = [] try: log_files = sorted( Path(self.log_dir).glob('*.log'), key=os.path.getmtime, reverse=True )[:limit] for log_file in log_files: logs.append({ 'filename': log_file.name, 'timestamp': datetime.fromtimestamp(log_file.stat().st_mtime).isoformat(), 'size': log_file.stat().st_size }) except Exception as e: logger.error(f"Failed to get execution logs: {e}") return logs # Global instance ansible_manager = None def get_ansible_manager(ansible_dir: str = None) -> AnsibleManager: """Get or create global Ansible manager instance""" global ansible_manager if ansible_manager is None: ansible_manager = AnsibleManager(ansible_dir) return ansible_manager