09138fbc18
Extract 9 route groups out of app.py into routes/ blueprints:
- routes/network.py — DNS, DHCP, NTP, network info/test (10 routes)
- routes/wireguard.py — WireGuard keys, peers, config, enforcement (18 routes)
- routes/cells.py — cell-to-cell connections (5 routes)
- routes/peers.py — peer CRUD + IP update + _next_peer_ip helper (10 routes)
- routes/routing.py — NAT, peer routes, firewall, iptables (17 routes)
- routes/vault.py — certs, trust, secrets (19 routes)
- routes/containers.py — containers, images, volumes (14 routes)
- routes/services.py — service bus, logs, services status/connectivity (18 routes)
- routes/peer_dashboard.py — peer-scoped dashboard/services (2 routes)
All blueprints use lazy `from app import X` inside route bodies to preserve
test patch compatibility (patch('app.email_manager', mock) still works).
Also included in this commit:
- A1 fix: backup/restore now includes email/calendar user files
- A2 fix: apply_config sets applying=True flag via helper container
- A3 fix: add_peer rolls back firewall on DNS failure
app.py reduced: 3011 → 1294 lines. 1021 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
292 lines
12 KiB
Python
292 lines
12 KiB
Python
import logging
|
|
import json
|
|
import os
|
|
from datetime import datetime
|
|
from flask import Blueprint, request, jsonify
|
|
logger = logging.getLogger('picell')
|
|
bp = Blueprint('services', __name__)
|
|
|
|
@bp.route('/api/services/bus/status', methods=['GET'])
|
|
def get_service_bus_status():
|
|
try:
|
|
from app import service_bus
|
|
return jsonify(service_bus.get_service_status_summary())
|
|
except Exception as e:
|
|
logger.error(f"Error getting service bus status: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/services/bus/events', methods=['GET'])
|
|
def get_service_bus_events():
|
|
try:
|
|
from app import service_bus
|
|
from service_bus import EventType
|
|
event_type = request.args.get('type')
|
|
source = request.args.get('source')
|
|
limit = int(request.args.get('limit', 100))
|
|
events = service_bus.get_event_history(
|
|
EventType(event_type) if event_type else None,
|
|
source,
|
|
limit
|
|
)
|
|
return jsonify([{
|
|
'event_id': e.event_id,
|
|
'event_type': e.event_type.value,
|
|
'source': e.source,
|
|
'data': e.data,
|
|
'timestamp': e.timestamp.isoformat()
|
|
} for e in events])
|
|
except Exception as e:
|
|
logger.error(f"Error getting service bus events: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/services/bus/services/<service_name>/start', methods=['POST'])
|
|
def start_service(service_name):
|
|
try:
|
|
from app import service_bus
|
|
success = service_bus.orchestrate_service_start(service_name)
|
|
if success:
|
|
return jsonify({"message": f"Service {service_name} started successfully"})
|
|
return jsonify({"error": f"Failed to start service {service_name}"}), 500
|
|
except Exception as e:
|
|
logger.error(f"Error starting service {service_name}: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/services/bus/services/<service_name>/stop', methods=['POST'])
|
|
def stop_service(service_name):
|
|
try:
|
|
from app import service_bus
|
|
success = service_bus.orchestrate_service_stop(service_name)
|
|
if success:
|
|
return jsonify({"message": f"Service {service_name} stopped successfully"})
|
|
return jsonify({"error": f"Failed to stop service {service_name}"}), 500
|
|
except Exception as e:
|
|
logger.error(f"Error stopping service {service_name}: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/services/bus/services/<service_name>/restart', methods=['POST'])
|
|
def restart_service(service_name):
|
|
try:
|
|
from app import service_bus
|
|
success = service_bus.orchestrate_service_restart(service_name)
|
|
if success:
|
|
return jsonify({"message": f"Service {service_name} restarted successfully"})
|
|
return jsonify({"error": f"Failed to restart service {service_name}"}), 500
|
|
except Exception as e:
|
|
logger.error(f"Error restarting service {service_name}: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/logs/services/<service>', methods=['GET'])
|
|
def get_service_logs(service):
|
|
try:
|
|
from app import log_manager
|
|
level = request.args.get('level', 'INFO')
|
|
lines = int(request.args.get('lines', 50))
|
|
logs = log_manager.get_service_logs(service, level, lines)
|
|
return jsonify({"service": service, "logs": logs})
|
|
except Exception as e:
|
|
logger.error(f"Error getting logs for {service}: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/logs/search', methods=['POST'])
|
|
def search_logs():
|
|
try:
|
|
from app import log_manager
|
|
data = request.get_json(silent=True) or {}
|
|
results = log_manager.search_logs(
|
|
data.get('query', ''),
|
|
data.get('time_range'),
|
|
data.get('services'),
|
|
data.get('level')
|
|
)
|
|
return jsonify({"results": results, "count": len(results)})
|
|
except Exception as e:
|
|
logger.error(f"Error searching logs: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/logs/export', methods=['POST'])
|
|
def export_logs():
|
|
try:
|
|
from app import log_manager
|
|
data = request.get_json(silent=True) or {}
|
|
format = data.get('format', 'json')
|
|
log_data = log_manager.export_logs(format, data.get('filters', {}))
|
|
return jsonify({"logs": log_data, "format": format})
|
|
except Exception as e:
|
|
logger.error(f"Error exporting logs: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/logs/statistics', methods=['GET'])
|
|
def get_log_statistics():
|
|
try:
|
|
from app import log_manager
|
|
return jsonify(log_manager.get_log_statistics(request.args.get('service')))
|
|
except Exception as e:
|
|
logger.error(f"Error getting log statistics: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/logs/rotate', methods=['POST'])
|
|
def rotate_logs():
|
|
try:
|
|
from app import log_manager
|
|
data = request.get_json(silent=True) or {}
|
|
log_manager.rotate_logs(data.get('service'))
|
|
return jsonify({"message": "Logs rotated successfully"})
|
|
except Exception as e:
|
|
logger.error(f"Error rotating logs: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/logs/files', methods=['GET'])
|
|
def get_log_file_infos():
|
|
try:
|
|
from app import log_manager
|
|
return jsonify(log_manager.get_all_log_file_infos())
|
|
except Exception as e:
|
|
logger.error(f"Error listing log files: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/logs/verbosity', methods=['GET'])
|
|
def get_log_verbosity():
|
|
try:
|
|
from app import log_manager
|
|
return jsonify(log_manager.get_service_levels())
|
|
except Exception as e:
|
|
logger.error(f"Error getting log verbosity: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/logs/verbosity', methods=['PUT'])
|
|
def set_log_verbosity():
|
|
try:
|
|
from app import log_manager
|
|
data = request.get_json(silent=True) or {}
|
|
for service, level in data.items():
|
|
log_manager.set_service_level(service, level)
|
|
levels_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'log_levels.json')
|
|
os.makedirs(os.path.dirname(levels_file), exist_ok=True)
|
|
current = {}
|
|
if os.path.exists(levels_file):
|
|
try:
|
|
with open(levels_file) as f:
|
|
current = json.load(f)
|
|
except Exception:
|
|
pass
|
|
current.update(data)
|
|
with open(levels_file, 'w') as f:
|
|
json.dump(current, f, indent=2)
|
|
return jsonify({"message": "Log levels updated", "levels": log_manager.get_service_levels()})
|
|
except Exception as e:
|
|
logger.error(f"Error setting log verbosity: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/services/status', methods=['GET'])
|
|
def get_all_services_status():
|
|
try:
|
|
from app import service_bus
|
|
services_status = {}
|
|
for service_name in service_bus.list_services():
|
|
try:
|
|
service = service_bus.get_service(service_name)
|
|
status = service.get_status()
|
|
if isinstance(status, dict):
|
|
clean_status = {
|
|
'status': status.get('status', 'unknown'),
|
|
'running': status.get('running', False),
|
|
'timestamp': status.get('timestamp', datetime.utcnow().isoformat())
|
|
}
|
|
if service_name == 'network':
|
|
clean_status.update({
|
|
'dns_status': status.get('dns_running', False),
|
|
'dhcp_status': status.get('dhcp_running', False),
|
|
'ntp_status': status.get('ntp_running', False)
|
|
})
|
|
elif service_name == 'wireguard':
|
|
clean_status.update({
|
|
'peers_count': status.get('peers_count', 0),
|
|
'interface': status.get('interface', 'unknown')
|
|
})
|
|
elif service_name == 'email':
|
|
clean_status.update({
|
|
'users_count': status.get('users_count', 0),
|
|
'domain': status.get('domain', 'unknown')
|
|
})
|
|
elif service_name == 'calendar':
|
|
clean_status.update({
|
|
'users_count': status.get('users_count', 0),
|
|
'calendars_count': status.get('calendars_count', 0)
|
|
})
|
|
elif service_name == 'files':
|
|
clean_status.update({
|
|
'users_count': status.get('users_count', 0),
|
|
'storage_used': status.get('total_storage_used', {})
|
|
})
|
|
elif service_name == 'routing':
|
|
clean_status.update({
|
|
'nat_rules_count': status.get('nat_rules_count', 0),
|
|
'peer_routes_count': status.get('peer_routes_count', 0),
|
|
'firewall_rules_count': status.get('firewall_rules_count', 0)
|
|
})
|
|
elif service_name == 'vault':
|
|
clean_status.update({
|
|
'certificates_count': status.get('certificates_count', 0),
|
|
'trusted_keys_count': status.get('trusted_keys_count', 0)
|
|
})
|
|
services_status[service_name] = clean_status
|
|
else:
|
|
services_status[service_name] = {'status': str(status), 'running': bool(status)}
|
|
except Exception as e:
|
|
services_status[service_name] = {'error': str(e), 'status': 'offline', 'running': False}
|
|
return jsonify({
|
|
"network": services_status.get('network', {}),
|
|
"wireguard": services_status.get('wireguard', {}),
|
|
"email": services_status.get('email', {}),
|
|
"calendar": services_status.get('calendar', {}),
|
|
"files": services_status.get('files', {}),
|
|
"routing": services_status.get('routing', {}),
|
|
"vault": services_status.get('vault', {}),
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error getting all services status: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/services/connectivity', methods=['GET'])
|
|
def test_all_services_connectivity():
|
|
try:
|
|
from app import service_bus
|
|
connectivity_results = {}
|
|
for service_name in service_bus.list_services():
|
|
try:
|
|
service = service_bus.get_service(service_name)
|
|
if hasattr(service, 'test_connectivity'):
|
|
connectivity_results[service_name] = service.test_connectivity()
|
|
else:
|
|
connectivity_results[service_name] = {'status': 'ok', 'message': 'No connectivity test available'}
|
|
except Exception as e:
|
|
connectivity_results[service_name] = {'status': 'error', 'message': str(e)}
|
|
return jsonify({
|
|
"network": connectivity_results.get('network', {}),
|
|
"wireguard": connectivity_results.get('wireguard', {}),
|
|
"email": connectivity_results.get('email', {}),
|
|
"calendar": connectivity_results.get('calendar', {}),
|
|
"files": connectivity_results.get('files', {}),
|
|
"routing": connectivity_results.get('routing', {}),
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error testing all services connectivity: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@bp.route('/api/logs', methods=['GET'])
|
|
def get_backend_logs():
|
|
log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'picell.log')
|
|
lines = int(request.args.get('lines', 100))
|
|
try:
|
|
if not os.path.exists(log_file):
|
|
return jsonify({"error": "Log file not found."}), 404
|
|
with open(log_file, 'r', encoding='utf-8', errors='ignore') as f:
|
|
all_lines = f.readlines()
|
|
tail_lines = all_lines[-lines:] if lines > 0 else all_lines
|
|
return jsonify({"log": ''.join(tail_lines)})
|
|
except Exception as e:
|
|
logger.error(f"Error reading log file: {e}")
|
|
return jsonify({"error": str(e)}), 500
|