7b39331417
- log_manager: add collect_container_logs (appends docker logs to container_<name>.log), get_container_log_lines, rotate_container_log, get_all_log_file_infos - app.py: new endpoints /api/logs/files (all log file sizes), /api/logs/containers/<name> (collect+return stored container logs); rotate endpoint now handles both service and container logs - Logs page: split into API Service Logs tab (python manager logs) and Container Logs tab (persistent docker stdout/stderr); Statistics tab shows both kinds with per-row rotate; each tab has a description explaining what it shows and where files live - wireguard_manager: test_connectivity peer_ip=None guard (already in previous commit, now rebuilt) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2183 lines
82 KiB
Python
2183 lines
82 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Personal Internet Cell API Server
|
|
|
|
Provides REST API endpoints for managing:
|
|
- Cell status and configuration
|
|
- Network services (DNS, DHCP, NTP)
|
|
- WireGuard VPN and peer management
|
|
- Email, Calendar, and File services
|
|
- Routing and VPN gateway
|
|
- Vault and trust management (Phase 6)
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from flask import Flask, request, jsonify, current_app
|
|
from flask_cors import CORS
|
|
import threading
|
|
import time
|
|
from collections import deque
|
|
import json as pyjson
|
|
from logging.handlers import RotatingFileHandler
|
|
import uuid
|
|
import contextvars
|
|
|
|
# Track API start time for uptime calculation
|
|
API_START_TIME = time.time()
|
|
|
|
from network_manager import NetworkManager
|
|
from wireguard_manager import WireGuardManager
|
|
from peer_registry import PeerRegistry
|
|
from email_manager import EmailManager
|
|
from calendar_manager import CalendarManager
|
|
from file_manager import FileManager
|
|
from routing_manager import RoutingManager
|
|
from cell_manager import CellManager
|
|
from vault_manager import VaultManager
|
|
from container_manager import ContainerManager
|
|
from config_manager import ConfigManager
|
|
from service_bus import ServiceBus, EventType
|
|
from log_manager import LogManager
|
|
import firewall_manager
|
|
|
|
# Context variable for request info
|
|
request_context = contextvars.ContextVar('request_context', default={})
|
|
|
|
# Set default log level and log file if not already defined
|
|
LOG_LEVEL = globals().get('LOG_LEVEL', 'INFO')
|
|
LOG_FILE = globals().get('LOG_FILE', 'picell.log')
|
|
|
|
class ContextFilter(logging.Filter):
|
|
def filter(self, record):
|
|
ctx = request_context.get({})
|
|
for k, v in ctx.items():
|
|
setattr(record, k, v)
|
|
return True
|
|
|
|
class JsonFormatter(logging.Formatter):
|
|
def format(self, record):
|
|
log_record = {
|
|
'timestamp': self.formatTime(record, self.datefmt),
|
|
'level': record.levelname,
|
|
'name': record.name,
|
|
'message': record.getMessage(),
|
|
'request_id': getattr(record, 'request_id', None),
|
|
'client_ip': getattr(record, 'client_ip', None),
|
|
'method': getattr(record, 'method', None),
|
|
'path': getattr(record, 'path', None),
|
|
'status': getattr(record, 'status', None),
|
|
'user': getattr(record, 'user', None),
|
|
}
|
|
if record.exc_info:
|
|
log_record['exception'] = self.formatException(record.exc_info)
|
|
return pyjson.dumps({k: v for k, v in log_record.items() if v is not None})
|
|
|
|
json_formatter = JsonFormatter()
|
|
context_filter = ContextFilter()
|
|
|
|
handlers = [logging.StreamHandler()]
|
|
try:
|
|
file_handler = RotatingFileHandler(LOG_FILE, maxBytes=5_000_000, backupCount=5, encoding='utf-8')
|
|
file_handler.setLevel(getattr(logging, LOG_LEVEL, logging.INFO))
|
|
file_handler.setFormatter(json_formatter)
|
|
file_handler.addFilter(context_filter)
|
|
handlers.append(file_handler)
|
|
except Exception as e:
|
|
print(f"Warning: Could not create rotating log file handler: {e}")
|
|
|
|
for h in handlers:
|
|
h.setFormatter(json_formatter)
|
|
h.addFilter(context_filter)
|
|
|
|
logging.basicConfig(
|
|
level=getattr(logging, LOG_LEVEL, logging.INFO),
|
|
handlers=handlers
|
|
)
|
|
logger = logging.getLogger('picell')
|
|
|
|
# Flask app setup
|
|
app = Flask(__name__)
|
|
CORS(app)
|
|
|
|
# Development mode flag
|
|
app.config['DEVELOPMENT_MODE'] = True # Set to True for development, False for production
|
|
|
|
# Initialize enhanced components
|
|
config_manager = ConfigManager(config_file='./config/cell_config.json', data_dir='./data')
|
|
service_bus = ServiceBus()
|
|
log_manager = LogManager(log_dir='./data/logs')
|
|
|
|
# Initialize service loggers
|
|
service_log_configs = {
|
|
'network': {'level': 'INFO', 'formatter': 'json', 'console': False},
|
|
'wireguard': {'level': 'INFO', 'formatter': 'json', 'console': False},
|
|
'email': {'level': 'INFO', 'formatter': 'json', 'console': False},
|
|
'calendar': {'level': 'INFO', 'formatter': 'json', 'console': False},
|
|
'files': {'level': 'INFO', 'formatter': 'json', 'console': False},
|
|
'routing': {'level': 'INFO', 'formatter': 'json', 'console': False},
|
|
'vault': {'level': 'INFO', 'formatter': 'json', 'console': False},
|
|
'api': {'level': 'INFO', 'formatter': 'json', 'console': True}
|
|
}
|
|
|
|
for service, config in service_log_configs.items():
|
|
log_manager.add_service_logger(service, config)
|
|
|
|
# Start service bus
|
|
service_bus.start()
|
|
|
|
@app.before_request
|
|
def enrich_log_context():
|
|
req_id = str(uuid.uuid4())
|
|
client_ip = request.remote_addr
|
|
method = request.method
|
|
path = request.path
|
|
user = getattr(getattr(request, 'user', None), 'id', None) or 'anonymous'
|
|
request_context.set({
|
|
'request_id': req_id,
|
|
'client_ip': client_ip,
|
|
'method': method,
|
|
'path': path,
|
|
'user': user
|
|
})
|
|
|
|
@app.after_request
|
|
def log_request(response):
|
|
ctx = request_context.get({})
|
|
ctx['status'] = response.status_code
|
|
logger.info(f"{ctx.get('method')} {ctx.get('path')} {ctx.get('status')}")
|
|
return response
|
|
|
|
@app.teardown_request
|
|
def clear_log_context(exc):
|
|
request_context.set({})
|
|
|
|
# Initialize managers — paths configurable via env for testing
|
|
_DATA_DIR = os.environ.get('DATA_DIR', '/app/data')
|
|
_CONFIG_DIR = os.environ.get('CONFIG_DIR', '/app/config')
|
|
|
|
network_manager = NetworkManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR)
|
|
wireguard_manager = WireGuardManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR)
|
|
peer_registry = PeerRegistry(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR)
|
|
email_manager = EmailManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR)
|
|
calendar_manager = CalendarManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR)
|
|
file_manager = FileManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR)
|
|
routing_manager = RoutingManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR)
|
|
cell_manager = CellManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR)
|
|
app.vault_manager = VaultManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR)
|
|
container_manager = ContainerManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR)
|
|
|
|
# Apply firewall + DNS rules from stored peer settings (survives API restarts)
|
|
def _apply_startup_enforcement():
|
|
try:
|
|
peers = peer_registry.list_peers()
|
|
firewall_manager.apply_all_peer_rules(peers)
|
|
firewall_manager.apply_all_dns_rules(peers, COREFILE_PATH)
|
|
logger.info(f"Applied enforcement rules for {len(peers)} peers on startup")
|
|
except Exception as e:
|
|
logger.warning(f"Startup enforcement failed (non-fatal): {e}")
|
|
|
|
COREFILE_PATH = '/app/config/dns/Corefile'
|
|
|
|
# Run in background so startup isn't blocked waiting on docker exec
|
|
threading.Thread(target=_apply_startup_enforcement, daemon=True).start()
|
|
|
|
# Register services with service bus
|
|
service_bus.register_service('network', network_manager)
|
|
service_bus.register_service('wireguard', wireguard_manager)
|
|
service_bus.register_service('email', email_manager)
|
|
service_bus.register_service('calendar', calendar_manager)
|
|
service_bus.register_service('files', file_manager)
|
|
service_bus.register_service('routing', routing_manager)
|
|
service_bus.register_service('vault', app.vault_manager)
|
|
service_bus.register_service('container', container_manager)
|
|
|
|
# Unified health monitoring
|
|
HEALTH_HISTORY_SIZE = 100
|
|
health_history = deque(maxlen=HEALTH_HISTORY_SIZE)
|
|
health_monitor_running = True
|
|
|
|
# Health alerting configuration
|
|
HEALTH_ALERT_THRESHOLD = 3 # Number of consecutive failures before alert
|
|
service_alert_counters = {}
|
|
|
|
def perform_health_check():
|
|
"""Perform a unified health check of all services, with alerting."""
|
|
try:
|
|
# Use service bus to get health from all services
|
|
result = {
|
|
'timestamp': datetime.utcnow().isoformat(),
|
|
'alerts': []
|
|
}
|
|
|
|
# Get health from each service
|
|
for service_name in service_bus.list_services():
|
|
try:
|
|
service = service_bus.get_service(service_name)
|
|
if hasattr(service, 'health_check'):
|
|
health = service.health_check()
|
|
else:
|
|
health = service.get_status()
|
|
result[service_name] = health
|
|
except Exception as e:
|
|
result[service_name] = {'error': str(e), 'status': 'offline'}
|
|
|
|
# Health alerting logic - improved to be more robust
|
|
global service_alert_counters
|
|
for service_name in service_bus.list_services():
|
|
if service_name in result:
|
|
status = result[service_name]
|
|
healthy = True
|
|
|
|
# Improved health determination logic
|
|
if isinstance(status, dict):
|
|
# Check for explicit healthy field first
|
|
if 'healthy' in status:
|
|
healthy = status['healthy']
|
|
# Check for running status
|
|
elif 'running' in status:
|
|
healthy = status['running']
|
|
# Check for status field with various healthy values
|
|
elif 'status' in status:
|
|
status_value = status['status']
|
|
if isinstance(status_value, str):
|
|
healthy = status_value.lower() in ('ok', 'healthy', 'online', 'active')
|
|
else:
|
|
healthy = bool(status_value)
|
|
# Check for error field
|
|
elif 'error' in status:
|
|
healthy = False
|
|
# If no health indicators, assume healthy if service exists
|
|
else:
|
|
healthy = True
|
|
else:
|
|
# If status is not a dict, assume it's a boolean
|
|
healthy = bool(status)
|
|
|
|
# Only count as unhealthy if we're certain it's down
|
|
if not healthy:
|
|
service_alert_counters[service_name] = service_alert_counters.get(service_name, 0) + 1
|
|
if service_alert_counters[service_name] >= HEALTH_ALERT_THRESHOLD:
|
|
alert_msg = f"ALERT: {service_name} unhealthy for {service_alert_counters[service_name]} consecutive checks."
|
|
logger.warning(alert_msg)
|
|
result['alerts'].append(alert_msg)
|
|
|
|
# Publish alert event
|
|
service_bus.publish_event(EventType.ERROR_OCCURRED, service_name, {
|
|
'error': alert_msg,
|
|
'service': service_name,
|
|
'consecutive_failures': service_alert_counters[service_name]
|
|
})
|
|
else:
|
|
# Reset counter if service is healthy
|
|
if service_alert_counters.get(service_name, 0) > 0:
|
|
logger.info(f"Service {service_name} recovered, resetting alert counter")
|
|
service_alert_counters[service_name] = 0
|
|
|
|
logger.info(f"Unified health check: {result}")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Unified health check failed: {e}")
|
|
return {'error': str(e), 'timestamp': datetime.utcnow().isoformat()}
|
|
|
|
def health_monitor_loop():
|
|
while health_monitor_running:
|
|
with app.app_context():
|
|
health_result = perform_health_check()
|
|
health_history.appendleft(health_result)
|
|
|
|
# Publish health check event
|
|
service_bus.publish_event(EventType.HEALTH_CHECK, 'api', health_result)
|
|
time.sleep(60) # Check every 60 seconds
|
|
|
|
# Start health monitor thread
|
|
health_monitor_thread = threading.Thread(target=health_monitor_loop, daemon=True)
|
|
health_monitor_thread.start()
|
|
|
|
def is_local_request():
|
|
# Allow requests from localhost, Docker networks, and internal IPs
|
|
remote_addr = request.remote_addr
|
|
forwarded_for = request.headers.get('X-Forwarded-For', '')
|
|
|
|
# Check direct remote address
|
|
if remote_addr in ('127.0.0.1', '::1', 'localhost'):
|
|
return True
|
|
|
|
# Check forwarded address (for reverse proxy scenarios)
|
|
if forwarded_for:
|
|
forwarded_ips = [ip.strip() for ip in forwarded_for.split(',')]
|
|
for ip in forwarded_ips:
|
|
if ip in ('127.0.0.1', '::1', 'localhost'):
|
|
return True
|
|
|
|
# Allow Docker internal networks (172.x.x.x, 192.168.x.x, 10.x.x.x)
|
|
if remote_addr:
|
|
try:
|
|
import ipaddress
|
|
ip = ipaddress.ip_address(remote_addr)
|
|
if ip.is_private or ip.is_loopback:
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
return False
|
|
|
|
@app.route('/health', methods=['GET'])
|
|
def health_check():
|
|
"""Health check endpoint."""
|
|
try:
|
|
return jsonify({
|
|
"status": "healthy",
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"version": "1.0.0"
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Health check failed: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/status', methods=['GET'])
|
|
def get_cell_status():
|
|
"""Get overall cell status."""
|
|
try:
|
|
# Use service bus to get status from all services
|
|
services_status = {}
|
|
for service_name in service_bus.list_services():
|
|
try:
|
|
service = service_bus.get_service(service_name)
|
|
services_status[service_name] = service.get_status()
|
|
except Exception as e:
|
|
services_status[service_name] = {'error': str(e)}
|
|
|
|
peers = peer_registry.list_peers()
|
|
|
|
# Calculate actual uptime
|
|
current_time = time.time()
|
|
uptime_seconds = int(current_time - API_START_TIME)
|
|
|
|
return jsonify({
|
|
"cell_name": "personal-internet-cell",
|
|
"domain": "cell.local",
|
|
"uptime": uptime_seconds,
|
|
"peers_count": len(peers),
|
|
"services": services_status,
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error getting cell status: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/config', methods=['GET'])
|
|
def get_config():
|
|
"""Get cell configuration."""
|
|
try:
|
|
service_configs = config_manager.get_all_configs()
|
|
config = {
|
|
'cell_name': os.environ.get('CELL_NAME', 'mycell'),
|
|
'domain': os.environ.get('CELL_DOMAIN', 'cell'),
|
|
'ip_range': os.environ.get('CELL_IP_RANGE', '172.20.0.0/16'),
|
|
'wireguard_port': int(os.environ.get('WG_PORT', '51820')),
|
|
}
|
|
config.update(service_configs)
|
|
return jsonify(config)
|
|
except Exception as e:
|
|
logger.error(f"Error getting config: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/config', methods=['PUT'])
|
|
def update_config():
|
|
"""Update cell configuration."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
|
|
# Update configuration using config manager
|
|
for service, config in data.items():
|
|
if service in config_manager.service_schemas:
|
|
success = config_manager.update_service_config(service, config)
|
|
if success:
|
|
# Publish config change event
|
|
service_bus.publish_event(EventType.CONFIG_CHANGED, service, {
|
|
'service': service,
|
|
'config': config
|
|
})
|
|
|
|
logger.info(f"Updated config: {data}")
|
|
return jsonify({"message": "Configuration updated successfully"})
|
|
except Exception as e:
|
|
logger.error(f"Error updating config: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# Configuration management endpoints
|
|
@app.route('/api/config/backup', methods=['POST'])
|
|
def create_config_backup():
|
|
"""Create configuration backup."""
|
|
try:
|
|
backup_id = config_manager.backup_config()
|
|
service_bus.publish_event(EventType.BACKUP_CREATED, 'api', {
|
|
'backup_id': backup_id,
|
|
'timestamp': datetime.utcnow().isoformat()
|
|
})
|
|
return jsonify({"backup_id": backup_id})
|
|
except Exception as e:
|
|
logger.error(f"Error creating backup: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/config/backups', methods=['GET'])
|
|
def list_config_backups():
|
|
"""List available backups."""
|
|
try:
|
|
backups = config_manager.list_backups()
|
|
return jsonify(backups)
|
|
except Exception as e:
|
|
logger.error(f"Error listing backups: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/config/restore/<backup_id>', methods=['POST'])
|
|
def restore_config(backup_id):
|
|
"""Restore configuration from backup."""
|
|
try:
|
|
success = config_manager.restore_config(backup_id)
|
|
if success:
|
|
service_bus.publish_event(EventType.RESTORE_COMPLETED, 'api', {
|
|
'backup_id': backup_id,
|
|
'timestamp': datetime.utcnow().isoformat()
|
|
})
|
|
return jsonify({"message": f"Configuration restored from backup: {backup_id}"})
|
|
else:
|
|
return jsonify({"error": f"Failed to restore backup: {backup_id}"}), 500
|
|
except Exception as e:
|
|
logger.error(f"Error restoring backup: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/config/export', methods=['GET'])
|
|
def export_config():
|
|
"""Export configuration."""
|
|
try:
|
|
format = request.args.get('format', 'json')
|
|
config_data = config_manager.export_config(format)
|
|
return jsonify({"config": config_data, "format": format})
|
|
except Exception as e:
|
|
logger.error(f"Error exporting config: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/config/import', methods=['POST'])
|
|
def import_config():
|
|
"""Import configuration."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
|
|
config_data = data.get('config')
|
|
format = data.get('format', 'json')
|
|
|
|
success = config_manager.import_config(config_data, format)
|
|
if success:
|
|
return jsonify({"message": "Configuration imported successfully"})
|
|
else:
|
|
return jsonify({"error": "Failed to import configuration"}), 500
|
|
except Exception as e:
|
|
logger.error(f"Error importing config: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# Service bus endpoints
|
|
@app.route('/api/services/bus/status', methods=['GET'])
|
|
def get_service_bus_status():
|
|
"""Get service bus status."""
|
|
try:
|
|
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
|
|
|
|
@app.route('/api/services/bus/events', methods=['GET'])
|
|
def get_service_bus_events():
|
|
"""Get service bus event history."""
|
|
try:
|
|
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
|
|
)
|
|
|
|
# Convert events to serializable format
|
|
serializable_events = []
|
|
for event in events:
|
|
serializable_events.append({
|
|
'event_id': event.event_id,
|
|
'event_type': event.event_type.value,
|
|
'source': event.source,
|
|
'data': event.data,
|
|
'timestamp': event.timestamp.isoformat()
|
|
})
|
|
|
|
return jsonify(serializable_events)
|
|
except Exception as e:
|
|
logger.error(f"Error getting service bus events: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/services/bus/services/<service_name>/start', methods=['POST'])
|
|
def start_service(service_name):
|
|
"""Start a service with orchestration."""
|
|
try:
|
|
success = service_bus.orchestrate_service_start(service_name)
|
|
if success:
|
|
return jsonify({"message": f"Service {service_name} started successfully"})
|
|
else:
|
|
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
|
|
|
|
@app.route('/api/services/bus/services/<service_name>/stop', methods=['POST'])
|
|
def stop_service(service_name):
|
|
"""Stop a service with orchestration."""
|
|
try:
|
|
success = service_bus.orchestrate_service_stop(service_name)
|
|
if success:
|
|
return jsonify({"message": f"Service {service_name} stopped successfully"})
|
|
else:
|
|
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
|
|
|
|
@app.route('/api/services/bus/services/<service_name>/restart', methods=['POST'])
|
|
def restart_service(service_name):
|
|
"""Restart a service with orchestration."""
|
|
try:
|
|
success = service_bus.orchestrate_service_restart(service_name)
|
|
if success:
|
|
return jsonify({"message": f"Service {service_name} restarted successfully"})
|
|
else:
|
|
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
|
|
|
|
# Logging endpoints
|
|
@app.route('/api/logs/services/<service>', methods=['GET'])
|
|
def get_service_logs(service):
|
|
"""Get logs for a specific service."""
|
|
try:
|
|
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
|
|
|
|
@app.route('/api/logs/search', methods=['POST'])
|
|
def search_logs():
|
|
"""Search logs across all services."""
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
query = data.get('query', '')
|
|
services = data.get('services')
|
|
level = data.get('level')
|
|
time_range = data.get('time_range')
|
|
|
|
results = log_manager.search_logs(query, time_range, services, 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
|
|
|
|
@app.route('/api/logs/export', methods=['POST'])
|
|
def export_logs():
|
|
"""Export logs in specified format."""
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
format = data.get('format', 'json')
|
|
filters = data.get('filters', {})
|
|
|
|
log_data = log_manager.export_logs(format, 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
|
|
|
|
@app.route('/api/logs/statistics', methods=['GET'])
|
|
def get_log_statistics():
|
|
"""Get log statistics."""
|
|
try:
|
|
service = request.args.get('service')
|
|
stats = log_manager.get_log_statistics(service)
|
|
return jsonify(stats)
|
|
except Exception as e:
|
|
logger.error(f"Error getting log statistics: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/logs/rotate', methods=['POST'])
|
|
def rotate_logs():
|
|
"""Manually rotate logs (API service log or container log)."""
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
name = data.get('name') # e.g. 'wireguard' or 'container_cell-api'
|
|
kind = data.get('kind', 'service') # 'service' or 'container'
|
|
|
|
if kind == 'container':
|
|
container_name = name[len('container_'):] if name and name.startswith('container_') else name
|
|
log_manager.rotate_container_log(container_name)
|
|
else:
|
|
log_manager.rotate_logs(name)
|
|
return jsonify({"message": "Logs rotated successfully"})
|
|
except Exception as e:
|
|
logger.error(f"Error rotating logs: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/logs/files', methods=['GET'])
|
|
def get_log_file_infos():
|
|
"""List all stored log files (service + container) with sizes."""
|
|
try:
|
|
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
|
|
|
|
@app.route('/api/logs/containers/<container_name>', methods=['GET'])
|
|
def get_stored_container_logs(container_name):
|
|
"""Collect latest docker logs into file and return last N lines."""
|
|
try:
|
|
tail = int(request.args.get('tail', 100))
|
|
log_manager.collect_container_logs(container_name)
|
|
lines = log_manager.get_container_log_lines(container_name, tail)
|
|
return jsonify({'container': container_name, 'lines': lines})
|
|
except Exception as e:
|
|
logger.error(f"Error getting stored container logs for {container_name}: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# Network Services API
|
|
@app.route('/api/dns/records', methods=['GET'])
|
|
def get_dns_records():
|
|
"""Get DNS records."""
|
|
try:
|
|
records = network_manager.get_dns_records()
|
|
return jsonify(records)
|
|
except Exception as e:
|
|
logger.error(f"Error getting DNS records: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/dns/records', methods=['POST'])
|
|
def add_dns_record():
|
|
"""Add DNS record."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = network_manager.add_dns_record(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error adding DNS record: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/dns/records', methods=['DELETE'])
|
|
def remove_dns_record():
|
|
"""Remove DNS record."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
result = network_manager.remove_dns_record(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error removing DNS record: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/dhcp/leases', methods=['GET'])
|
|
def get_dhcp_leases():
|
|
"""Get DHCP leases."""
|
|
try:
|
|
leases = network_manager.get_dhcp_leases()
|
|
return jsonify(leases)
|
|
except Exception as e:
|
|
logger.error(f"Error getting DHCP leases: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/dhcp/reservations', methods=['POST'])
|
|
def add_dhcp_reservation():
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = network_manager.add_dhcp_reservation(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error adding DHCP reservation: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/dhcp/reservations', methods=['DELETE'])
|
|
def remove_dhcp_reservation():
|
|
"""Remove DHCP reservation."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
result = network_manager.remove_dhcp_reservation(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error removing DHCP reservation: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/ntp/status', methods=['GET'])
|
|
def get_ntp_status():
|
|
"""Get NTP status."""
|
|
try:
|
|
status = network_manager.get_ntp_status()
|
|
return jsonify(status)
|
|
except Exception as e:
|
|
logger.error(f"Error getting NTP status: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/network/info', methods=['GET'])
|
|
def get_network_info():
|
|
"""Get general network info (interfaces, gateway, DNS, etc.)"""
|
|
try:
|
|
info = network_manager.get_network_info()
|
|
return jsonify(info)
|
|
except Exception as e:
|
|
logger.error(f"Error getting network info: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/dns/status', methods=['GET'])
|
|
def get_dns_status():
|
|
"""Get DNS service status and summary info."""
|
|
try:
|
|
status = network_manager.get_dns_status()
|
|
return jsonify(status)
|
|
except Exception as e:
|
|
logger.error(f"Error getting DNS status: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/network/test', methods=['POST'])
|
|
def test_network():
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = network_manager.test_connectivity(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error testing network: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# WireGuard API
|
|
@app.route('/api/wireguard/keys', methods=['GET'])
|
|
def get_wireguard_keys():
|
|
"""Get WireGuard keys."""
|
|
try:
|
|
result = wireguard_manager.get_keys()
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error getting WireGuard keys: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/keys/peer', methods=['POST'])
|
|
def generate_peer_keys():
|
|
"""Generate peer keys."""
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
name = data.get('name') or data.get('peer_name')
|
|
if not name:
|
|
return jsonify({"error": "Missing peer name"}), 400
|
|
result = wireguard_manager.generate_peer_keys(name)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error generating peer keys: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/config', methods=['GET'])
|
|
def get_wireguard_config():
|
|
"""Get WireGuard configuration."""
|
|
try:
|
|
result = wireguard_manager.get_config()
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error getting WireGuard config: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/peers', methods=['GET'])
|
|
def get_wireguard_peers():
|
|
"""Get WireGuard peers."""
|
|
try:
|
|
peers = wireguard_manager.get_peers()
|
|
return jsonify(peers)
|
|
except Exception as e:
|
|
logger.error(f"Error getting WireGuard peers: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/peers', methods=['POST'])
|
|
def add_wireguard_peer():
|
|
"""Add WireGuard peer."""
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
result = wireguard_manager.add_peer(
|
|
name=data.get('name', ''),
|
|
public_key=data.get('public_key', ''),
|
|
endpoint_ip=data.get('endpoint', data.get('endpoint_ip', '')),
|
|
allowed_ips=data.get('allowed_ips', ''),
|
|
persistent_keepalive=data.get('persistent_keepalive', 25)
|
|
)
|
|
return jsonify({"success": result})
|
|
except Exception as e:
|
|
logger.error(f"Error adding WireGuard peer: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/peers', methods=['DELETE'])
|
|
def remove_wireguard_peer():
|
|
"""Remove WireGuard peer."""
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
public_key = data.get('public_key') or data.get('name', '')
|
|
result = wireguard_manager.remove_peer(public_key)
|
|
return jsonify({"success": result})
|
|
except Exception as e:
|
|
logger.error(f"Error removing WireGuard peer: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/status', methods=['GET'])
|
|
def get_wireguard_status():
|
|
"""Get WireGuard status."""
|
|
try:
|
|
status = wireguard_manager.get_status()
|
|
return jsonify(status)
|
|
except Exception as e:
|
|
logger.error(f"Error getting WireGuard status: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/connectivity', methods=['POST'])
|
|
def test_wireguard_connectivity():
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = wireguard_manager.test_connectivity(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error testing WireGuard connectivity: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/peers/ip', methods=['PUT'])
|
|
def update_peer_ip():
|
|
"""Update peer IP."""
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
result = wireguard_manager.update_peer_ip(
|
|
data.get('public_key', data.get('peer', '')),
|
|
data.get('ip', '')
|
|
)
|
|
return jsonify({"success": result})
|
|
except Exception as e:
|
|
logger.error(f"Error updating peer IP: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/peers/status', methods=['POST'])
|
|
def get_peer_status():
|
|
"""Get live WireGuard status for a single peer."""
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
public_key = data.get('public_key', '')
|
|
if not public_key:
|
|
return jsonify({"error": "Missing public_key"}), 400
|
|
status = wireguard_manager.get_peer_status(public_key)
|
|
return jsonify(status)
|
|
except Exception as e:
|
|
logger.error(f"Error getting peer status: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/peers/statuses', methods=['GET'])
|
|
def get_all_peer_statuses():
|
|
"""Get live WireGuard status for all peers (keyed by public_key)."""
|
|
try:
|
|
statuses = wireguard_manager.get_all_peer_statuses()
|
|
return jsonify(statuses)
|
|
except Exception as e:
|
|
logger.error(f"Error getting peer statuses: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/network/setup', methods=['POST'])
|
|
def setup_network():
|
|
"""Setup network configuration for internet access."""
|
|
try:
|
|
success = wireguard_manager.setup_network_configuration()
|
|
if success:
|
|
return jsonify({"message": "Network configuration setup completed successfully"})
|
|
else:
|
|
return jsonify({"error": "Failed to setup network configuration"}), 500
|
|
except Exception as e:
|
|
logger.error(f"Error setting up network configuration: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/network/status', methods=['GET'])
|
|
def get_network_status():
|
|
"""Get network configuration status."""
|
|
try:
|
|
status = wireguard_manager.get_network_status()
|
|
return jsonify(status)
|
|
except Exception as e:
|
|
logger.error(f"Error getting network status: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/peers/config', methods=['POST'])
|
|
def get_peer_config():
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
peer_name = data.get('name', data.get('peer', ''))
|
|
|
|
# Look up peer details from registry if not supplied
|
|
peer_ip = data.get('ip', '')
|
|
peer_private_key = data.get('private_key', '')
|
|
registered = peer_registry.get_peer(peer_name) if peer_name else {}
|
|
if peer_name and (not peer_ip or not peer_private_key):
|
|
if registered:
|
|
peer_ip = peer_ip or registered.get('ip', '')
|
|
peer_private_key = peer_private_key or registered.get('private_key', '')
|
|
|
|
# Use real external endpoint if not supplied
|
|
server_endpoint = data.get('server_endpoint', '')
|
|
if not server_endpoint:
|
|
srv = wireguard_manager.get_server_config()
|
|
server_endpoint = srv.get('endpoint') or '<SERVER_IP>'
|
|
|
|
# Determine AllowedIPs: explicit > peer's stored internet_access > default full tunnel
|
|
allowed_ips = data.get('allowed_ips') or None
|
|
if not allowed_ips and registered:
|
|
internet_access = registered.get('internet_access', True)
|
|
allowed_ips = wireguard_manager.FULL_TUNNEL_IPS if internet_access else wireguard_manager.SPLIT_TUNNEL_IPS
|
|
|
|
result = wireguard_manager.get_peer_config(
|
|
peer_name=peer_name,
|
|
peer_ip=peer_ip,
|
|
peer_private_key=peer_private_key,
|
|
server_endpoint=server_endpoint,
|
|
allowed_ips=allowed_ips,
|
|
)
|
|
return jsonify({"config": result})
|
|
except Exception as e:
|
|
logger.error(f"Error getting peer config: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/server-config', methods=['GET'])
|
|
def get_server_config():
|
|
try:
|
|
config = wireguard_manager.get_server_config()
|
|
return jsonify(config)
|
|
except Exception as e:
|
|
logger.error(f"Error getting server config: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/refresh-ip', methods=['POST'])
|
|
def refresh_external_ip():
|
|
try:
|
|
ip = wireguard_manager.get_external_ip(force_refresh=True)
|
|
return jsonify({
|
|
'external_ip': ip,
|
|
'port': 51820,
|
|
'endpoint': f'{ip}:51820' if ip else None,
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error refreshing external IP: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/apply-enforcement', methods=['POST'])
|
|
def apply_wireguard_enforcement():
|
|
"""Re-apply per-peer iptables and DNS enforcement rules (call after WireGuard restart)."""
|
|
try:
|
|
peers = peer_registry.list_peers()
|
|
firewall_manager.apply_all_peer_rules(peers)
|
|
firewall_manager.apply_all_dns_rules(peers, COREFILE_PATH)
|
|
return jsonify({'ok': True, 'peers': len(peers)})
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/wireguard/check-port', methods=['POST'])
|
|
def check_wireguard_port():
|
|
try:
|
|
port_open = wireguard_manager.check_port_open()
|
|
return jsonify({'port_open': port_open, 'port': 51820})
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# Peer Registry API
|
|
@app.route('/api/peers', methods=['GET'])
|
|
def get_peers():
|
|
"""Get all peers."""
|
|
try:
|
|
peers = peer_registry.list_peers()
|
|
return jsonify(peers)
|
|
except Exception as e:
|
|
logger.error(f"Error getting peers: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
def _next_peer_ip() -> str:
|
|
"""Auto-assign the next free 10.0.0.x address (starts at .2, skips .1 = server)."""
|
|
import ipaddress
|
|
used = {p.get('ip', '').split('/')[0] for p in peer_registry.list_peers()}
|
|
network = ipaddress.ip_network('10.0.0.0/24')
|
|
for host in network.hosts():
|
|
ip = str(host)
|
|
if ip == '10.0.0.1':
|
|
continue # server address
|
|
if ip not in used:
|
|
return ip
|
|
raise ValueError('No free IPs left in 10.0.0.0/24')
|
|
|
|
|
|
@app.route('/api/peers', methods=['POST'])
|
|
def add_peer():
|
|
"""Add a peer."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
|
|
# Validate required fields (ip is optional — auto-assigned if omitted)
|
|
required_fields = ['name', 'public_key']
|
|
for field in required_fields:
|
|
if field not in data:
|
|
return jsonify({"error": f"Missing required field: {field}"}), 400
|
|
|
|
assigned_ip = data.get('ip') or _next_peer_ip()
|
|
|
|
# Add peer to registry with all provided fields
|
|
peer_info = {
|
|
'peer': data['name'],
|
|
'ip': assigned_ip,
|
|
'public_key': data['public_key'],
|
|
'private_key': data.get('private_key'),
|
|
'server_public_key': data.get('server_public_key'),
|
|
'server_endpoint': data.get('server_endpoint'),
|
|
'allowed_ips': data.get('allowed_ips'),
|
|
'persistent_keepalive': data.get('persistent_keepalive'),
|
|
'description': data.get('description'),
|
|
'internet_access': data.get('internet_access', True),
|
|
'service_access': data.get('service_access', ['calendar', 'files', 'mail', 'webdav']),
|
|
'peer_access': data.get('peer_access', True),
|
|
'config_needs_reinstall': False,
|
|
}
|
|
|
|
success = peer_registry.add_peer(peer_info)
|
|
if success:
|
|
# Apply server-side enforcement immediately
|
|
firewall_manager.apply_peer_rules(peer_info['ip'], peer_info)
|
|
firewall_manager.apply_all_dns_rules(peer_registry.list_peers(), COREFILE_PATH)
|
|
return jsonify({"message": f"Peer {data['name']} added successfully", "ip": assigned_ip}), 201
|
|
else:
|
|
return jsonify({"error": f"Peer {data['name']} already exists"}), 400
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error adding peer: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@app.route('/api/peers/<peer_name>', methods=['PUT'])
|
|
def update_peer(peer_name):
|
|
"""Update peer settings. Marks config_needs_reinstall if VPN config changed."""
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
existing = peer_registry.get_peer(peer_name)
|
|
if not existing:
|
|
return jsonify({"error": "Peer not found"}), 404
|
|
|
|
# Detect changes that require client to reinstall tunnel config
|
|
config_changed = (
|
|
('internet_access' in data and data['internet_access'] != existing.get('internet_access', True)) or
|
|
('ip' in data and data['ip'] != existing.get('ip')) or
|
|
('persistent_keepalive' in data and data['persistent_keepalive'] != existing.get('persistent_keepalive'))
|
|
)
|
|
|
|
updates = {k: v for k, v in data.items()}
|
|
if config_changed:
|
|
updates['config_needs_reinstall'] = True
|
|
|
|
success = peer_registry.update_peer(peer_name, updates)
|
|
if success:
|
|
# Re-apply server-side enforcement with updated settings
|
|
updated_peer = peer_registry.get_peer(peer_name)
|
|
if updated_peer:
|
|
firewall_manager.apply_peer_rules(updated_peer['ip'], updated_peer)
|
|
firewall_manager.apply_all_dns_rules(peer_registry.list_peers(), COREFILE_PATH)
|
|
result = {"message": f"Peer {peer_name} updated", "config_changed": config_changed}
|
|
return jsonify(result)
|
|
else:
|
|
return jsonify({"error": "Update failed"}), 500
|
|
except Exception as e:
|
|
logger.error(f"Error updating peer {peer_name}: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@app.route('/api/peers/<peer_name>/clear-reinstall', methods=['POST'])
|
|
def clear_peer_reinstall(peer_name):
|
|
"""Clear the config_needs_reinstall flag once user has downloaded new config."""
|
|
try:
|
|
peer_registry.clear_reinstall_flag(peer_name)
|
|
return jsonify({"message": "Reinstall flag cleared"})
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@app.route('/api/peers/<peer_name>', methods=['DELETE'])
|
|
def remove_peer(peer_name):
|
|
"""Remove a peer."""
|
|
try:
|
|
success = peer_registry.remove_peer(peer_name)
|
|
if success:
|
|
return jsonify({"message": f"Peer {peer_name} removed successfully"})
|
|
else:
|
|
return jsonify({"message": f"Peer {peer_name} not found or already removed"})
|
|
except Exception as e:
|
|
logger.error(f"Error removing peer: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/peers/register', methods=['POST'])
|
|
def register_peer():
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = peer_registry.register_peer(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error registering peer: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/peers/<peer_name>/unregister', methods=['DELETE'])
|
|
def unregister_peer(peer_name):
|
|
"""Unregister a peer."""
|
|
try:
|
|
result = peer_registry.unregister_peer(peer_name)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error unregistering peer: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/peers/<peer_name>/update-ip', methods=['PUT'])
|
|
def update_peer_ip_registry(peer_name):
|
|
"""Update peer IP."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
new_ip = data.get('ip') if data else None
|
|
if not new_ip:
|
|
return jsonify({"error": "Missing ip"}), 400
|
|
success = peer_registry.update_peer_ip(peer_name, new_ip)
|
|
if success:
|
|
# Update routing and WireGuard configs
|
|
try:
|
|
routing_manager.update_peer_ip(peer_name, new_ip)
|
|
except Exception as e:
|
|
logger.warning(f"RoutingManager update_peer_ip failed: {e}")
|
|
try:
|
|
# For now, skip WireGuard update - method not implemented
|
|
logger.warning(f"WireGuardManager update_peer_ip not implemented yet")
|
|
except Exception as e:
|
|
logger.warning(f"WireGuardManager update_peer_ip failed: {e}")
|
|
return jsonify({"message": f"IP update received for {peer_name}"})
|
|
else:
|
|
return jsonify({"error": f"Peer {peer_name} not found"}), 404
|
|
except Exception as e:
|
|
logger.error(f"Error updating peer IP: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/ip-update', methods=['POST'])
|
|
def ip_update():
|
|
"""Handle IP update from peer."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
peer_name = data.get('peer')
|
|
new_ip = data.get('ip')
|
|
if not peer_name or not new_ip:
|
|
return jsonify({"error": "Missing peer or ip"}), 400
|
|
success = peer_registry.update_peer_ip(peer_name, new_ip)
|
|
if success:
|
|
# Update routing and WireGuard configs
|
|
try:
|
|
routing_manager.update_peer_ip(peer_name, new_ip)
|
|
except Exception as e:
|
|
logger.warning(f"RoutingManager update_peer_ip failed: {e}")
|
|
try:
|
|
# For now, skip WireGuard update - method not implemented
|
|
logger.warning(f"WireGuardManager update_peer_ip not implemented yet")
|
|
except Exception as e:
|
|
logger.warning(f"WireGuardManager update_peer_ip failed: {e}")
|
|
return jsonify({"message": f"IP update received for {peer_name}"})
|
|
else:
|
|
return jsonify({"error": f"Peer {peer_name} not found"}), 404
|
|
except Exception as e:
|
|
logger.error(f"Error handling IP update: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# Email Services API
|
|
@app.route('/api/email/users', methods=['GET'])
|
|
def get_email_users():
|
|
"""Get email users."""
|
|
try:
|
|
users = email_manager.get_users()
|
|
return jsonify(users)
|
|
except Exception as e:
|
|
logger.error(f"Error getting email users: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/email/users', methods=['POST'])
|
|
def create_email_user():
|
|
"""Create email user."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = email_manager.create_user(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error creating email user: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/email/users/<username>', methods=['DELETE'])
|
|
def delete_email_user(username):
|
|
"""Delete email user."""
|
|
try:
|
|
result = email_manager.delete_user(username)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error deleting email user: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/email/status', methods=['GET'])
|
|
def get_email_status():
|
|
"""Get email service status."""
|
|
try:
|
|
status = email_manager.get_status()
|
|
return jsonify(status)
|
|
except Exception as e:
|
|
logger.error(f"Error getting email status: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/email/connectivity', methods=['GET'])
|
|
def test_email_connectivity():
|
|
"""Test email connectivity."""
|
|
try:
|
|
result = email_manager.test_connectivity()
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error testing email connectivity: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/email/send', methods=['POST'])
|
|
def send_email():
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = email_manager.send_email(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error sending email: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/email/mailbox/<username>', methods=['GET'])
|
|
def get_mailbox_info(username):
|
|
"""Get mailbox information."""
|
|
try:
|
|
result = email_manager.get_mailbox_info(username)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error getting mailbox info: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# Calendar Services API
|
|
@app.route('/api/calendar/users', methods=['GET'])
|
|
def get_calendar_users():
|
|
"""Get calendar users."""
|
|
try:
|
|
users = calendar_manager.get_users()
|
|
return jsonify(users)
|
|
except Exception as e:
|
|
logger.error(f"Error getting calendar users: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/calendar/users', methods=['POST'])
|
|
def create_calendar_user():
|
|
"""Create calendar user."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = calendar_manager.create_user(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error creating calendar user: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/calendar/users/<username>', methods=['DELETE'])
|
|
def delete_calendar_user(username):
|
|
"""Delete calendar user."""
|
|
try:
|
|
result = calendar_manager.delete_user(username)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error deleting calendar user: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/calendar/calendars', methods=['POST'])
|
|
def create_calendar():
|
|
"""Create calendar."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = calendar_manager.create_calendar(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error creating calendar: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/calendar/events', methods=['POST'])
|
|
def add_calendar_event():
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = calendar_manager.add_event(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error adding calendar event: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/calendar/events/<username>/<calendar_name>', methods=['GET'])
|
|
def get_calendar_events(username, calendar_name):
|
|
"""Get calendar events."""
|
|
try:
|
|
params = request.args.to_dict()
|
|
result = calendar_manager.get_events(username, calendar_name, params)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error getting calendar events: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/calendar/status', methods=['GET'])
|
|
def get_calendar_status():
|
|
"""Get calendar service status."""
|
|
try:
|
|
status = calendar_manager.get_status()
|
|
return jsonify(status)
|
|
except Exception as e:
|
|
logger.error(f"Error getting calendar status: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/calendar/connectivity', methods=['GET'])
|
|
def test_calendar_connectivity():
|
|
"""Test calendar connectivity."""
|
|
try:
|
|
result = calendar_manager.test_connectivity()
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error testing calendar connectivity: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# File Services API
|
|
@app.route('/api/files/users', methods=['GET'])
|
|
def get_file_users():
|
|
"""Get file storage users."""
|
|
try:
|
|
users = file_manager.get_users()
|
|
return jsonify(users)
|
|
except Exception as e:
|
|
logger.error(f"Error getting file users: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/files/users', methods=['POST'])
|
|
def create_file_user():
|
|
"""Create file storage user."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = file_manager.create_user(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error creating file user: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/files/users/<username>', methods=['DELETE'])
|
|
def delete_file_user(username):
|
|
"""Delete file storage user."""
|
|
try:
|
|
result = file_manager.delete_user(username)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error deleting file user: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/files/folders', methods=['POST'])
|
|
def create_folder():
|
|
"""Create folder."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = file_manager.create_folder(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error creating folder: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/files/folders/<username>/<path:folder_path>', methods=['DELETE'])
|
|
def delete_folder(username, folder_path):
|
|
"""Delete folder."""
|
|
try:
|
|
result = file_manager.delete_folder(username, folder_path)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error deleting folder: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/files/upload/<username>', methods=['POST'])
|
|
def upload_file(username):
|
|
"""Upload file."""
|
|
try:
|
|
if 'file' not in request.files:
|
|
return jsonify({"error": "No file provided"}), 400
|
|
|
|
file = request.files['file']
|
|
path = request.form.get('path', '')
|
|
|
|
result = file_manager.upload_file(username, file, path)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error uploading file: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/files/download/<username>/<path:file_path>', methods=['GET'])
|
|
def download_file(username, file_path):
|
|
"""Download file."""
|
|
try:
|
|
result = file_manager.download_file(username, file_path)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error downloading file: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/files/delete/<username>/<path:file_path>', methods=['DELETE'])
|
|
def delete_file(username, file_path):
|
|
"""Delete file."""
|
|
try:
|
|
result = file_manager.delete_file(username, file_path)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error deleting file: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/files/list/<username>', methods=['GET'])
|
|
def list_files(username):
|
|
"""List files."""
|
|
try:
|
|
folder = request.args.get('folder', '')
|
|
result = file_manager.list_files(username, folder)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error listing files: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/files/status', methods=['GET'])
|
|
def get_file_status():
|
|
"""Get file service status."""
|
|
try:
|
|
status = file_manager.get_status()
|
|
return jsonify(status)
|
|
except Exception as e:
|
|
logger.error(f"Error getting file status: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/files/connectivity', methods=['GET'])
|
|
def test_file_connectivity():
|
|
"""Test file service connectivity."""
|
|
try:
|
|
result = file_manager.test_connectivity()
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error testing file connectivity: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# Routing API
|
|
@app.route('/api/routing/status', methods=['GET'])
|
|
def get_routing_status():
|
|
"""Get routing status."""
|
|
try:
|
|
status = routing_manager.get_status()
|
|
return jsonify(status)
|
|
except Exception as e:
|
|
logger.error(f"Error getting routing status: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/routing/setup', methods=['POST'])
|
|
def setup_routing():
|
|
"""Apply/verify routing setup (WireGuard handles NAT via PostUp rules)."""
|
|
try:
|
|
status = routing_manager.get_status()
|
|
return jsonify({'success': True, 'message': 'Routing managed by WireGuard PostUp rules', **status})
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/routing/nat', methods=['POST'])
|
|
def add_nat_rule():
|
|
"""Add NAT rule.
|
|
JSON fields:
|
|
- source_network (CIDR)
|
|
- target_interface (str)
|
|
- masquerade (bool, default True)
|
|
- nat_type (MASQUERADE, SNAT, DNAT)
|
|
- protocol (TCP, UDP, ALL)
|
|
- external_port (str, optional)
|
|
- internal_ip (str, optional)
|
|
- internal_port (str, optional)
|
|
"""
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
result = routing_manager.add_nat_rule(
|
|
source_network=data.get('source_network'),
|
|
target_interface=data.get('target_interface'),
|
|
masquerade=data.get('masquerade', True),
|
|
nat_type=data.get('nat_type', 'MASQUERADE'),
|
|
protocol=data.get('protocol', 'ALL'),
|
|
external_port=data.get('external_port'),
|
|
internal_ip=data.get('internal_ip'),
|
|
internal_port=data.get('internal_port')
|
|
)
|
|
return jsonify({'success': result})
|
|
except Exception as e:
|
|
logger.error(f"Error adding NAT rule: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/routing/nat/<rule_id>', methods=['DELETE'])
|
|
def remove_nat_rule(rule_id):
|
|
"""Remove NAT rule."""
|
|
try:
|
|
result = routing_manager.remove_nat_rule(rule_id)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error removing NAT rule: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/routing/peers', methods=['POST'])
|
|
def add_peer_route():
|
|
"""Add peer route."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
result = routing_manager.add_peer_route(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error adding peer route: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/routing/peers/<peer_name>', methods=['DELETE'])
|
|
def remove_peer_route(peer_name):
|
|
"""Remove peer route."""
|
|
try:
|
|
result = routing_manager.remove_peer_route(peer_name)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error removing peer route: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/routing/exit-nodes', methods=['POST'])
|
|
def add_exit_node():
|
|
"""Add exit node."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
result = routing_manager.add_exit_node(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error adding exit node: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/routing/bridge', methods=['POST'])
|
|
def add_bridge_route():
|
|
"""Add bridge route."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
result = routing_manager.add_bridge_route(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error adding bridge route: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/routing/split', methods=['POST'])
|
|
def add_split_route():
|
|
"""Add split route."""
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
result = routing_manager.add_split_route(data)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error adding split route: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/routing/firewall', methods=['POST'])
|
|
def add_firewall_rule():
|
|
"""Add firewall rule.
|
|
JSON fields:
|
|
- rule_type (INPUT, OUTPUT, FORWARD)
|
|
- source (CIDR)
|
|
- destination (CIDR)
|
|
- action (ACCEPT, DROP, REJECT)
|
|
- protocol (TCP, UDP, ICMP, ALL)
|
|
- port (str, optional)
|
|
- port_range (str, optional, e.g. '1000-2000')
|
|
"""
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
result = routing_manager.add_firewall_rule(
|
|
rule_type=data.get('rule_type'),
|
|
source=data.get('source'),
|
|
destination=data.get('destination'),
|
|
action=data.get('action', 'ACCEPT'),
|
|
port=data.get('port'),
|
|
protocol=data.get('protocol', 'ALL'),
|
|
port_range=data.get('port_range')
|
|
)
|
|
return jsonify({'success': result})
|
|
except Exception as e:
|
|
logger.error(f"Error adding firewall rule: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/routing/firewall/<rule_id>', methods=['DELETE'])
|
|
def remove_firewall_rule(rule_id):
|
|
try:
|
|
result = routing_manager.remove_firewall_rule(rule_id)
|
|
return jsonify({'success': result}), (200 if result else 404)
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/routing/live-iptables', methods=['GET'])
|
|
def get_live_iptables():
|
|
try:
|
|
return jsonify(routing_manager.get_live_iptables())
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/routing/connectivity', methods=['POST'])
|
|
def test_routing_connectivity():
|
|
"""Test routing connectivity."""
|
|
try:
|
|
data = request.get_json(silent=True) or {}
|
|
target_ip = data.get('target_ip', '8.8.8.8')
|
|
via_peer = data.get('via_peer')
|
|
result = routing_manager.test_routing_connectivity(target_ip, via_peer)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error testing routing connectivity: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/routing/logs', methods=['GET'])
|
|
def get_routing_logs():
|
|
"""Get routing logs."""
|
|
try:
|
|
lines = request.args.get('lines', 50, type=int)
|
|
result = routing_manager.get_logs(lines)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error getting routing logs: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/routing/nat', methods=['GET'])
|
|
def get_nat_rules():
|
|
"""Get all NAT rules."""
|
|
try:
|
|
rules = routing_manager.get_nat_rules()
|
|
return jsonify({"nat_rules": rules})
|
|
except Exception as e:
|
|
logger.error(f"Error getting NAT rules: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/routing/peers', methods=['GET'])
|
|
def get_peer_routes():
|
|
"""Get all peer routes."""
|
|
try:
|
|
routes = routing_manager.get_peer_routes()
|
|
return jsonify({"peer_routes": routes})
|
|
except Exception as e:
|
|
logger.error(f"Error getting peer routes: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/routing/firewall', methods=['GET'])
|
|
def get_firewall_rules():
|
|
"""Get all firewall rules."""
|
|
try:
|
|
rules = routing_manager.get_firewall_rules()
|
|
return jsonify({"firewall_rules": rules})
|
|
except Exception as e:
|
|
logger.error(f"Error getting firewall rules: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# Vault & Trust API (Phase 6)
|
|
@app.route('/api/vault/status', methods=['GET'])
|
|
def get_vault_status():
|
|
"""Get vault status."""
|
|
try:
|
|
status = current_app.vault_manager.get_status()
|
|
return jsonify(status)
|
|
except Exception as e:
|
|
logger.error(f"Error getting vault status: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/vault/certificates', methods=['GET'])
|
|
def get_certificates():
|
|
"""Get all certificates."""
|
|
try:
|
|
certificates = current_app.vault_manager.list_certificates()
|
|
return jsonify(certificates)
|
|
except Exception as e:
|
|
logger.error(f"Error getting certificates: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/vault/certificates', methods=['POST'])
|
|
def generate_certificate():
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = current_app.vault_manager.generate_certificate(
|
|
common_name=data['common_name'],
|
|
domains=data.get('domains', []),
|
|
key_size=data.get('key_size', 2048),
|
|
days=data.get('days', 365)
|
|
)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"Error generating certificate: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/vault/certificates/<common_name>', methods=['DELETE'])
|
|
def revoke_certificate(common_name):
|
|
"""Revoke certificate."""
|
|
try:
|
|
result = current_app.vault_manager.revoke_certificate(common_name)
|
|
return jsonify({"revoked": result})
|
|
except Exception as e:
|
|
logger.error(f"Error revoking certificate: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/vault/ca/certificate', methods=['GET'])
|
|
def get_ca_certificate():
|
|
"""Get CA certificate."""
|
|
try:
|
|
cert = current_app.vault_manager.get_ca_certificate()
|
|
return jsonify({"certificate": cert})
|
|
except Exception as e:
|
|
logger.error(f"Error getting CA certificate: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/vault/age/public-key', methods=['GET'])
|
|
def get_age_public_key():
|
|
"""Get Age public key."""
|
|
try:
|
|
key = current_app.vault_manager.get_age_public_key()
|
|
return jsonify({"public_key": key})
|
|
except Exception as e:
|
|
logger.error(f"Error getting Age public key: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/vault/trust/keys', methods=['GET'])
|
|
def get_trusted_keys():
|
|
"""Get trusted keys."""
|
|
try:
|
|
keys = current_app.vault_manager.get_trusted_keys()
|
|
return jsonify(keys)
|
|
except Exception as e:
|
|
logger.error(f"Error getting trusted keys: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/vault/trust/keys', methods=['POST'])
|
|
def add_trusted_key():
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = current_app.vault_manager.add_trusted_key(
|
|
name=data['name'],
|
|
public_key=data['public_key'],
|
|
trust_level=data.get('trust_level', 'direct')
|
|
)
|
|
return jsonify({"added": result})
|
|
except Exception as e:
|
|
logger.error(f"Error adding trusted key: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/vault/trust/keys/<name>', methods=['DELETE'])
|
|
def remove_trusted_key(name):
|
|
"""Remove trusted key."""
|
|
try:
|
|
result = current_app.vault_manager.remove_trusted_key(name)
|
|
return jsonify({"removed": result})
|
|
except Exception as e:
|
|
logger.error(f"Error removing trusted key: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/vault/trust/verify', methods=['POST'])
|
|
def verify_trust_chain():
|
|
try:
|
|
data = request.get_json(silent=True)
|
|
if data is None:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
result = current_app.vault_manager.verify_trust_chain(
|
|
peer_name=data['peer_name'],
|
|
signature=data['signature'],
|
|
data=data['data']
|
|
)
|
|
return jsonify({"verified": result})
|
|
except Exception as e:
|
|
logger.error(f"Error verifying trust chain: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/api/vault/trust/chains', methods=['GET'])
|
|
def get_trust_chains():
|
|
"""Get trust chains."""
|
|
try:
|
|
chains = current_app.vault_manager.get_trust_chains()
|
|
return jsonify(chains)
|
|
except Exception as e:
|
|
logger.error(f"Error getting trust chains: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# Services API
|
|
@app.route('/api/services/status', methods=['GET'])
|
|
def get_all_services_status():
|
|
"""Get status of all services."""
|
|
try:
|
|
# Use service bus to get status from all services
|
|
services_status = {}
|
|
for service_name in service_bus.list_services():
|
|
try:
|
|
service = service_bus.get_service(service_name)
|
|
status = service.get_status()
|
|
|
|
# Clean up status for UI consumption
|
|
if isinstance(status, dict):
|
|
# Extract core status information
|
|
clean_status = {
|
|
'status': status.get('status', 'unknown'),
|
|
'running': status.get('running', False),
|
|
'timestamp': status.get('timestamp', datetime.utcnow().isoformat())
|
|
}
|
|
|
|
# Add service-specific metrics
|
|
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
|
|
|
|
@app.route('/api/services/connectivity', methods=['GET'])
|
|
def test_all_services_connectivity():
|
|
"""Test connectivity of all services."""
|
|
try:
|
|
# Use service bus to test connectivity
|
|
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
|
|
|
|
@app.route('/api/health/history', methods=['GET'])
|
|
def get_health_history():
|
|
"""Get recent unified health check results."""
|
|
return jsonify(list(health_history))
|
|
|
|
@app.route('/api/logs', methods=['GET'])
|
|
def get_backend_logs():
|
|
"""Get backend log file contents (last N lines)."""
|
|
log_file = os.path.join(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
|
|
|
|
@app.route('/api/containers', methods=['GET'])
|
|
def list_containers():
|
|
if not is_local_request():
|
|
return jsonify({'error': 'Access denied'}), 403
|
|
try:
|
|
containers = container_manager.list_containers()
|
|
return jsonify(containers)
|
|
except Exception as e:
|
|
logger.error(f"Error listing containers: {e}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/containers/<name>/start', methods=['POST'])
|
|
def start_container(name):
|
|
if not is_local_request():
|
|
return jsonify({'error': 'Access denied'}), 403
|
|
try:
|
|
success = container_manager.start_container(name)
|
|
return jsonify({'started': success})
|
|
except Exception as e:
|
|
logger.error(f"Error starting container {name}: {e}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/containers/<name>/stop', methods=['POST'])
|
|
def stop_container(name):
|
|
if not is_local_request():
|
|
return jsonify({'error': 'Access denied'}), 403
|
|
try:
|
|
success = container_manager.stop_container(name)
|
|
return jsonify({'stopped': success})
|
|
except Exception as e:
|
|
logger.error(f"Error stopping container {name}: {e}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/containers/<name>/restart', methods=['POST'])
|
|
def restart_container(name):
|
|
if not is_local_request():
|
|
return jsonify({'error': 'Access denied'}), 403
|
|
try:
|
|
success = container_manager.restart_container(name)
|
|
return jsonify({'restarted': success})
|
|
except Exception as e:
|
|
logger.error(f"Error restarting container {name}: {e}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/containers/<name>/logs', methods=['GET'])
|
|
def get_container_logs(name):
|
|
# Temporarily disable access control for debugging
|
|
# if not is_local_request():
|
|
# return jsonify({'error': 'Access denied'}), 403
|
|
tail = request.args.get('tail', default=100, type=int)
|
|
try:
|
|
logs = container_manager.get_container_logs(name, tail=tail)
|
|
return jsonify({'logs': logs})
|
|
except Exception as e:
|
|
logger.error(f"Error getting logs for container {name}: {e}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/containers/<name>/stats', methods=['GET'])
|
|
def get_container_stats(name):
|
|
# Temporarily disable access control for debugging
|
|
# if not is_local_request():
|
|
# return jsonify({'error': 'Access denied'}), 403
|
|
try:
|
|
stats = container_manager.get_container_stats(name)
|
|
return jsonify(stats)
|
|
except Exception as e:
|
|
logger.error(f"Error getting stats for container {name}: {e}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/vault/secrets', methods=['GET'])
|
|
def list_secrets():
|
|
# Temporarily disable access control for debugging
|
|
# if not is_local_request():
|
|
# return jsonify({'error': 'Access denied'}), 403
|
|
secrets = app.vault_manager.list_secrets()
|
|
return jsonify({'secrets': secrets})
|
|
|
|
@app.route('/api/vault/secrets', methods=['POST'])
|
|
def store_secret():
|
|
# Temporarily disable access control for debugging
|
|
# if not is_local_request():
|
|
# return jsonify({'error': 'Access denied'}), 403
|
|
data = request.get_json(silent=True)
|
|
if not data or 'name' not in data or 'value' not in data:
|
|
return jsonify({'error': 'Missing name or value'}), 400
|
|
app.vault_manager.store_secret(data['name'], data['value'])
|
|
return jsonify({'stored': True})
|
|
|
|
@app.route('/api/vault/secrets/<name>', methods=['GET'])
|
|
def get_secret(name):
|
|
# Temporarily disable access control for debugging
|
|
# if not is_local_request():
|
|
# return jsonify({'error': 'Access denied'}), 403
|
|
value = app.vault_manager.get_secret(name)
|
|
if value is None:
|
|
return jsonify({'error': 'Not found'}), 404
|
|
return jsonify({'name': name, 'value': value})
|
|
|
|
@app.route('/api/vault/secrets/<name>', methods=['DELETE'])
|
|
def delete_secret(name):
|
|
# Temporarily disable access control for debugging
|
|
# if not is_local_request():
|
|
# return jsonify({'error': 'Access denied'}), 403
|
|
result = app.vault_manager.delete_secret(name)
|
|
return jsonify({'deleted': result})
|
|
|
|
# Enhance container creation to support secrets
|
|
@app.route('/api/containers', methods=['POST'])
|
|
def create_container():
|
|
# Temporarily disable access control for debugging
|
|
# if not is_local_request():
|
|
# return jsonify({'error': 'Access denied'}), 403
|
|
data = request.get_json(silent=True)
|
|
if not data or 'image' not in data:
|
|
return jsonify({'error': 'Missing image parameter'}), 400
|
|
name = data.get('name', '')
|
|
env = data.get('env', {})
|
|
# If 'secrets' is provided, resolve secret values and add to env
|
|
secrets = data.get('secrets', [])
|
|
if secrets:
|
|
for secret_name in secrets:
|
|
secret_value = app.vault_manager.get_secret(secret_name)
|
|
if secret_value is not None:
|
|
env[secret_name] = secret_value
|
|
volumes = data.get('volumes', {})
|
|
command = data.get('command', '')
|
|
ports = data.get('ports', {})
|
|
result = container_manager.create_container(
|
|
image=data['image'],
|
|
name=name,
|
|
env=env,
|
|
volumes=volumes,
|
|
command=command,
|
|
ports=ports
|
|
)
|
|
if 'error' in result:
|
|
return jsonify(result), 500
|
|
return jsonify(result)
|
|
|
|
@app.route('/api/containers/<name>', methods=['DELETE'])
|
|
def remove_container(name):
|
|
# Temporarily disable access control for debugging
|
|
# if not is_local_request():
|
|
# return jsonify({'error': 'Access denied'}), 403
|
|
force = request.args.get('force', default=False, type=bool)
|
|
success = container_manager.remove_container(name, force=force)
|
|
return jsonify({'removed': success})
|
|
|
|
@app.route('/api/images', methods=['GET'])
|
|
def list_images():
|
|
# Temporarily disable access control for debugging
|
|
# if not is_local_request():
|
|
# return jsonify({'error': 'Access denied'}), 403
|
|
images = container_manager.list_images()
|
|
return jsonify(images)
|
|
|
|
@app.route('/api/images/pull', methods=['POST'])
|
|
def pull_image():
|
|
if not is_local_request():
|
|
return jsonify({'error': 'Access denied'}), 403
|
|
data = request.get_json(silent=True)
|
|
if not data or 'image' not in data:
|
|
return jsonify({'error': 'Missing image parameter'}), 400
|
|
result = container_manager.pull_image(data['image'])
|
|
if 'error' in result:
|
|
return jsonify(result), 500
|
|
return jsonify(result)
|
|
|
|
@app.route('/api/images/<image>', methods=['DELETE'])
|
|
def remove_image(image):
|
|
if not is_local_request():
|
|
return jsonify({'error': 'Access denied'}), 403
|
|
force = request.args.get('force', default=False, type=bool)
|
|
success = container_manager.remove_image(image, force=force)
|
|
return jsonify({'removed': success})
|
|
|
|
@app.route('/api/volumes', methods=['GET'])
|
|
def list_volumes():
|
|
# Temporarily disable access control for debugging
|
|
# if not is_local_request():
|
|
# return jsonify({'error': 'Access denied'}), 403
|
|
volumes = container_manager.list_volumes()
|
|
return jsonify(volumes)
|
|
|
|
@app.route('/api/volumes', methods=['POST'])
|
|
def create_volume():
|
|
if not is_local_request():
|
|
return jsonify({'error': 'Access denied'}), 403
|
|
data = request.get_json(silent=True)
|
|
if not data or 'name' not in data:
|
|
return jsonify({'error': 'Missing name parameter'}), 400
|
|
result = container_manager.create_volume(data['name'])
|
|
if 'error' in result:
|
|
return jsonify(result), 500
|
|
return jsonify(result)
|
|
|
|
@app.route('/api/volumes/<name>', methods=['DELETE'])
|
|
def remove_volume(name):
|
|
if not is_local_request():
|
|
return jsonify({'error': 'Access denied'}), 403
|
|
force = request.args.get('force', default=False, type=bool)
|
|
success = container_manager.remove_volume(name, force=force)
|
|
return jsonify({'removed': success})
|
|
|
|
if __name__ == '__main__':
|
|
app.run(host='0.0.0.0', port=3000, debug=True) |