- Added ansible/ directory with playbooks for: * deploy.yml: Update applications on devices from git * commands.yml: Execute arbitrary commands on devices * system_update.yml: OS updates and health checks * inventory.ini: Device and group configuration * README.md: Comprehensive Ansible guide * requirements.txt: Installation instructions - Added ansible_integration.py: Python module wrapping Ansible operations - Added utils_ansible.py: Updated utilities using Ansible instead of HTTP commands Key benefits: - Idempotent operations with error recovery - Comprehensive logging and backup - Multi-device orchestration - Better reliability and control - Replaces unreliable direct HTTP command execution
426 lines
14 KiB
Python
426 lines
14 KiB
Python
"""
|
|
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
|