Add Ansible integration for device management and deployment automation
- 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
This commit is contained in:
425
ansible_integration.py
Normal file
425
ansible_integration.py
Normal file
@@ -0,0 +1,425 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user