Files
Server_Monitorizare/utils_ansible.py
Developer cb52e67afa 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
2025-12-18 13:59:48 +02:00

482 lines
15 KiB
Python

"""
Updated Utility Functions for Server_Monitorizare
Now using Ansible for remote device management instead of direct HTTP commands
"""
import logging
from functools import wraps
from flask import jsonify, request, session
from datetime import datetime
from ansible_integration import get_ansible_manager
logger = logging.getLogger(__name__)
class APIError(Exception):
"""Custom exception for API errors"""
def __init__(self, message, status_code=400, details=None):
self.message = message
self.status_code = status_code
self.details = details or {}
super().__init__(self.message)
def error_response(error_message, status_code=400, details=None):
"""Create a standardized error response"""
response = {
'success': False,
'error': error_message,
'timestamp': datetime.now().isoformat()
}
if details:
response['details'] = details
return jsonify(response), status_code
def success_response(data=None, message="Success", status_code=200):
"""Create a standardized success response"""
response = {
'success': True,
'message': message,
'timestamp': datetime.now().isoformat()
}
if data is not None:
response['data'] = data
return jsonify(response), status_code
def require_auth(f):
"""Decorator to require authentication"""
@wraps(f)
def decorated_function(*args, **kwargs):
# Check for API key in headers
api_key = request.headers.get('X-API-Key')
from config import get_config
config = get_config()
if not api_key or api_key != config.API_KEY:
logger.warning(f"Unauthorized access attempt from {request.remote_addr}")
return error_response("Unauthorized", 401)
return f(*args, **kwargs)
return decorated_function
def require_session_auth(f):
"""Decorator to require session-based authentication"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return error_response("Authentication required", 401)
return f(*args, **kwargs)
return decorated_function
def log_request(f):
"""Decorator to log request details"""
@wraps(f)
def decorated_function(*args, **kwargs):
logger.info(f"{request.method} {request.path} from {request.remote_addr}")
try:
return f(*args, **kwargs)
except APIError as e:
logger.error(f"API Error in {f.__name__}: {e.message}")
return error_response(e.message, e.status_code, e.details)
except Exception as e:
logger.exception(f"Unexpected error in {f.__name__}")
return error_response("Internal server error", 500)
return decorated_function
def validate_required_fields(required_fields):
"""Decorator to validate required fields in JSON request"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not request.is_json:
return error_response("Content-Type must be application/json", 400)
data = request.get_json()
missing_fields = [field for field in required_fields if field not in data or not data[field]]
if missing_fields:
return error_response(
"Missing required fields",
400,
{'missing_fields': missing_fields}
)
return f(*args, **kwargs)
return decorated_function
return decorator
def validate_ip_address(ip):
"""Validate IP address format"""
import re
pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
if not re.match(pattern, ip):
return False
parts = ip.split('.')
return all(0 <= int(part) <= 255 for part in parts)
def sanitize_hostname(hostname):
"""Sanitize hostname to prevent injection"""
import re
# Allow alphanumeric, dash, and underscore
if not re.match(r'^[a-zA-Z0-9_-]+$', hostname):
raise APIError("Invalid hostname format", 400)
if len(hostname) > 255:
raise APIError("Hostname too long", 400)
return hostname
def setup_logging(config):
"""Setup logging configuration"""
import os
from logging.handlers import RotatingFileHandler
# Create logs directory if it doesn't exist
log_dir = os.path.dirname(config.LOG_FILE)
if log_dir and not os.path.exists(log_dir):
os.makedirs(log_dir)
# Create logger
logger = logging.getLogger()
logger.setLevel(getattr(logging, config.LOG_LEVEL))
# File handler with rotation
file_handler = RotatingFileHandler(
config.LOG_FILE,
maxBytes=config.LOG_MAX_BYTES,
backupCount=config.LOG_BACKUP_COUNT
)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
))
# Console handler
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
))
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger
# ============================================================================
# ANSIBLE-BASED DEVICE MANAGEMENT FUNCTIONS
# ============================================================================
def execute_command_on_device(device_hostname: str, command: str, user: str = 'pi'):
"""
Execute command on a device using Ansible
Args:
device_hostname: Device hostname from inventory
command: Command to execute
user: User to execute command as
Returns:
dict: Execution result with success status and output
"""
try:
ansible_mgr = get_ansible_manager()
logger.info(f"Executing command on {device_hostname}: {command}")
result = ansible_mgr.execute_command_on_device(
device=device_hostname,
command=command,
user=user
)
if result['success']:
logger.info(f"Command executed successfully on {device_hostname}")
return {
"success": True,
"result": result['output'],
"device": device_hostname,
"timestamp": result['timestamp']
}
else:
logger.error(f"Command failed on {device_hostname}: {result.get('error')}")
return {
"success": False,
"error": result.get('error', 'Unknown error'),
"device": device_hostname,
"timestamp": result['timestamp']
}
except Exception as e:
logger.exception(f"Error executing command on {device_hostname}: {e}")
return {
"success": False,
"error": f"Connection error: {str(e)}",
"device": device_hostname
}
def deploy_updates_to_device(
device_hostname: str,
git_branch: str = 'dev',
backup: bool = True,
restart: bool = True
):
"""
Deploy updates to device using Ansible deploy playbook
Args:
device_hostname: Device hostname from inventory
git_branch: Git branch to deploy
backup: Whether to backup before deploy
restart: Whether to restart service
Returns:
dict: Deployment result
"""
try:
ansible_mgr = get_ansible_manager()
logger.info(f"Starting deployment on {device_hostname} from branch {git_branch}")
result = ansible_mgr.deploy_to_devices(
target_hosts=device_hostname,
git_branch=git_branch,
backup=backup,
restart=restart
)
if result['success']:
logger.info(f"Deployment completed successfully on {device_hostname}")
return {
"success": True,
"message": f"Deployed {git_branch} to {device_hostname}",
"device": device_hostname,
"branch": git_branch,
"log_file": result.get('log_file'),
"timestamp": result['timestamp']
}
else:
logger.error(f"Deployment failed on {device_hostname}: {result.get('error')}")
return {
"success": False,
"error": result.get('error', 'Deployment failed'),
"device": device_hostname,
"timestamp": result['timestamp']
}
except Exception as e:
logger.exception(f"Error deploying to {device_hostname}: {e}")
return {
"success": False,
"error": f"Deployment error: {str(e)}",
"device": device_hostname
}
def deploy_updates_to_all_devices(
git_branch: str = 'dev',
backup: bool = True,
restart: bool = True
):
"""
Deploy updates to all devices in the 'prezenta_devices' group
Args:
git_branch: Git branch to deploy
backup: Whether to backup before deploy
restart: Whether to restart service
Returns:
dict: Deployment result for all devices
"""
try:
ansible_mgr = get_ansible_manager()
logger.info(f"Starting batch deployment to all devices from branch {git_branch}")
result = ansible_mgr.deploy_to_devices(
target_hosts='prezenta_devices',
git_branch=git_branch,
backup=backup,
restart=restart
)
if result['success']:
logger.info(f"Batch deployment completed successfully")
return {
"success": True,
"message": f"Deployed {git_branch} to all devices",
"branch": git_branch,
"log_file": result.get('log_file'),
"timestamp": result['timestamp']
}
else:
logger.error(f"Batch deployment failed: {result.get('error')}")
return {
"success": False,
"error": result.get('error', 'Batch deployment failed'),
"timestamp": result['timestamp']
}
except Exception as e:
logger.exception(f"Error in batch deployment: {e}")
return {
"success": False,
"error": f"Batch deployment error: {str(e)}"
}
def system_update_device(
device_hostname: str,
update_os: bool = False,
update_python: bool = True,
health_check: bool = True
):
"""
Perform system update and maintenance on device
Args:
device_hostname: Device hostname from inventory
update_os: Whether to update OS packages
update_python: Whether to update Python packages
health_check: Whether to perform health check
Returns:
dict: Update result
"""
try:
ansible_mgr = get_ansible_manager()
logger.info(f"Starting system update on {device_hostname}")
result = ansible_mgr.system_update(
target_hosts=device_hostname,
update_os=update_os,
update_python=update_python,
health_check=health_check
)
if result['success']:
logger.info(f"System update completed on {device_hostname}")
return {
"success": True,
"message": f"System update completed on {device_hostname}",
"device": device_hostname,
"log_file": result.get('log_file'),
"timestamp": result['timestamp']
}
else:
logger.error(f"System update failed on {device_hostname}: {result.get('error')}")
return {
"success": False,
"error": result.get('error', 'System update failed'),
"device": device_hostname,
"timestamp": result['timestamp']
}
except Exception as e:
logger.exception(f"Error updating system on {device_hostname}: {e}")
return {
"success": False,
"error": f"System update error: {str(e)}",
"device": device_hostname
}
def get_device_facts(device_hostname: str):
"""
Get facts about a device using Ansible
Args:
device_hostname: Device hostname from inventory
Returns:
dict: Device facts and information
"""
try:
ansible_mgr = get_ansible_manager()
logger.info(f"Gathering facts for {device_hostname}")
result = ansible_mgr.get_device_facts(device_hostname)
if result['success']:
return {
"success": True,
"device": device_hostname,
"facts": result.get('facts', {}),
"timestamp": result['timestamp']
}
else:
logger.error(f"Failed to get facts for {device_hostname}: {result.get('error')}")
return {
"success": False,
"error": result.get('error', 'Failed to get device facts'),
"device": device_hostname
}
except Exception as e:
logger.exception(f"Error getting facts for {device_hostname}: {e}")
return {
"success": False,
"error": f"Error getting device facts: {str(e)}",
"device": device_hostname
}
def get_device_status(device_hostname: str):
"""
Get device status (uses get_device_facts internally)
Args:
device_hostname: Device hostname from inventory
Returns:
dict: Device status information
"""
facts = get_device_facts(device_hostname)
if facts['success']:
ansible_facts = facts.get('facts', {})
return {
"success": True,
"status": {
"hostname": device_hostname,
"system": ansible_facts.get('ansible_system'),
"distribution": ansible_facts.get('ansible_distribution'),
"version": ansible_facts.get('ansible_distribution_version'),
"ip_address": ansible_facts.get('ansible_default_ipv4', {}).get('address'),
"uptime_seconds": ansible_facts.get('ansible_uptime_seconds'),
"processor_count": ansible_facts.get('ansible_processor_count'),
"memtotal_mb": ansible_facts.get('ansible_memtotal_mb'),
"memfree_mb": ansible_facts.get('ansible_memfree_mb'),
"timestamp": facts['timestamp']
}
}
else:
return facts
def get_execution_logs(limit: int = 10):
"""Get recent Ansible execution logs"""
try:
ansible_mgr = get_ansible_manager()
logs = ansible_mgr.get_execution_logs(limit)
return {
"success": True,
"logs": logs,
"count": len(logs)
}
except Exception as e:
logger.exception(f"Error getting execution logs: {e}")
return {
"success": False,
"error": f"Error getting logs: {str(e)}"
}