- 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
482 lines
15 KiB
Python
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)}"
|
|
}
|