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:
481
utils_ansible.py
Normal file
481
utils_ansible.py
Normal file
@@ -0,0 +1,481 @@
|
||||
"""
|
||||
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)}"
|
||||
}
|
||||
Reference in New Issue
Block a user