454 lines
16 KiB
Python
454 lines
16 KiB
Python
"""
|
|
Ansible and SSH management API endpoints
|
|
"""
|
|
from flask import Blueprint, request, jsonify
|
|
import json
|
|
import os
|
|
from datetime import datetime
|
|
from app.services.ansible_service import AnsibleService
|
|
from app.models import Device, AnsibleExecution
|
|
from config.database_config import get_db
|
|
import logging
|
|
|
|
# Create blueprint
|
|
ansible_bp = Blueprint('ansible', __name__, url_prefix='/api/ansible')
|
|
|
|
# Initialize service
|
|
ansible_service = AnsibleService()
|
|
|
|
@ansible_bp.route('/inventory', methods=['GET'])
|
|
def get_inventory():
|
|
"""Get current Ansible inventory (structured)"""
|
|
try:
|
|
data = ansible_service.get_inventory_data()
|
|
return jsonify({'success': True, 'inventory': data})
|
|
except Exception as e:
|
|
logging.error(f"Error getting inventory: {e}")
|
|
return jsonify({'error': str(e), 'success': False}), 500
|
|
|
|
@ansible_bp.route('/inventory/raw', methods=['GET'])
|
|
def get_inventory_raw():
|
|
"""Get raw YAML inventory text"""
|
|
try:
|
|
data = ansible_service.get_inventory_data()
|
|
return jsonify({'success': True, 'yaml': data.get('raw_yaml', '')})
|
|
except Exception as e:
|
|
return jsonify({'error': str(e), 'success': False}), 500
|
|
|
|
@ansible_bp.route('/inventory/sync', methods=['POST'])
|
|
def sync_inventory():
|
|
"""Sync all active app devices into monitoring_devices inventory group"""
|
|
try:
|
|
result = ansible_service.sync_devices_to_inventory()
|
|
status = 200 if result.get('success') else 400
|
|
return jsonify(result), status
|
|
except Exception as e:
|
|
logging.error(f"Error syncing inventory: {e}")
|
|
return jsonify({'error': str(e), 'success': False}), 500
|
|
|
|
@ansible_bp.route('/inventory/group/add', methods=['POST'])
|
|
def add_inventory_group():
|
|
"""Add a new inventory group"""
|
|
try:
|
|
data = request.get_json()
|
|
group_name = (data or {}).get('group_name', '').strip()
|
|
if not group_name:
|
|
return jsonify({'success': False, 'error': 'group_name is required'}), 400
|
|
result = ansible_service.add_group_to_inventory(group_name)
|
|
return jsonify(result), 200 if result.get('success') else 400
|
|
except Exception as e:
|
|
return jsonify({'error': str(e), 'success': False}), 500
|
|
|
|
@ansible_bp.route('/inventory/group/remove', methods=['POST'])
|
|
def remove_inventory_group():
|
|
"""Remove an inventory group"""
|
|
try:
|
|
data = request.get_json()
|
|
group_name = (data or {}).get('group_name', '').strip()
|
|
if not group_name:
|
|
return jsonify({'success': False, 'error': 'group_name is required'}), 400
|
|
result = ansible_service.remove_group_from_inventory(group_name)
|
|
return jsonify(result), 200 if result.get('success') else 400
|
|
except Exception as e:
|
|
return jsonify({'error': str(e), 'success': False}), 500
|
|
|
|
@ansible_bp.route('/inventory/host/add', methods=['POST'])
|
|
def add_inventory_host():
|
|
"""Add a host to an inventory group"""
|
|
try:
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({'success': False, 'error': 'JSON body required'}), 400
|
|
group = data.get('group', '').strip()
|
|
hostname = data.get('hostname', '').strip()
|
|
ip = data.get('ip', '').strip()
|
|
ssh_user = data.get('ssh_user', 'pi').strip() or 'pi'
|
|
ssh_port = int(data.get('ssh_port', 22))
|
|
use_key = bool(data.get('use_key', True))
|
|
password = data.get('password', None)
|
|
if not group or not hostname or not ip:
|
|
return jsonify({'success': False, 'error': 'group, hostname and ip are required'}), 400
|
|
result = ansible_service.add_host_to_inventory(
|
|
group=group, hostname=hostname, ip=ip,
|
|
ssh_user=ssh_user, ssh_port=ssh_port,
|
|
use_key=use_key, password=password
|
|
)
|
|
return jsonify(result), 200 if result.get('success') else 400
|
|
except Exception as e:
|
|
return jsonify({'error': str(e), 'success': False}), 500
|
|
|
|
@ansible_bp.route('/inventory/host/remove', methods=['POST'])
|
|
def remove_inventory_host():
|
|
"""Remove a host from an inventory group"""
|
|
try:
|
|
data = request.get_json()
|
|
group = (data or {}).get('group', '').strip()
|
|
hostname = (data or {}).get('hostname', '').strip()
|
|
if not group or not hostname:
|
|
return jsonify({'success': False, 'error': 'group and hostname are required'}), 400
|
|
result = ansible_service.remove_host_from_inventory(group=group, hostname=hostname)
|
|
return jsonify(result), 200 if result.get('success') else 400
|
|
except Exception as e:
|
|
return jsonify({'error': str(e), 'success': False}), 500
|
|
|
|
@ansible_bp.route('/inventory/refresh', methods=['POST'])
|
|
def refresh_inventory():
|
|
"""Refresh Ansible inventory from database (legacy alias for /sync)"""
|
|
try:
|
|
result = ansible_service.sync_devices_to_inventory()
|
|
return jsonify(result), 200 if result.get('success') else 400
|
|
except Exception as e:
|
|
logging.error(f"Error refreshing inventory: {e}")
|
|
return jsonify({'error': str(e), 'success': False}), 500
|
|
|
|
@ansible_bp.route('/playbooks', methods=['GET'])
|
|
def list_playbooks():
|
|
"""List available Ansible playbooks"""
|
|
try:
|
|
playbook_dir = ansible_service.playbook_dir
|
|
playbooks = []
|
|
|
|
if playbook_dir.exists():
|
|
for file in playbook_dir.glob('*.yml'):
|
|
playbooks.append({
|
|
'name': file.stem,
|
|
'filename': file.name,
|
|
'path': str(file),
|
|
'modified': datetime.fromtimestamp(file.stat().st_mtime).isoformat()
|
|
})
|
|
|
|
# Add built-in playbooks
|
|
builtin_playbooks = [
|
|
{
|
|
'name': 'update_devices',
|
|
'description': 'Update all packages on monitoring devices',
|
|
'builtin': True
|
|
},
|
|
{
|
|
'name': 'restart_service',
|
|
'description': 'Restart monitoring services on devices',
|
|
'builtin': True
|
|
}
|
|
]
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'playbooks': playbooks,
|
|
'builtin_playbooks': builtin_playbooks
|
|
})
|
|
except Exception as e:
|
|
logging.error(f"Error listing playbooks: {e}")
|
|
return jsonify({
|
|
'error': str(e),
|
|
'success': False
|
|
}), 500
|
|
|
|
@ansible_bp.route('/execute', methods=['POST'])
|
|
def execute_playbook():
|
|
"""
|
|
Execute Ansible playbook
|
|
|
|
Expected JSON:
|
|
{
|
|
"playbook": "update_devices",
|
|
"limit_hosts": ["device-01", "device-02"], # optional
|
|
"extra_vars": {"key": "value"}, # optional
|
|
"create_builtin": True # optional, create builtin playbook if needed
|
|
}
|
|
"""
|
|
try:
|
|
data = request.get_json()
|
|
|
|
if not data or not data.get('playbook'):
|
|
return jsonify({
|
|
'error': 'Playbook name is required',
|
|
'success': False
|
|
}), 400
|
|
|
|
playbook_name = data['playbook']
|
|
limit_hosts = data.get('limit_hosts')
|
|
extra_vars = data.get('extra_vars', {})
|
|
create_builtin = data.get('create_builtin', True)
|
|
|
|
# Create builtin playbooks if they don't exist
|
|
if create_builtin:
|
|
if playbook_name == 'update_devices':
|
|
ansible_service.create_update_playbook()
|
|
elif playbook_name == 'restart_service':
|
|
ansible_service.create_restart_service_playbook()
|
|
|
|
# Add controller IP to extra vars for callbacks
|
|
extra_vars['ansible_controller_ip'] = request.host
|
|
|
|
# Execute playbook
|
|
result = ansible_service.execute_playbook(
|
|
playbook_name=playbook_name,
|
|
limit_hosts=limit_hosts,
|
|
extra_vars=extra_vars
|
|
)
|
|
|
|
if result['success']:
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Playbook execution started',
|
|
'execution_id': result['execution_id'],
|
|
'log_file': result['log_file']
|
|
})
|
|
else:
|
|
return jsonify(result), 500
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error executing playbook: {e}")
|
|
return jsonify({
|
|
'error': str(e),
|
|
'success': False
|
|
}), 500
|
|
|
|
@ansible_bp.route('/executions', methods=['GET'])
|
|
def get_executions():
|
|
"""Get Ansible execution history"""
|
|
try:
|
|
limit = min(int(request.args.get('limit', 50)), 200)
|
|
executions = ansible_service.get_execution_history(limit=limit)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'executions': executions
|
|
})
|
|
except Exception as e:
|
|
logging.error(f"Error getting executions: {e}")
|
|
return jsonify({
|
|
'error': str(e),
|
|
'success': False
|
|
}), 500
|
|
|
|
|
|
@ansible_bp.route('/executions/<execution_id>/live', methods=['GET'])
|
|
def get_execution_live(execution_id):
|
|
"""Poll current log + status for a running or finished execution (UUID)."""
|
|
result = ansible_service.get_live_execution(execution_id)
|
|
if not result.get('success'):
|
|
return jsonify(result), 404
|
|
return jsonify(result), 200
|
|
|
|
@ansible_bp.route('/executions/<int:execution_id>', methods=['GET'])
|
|
def get_execution_details(execution_id):
|
|
"""Get detailed execution information"""
|
|
try:
|
|
with get_db().get_session() as session:
|
|
execution = session.query(AnsibleExecution).get(execution_id)
|
|
|
|
if not execution:
|
|
return jsonify({
|
|
'error': 'Execution not found',
|
|
'success': False
|
|
}), 404
|
|
|
|
execution_data = {
|
|
'id': execution.id,
|
|
'playbook_name': execution.playbook_name,
|
|
'target_devices': json.loads(execution.target_devices) if execution.target_devices else [],
|
|
'command_line': execution.command_line,
|
|
'start_time': execution.start_time.isoformat() if execution.start_time else None,
|
|
'end_time': execution.end_time.isoformat() if execution.end_time else None,
|
|
'status': execution.status,
|
|
'exit_code': execution.exit_code,
|
|
'stdout_log': execution.stdout_log,
|
|
'stderr_log': execution.stderr_log,
|
|
'successful_hosts': execution.successful_hosts,
|
|
'failed_hosts': execution.failed_hosts,
|
|
'unreachable_hosts': execution.unreachable_hosts
|
|
}
|
|
|
|
# Read log file if it exists
|
|
if execution.ansible_log_file and os.path.exists(execution.ansible_log_file):
|
|
with open(execution.ansible_log_file, 'r') as f:
|
|
execution_data['full_log'] = f.read()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'execution': execution_data
|
|
})
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error getting execution details: {e}")
|
|
return jsonify({
|
|
'error': str(e),
|
|
'success': False
|
|
}), 500
|
|
|
|
@ansible_bp.route('/ssh/test', methods=['POST'])
|
|
def test_ssh_connectivity():
|
|
"""
|
|
Test SSH connectivity to devices
|
|
|
|
Expected JSON:
|
|
{
|
|
"device_ips": ["192.168.1.100", "192.168.1.101"],
|
|
"username": "pi" # optional, defaults to "pi"
|
|
}
|
|
"""
|
|
try:
|
|
data = request.get_json()
|
|
|
|
if not data or not data.get('device_ips'):
|
|
return jsonify({
|
|
'error': 'device_ips list is required',
|
|
'success': False
|
|
}), 400
|
|
|
|
device_ips = data['device_ips']
|
|
username = data.get('username', 'pi')
|
|
|
|
# Test connectivity
|
|
if len(device_ips) == 1:
|
|
# Single device test
|
|
result = ansible_service.test_ssh_connectivity(device_ips[0], username)
|
|
return jsonify({
|
|
'success': True,
|
|
'results': {device_ips[0]: result}
|
|
})
|
|
else:
|
|
# Bulk test
|
|
results = ansible_service.bulk_ssh_test(device_ips)
|
|
|
|
# Summary
|
|
successful = sum(1 for r in results.values() if r.get('success'))
|
|
total = len(results)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'results': results,
|
|
'summary': {
|
|
'successful': successful,
|
|
'failed': total - successful,
|
|
'total': total
|
|
}
|
|
})
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error testing SSH connectivity: {e}")
|
|
return jsonify({
|
|
'error': str(e),
|
|
'success': False
|
|
}), 500
|
|
|
|
@ansible_bp.route('/ssh/keys/setup', methods=['POST'])
|
|
def setup_ssh_keys():
|
|
"""Setup SSH keys for Ansible authentication"""
|
|
try:
|
|
result = ansible_service.setup_ssh_keys()
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logging.error(f"Error setting up SSH keys: {e}")
|
|
return jsonify({
|
|
'error': str(e),
|
|
'success': False
|
|
}), 500
|
|
|
|
@ansible_bp.route('/ssh/keys/public', methods=['GET'])
|
|
def get_public_key():
|
|
"""Get public key for distribution to devices"""
|
|
try:
|
|
public_key_path = ansible_service.ssh_key_path.with_suffix('.pub')
|
|
|
|
if not public_key_path.exists():
|
|
return jsonify({
|
|
'error': 'Public key not found. Run SSH setup first.',
|
|
'success': False
|
|
}), 404
|
|
|
|
with open(public_key_path, 'r') as f:
|
|
public_key = f.read().strip()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'public_key': public_key,
|
|
'key_path': str(public_key_path)
|
|
})
|
|
except Exception as e:
|
|
logging.error(f"Error getting public key: {e}")
|
|
return jsonify({
|
|
'error': str(e),
|
|
'success': False
|
|
}), 500
|
|
|
|
@ansible_bp.route('/devices/status', methods=['GET'])
|
|
def get_devices_status():
|
|
"""Get status of all devices for Ansible operations"""
|
|
try:
|
|
with get_db().get_session() as session:
|
|
devices = session.query(Device).all()
|
|
|
|
device_data = []
|
|
for device in devices:
|
|
device_data.append({
|
|
'id': device.id,
|
|
'hostname': device.hostname,
|
|
'device_ip': device.device_ip,
|
|
'nume_masa': device.nume_masa,
|
|
'status': device.status,
|
|
'last_seen': device.last_seen.isoformat() if device.last_seen else None,
|
|
'device_type': device.device_type,
|
|
'os_version': device.os_version,
|
|
'location': device.location
|
|
})
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'devices': device_data,
|
|
'total_count': len(device_data)
|
|
})
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error getting devices status: {e}")
|
|
return jsonify({
|
|
'error': str(e),
|
|
'success': False
|
|
}), 500
|
|
|
|
# Callback endpoints for Ansible playbooks
|
|
@ansible_bp.route('/callback/update_complete', methods=['POST'])
|
|
def update_complete_callback():
|
|
"""Callback endpoint for update completion"""
|
|
try:
|
|
data = request.get_json()
|
|
logging.info(f"Update completed for {data.get('hostname')}: {data}")
|
|
|
|
# You could update device status, send notifications, etc.
|
|
return jsonify({'success': True})
|
|
except Exception as e:
|
|
logging.error(f"Error in update callback: {e}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@ansible_bp.route('/callback/service_restarted', methods=['POST'])
|
|
def service_restart_callback():
|
|
"""Callback endpoint for service restart completion"""
|
|
try:
|
|
data = request.get_json()
|
|
logging.info(f"Service restarted for {data.get('hostname')}: {data}")
|
|
|
|
return jsonify({'success': True})
|
|
except Exception as e:
|
|
logging.error(f"Error in service restart callback: {e}")
|
|
return jsonify({'error': str(e)}), 500 |