A5: Extract all route groups into Flask blueprints (app.py -1735 lines)

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>
This commit is contained in:
2026-05-01 06:11:21 -04:00
parent d54844cd44
commit 09138fbc18
16 changed files with 2108 additions and 2072 deletions
+26 -2070
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -235,6 +235,20 @@ class ConfigManager:
for zone_file in dns_data.glob('*.zone'): for zone_file in dns_data.glob('*.zone'):
shutil.copy2(zone_file, zones_dir / zone_file.name) shutil.copy2(zone_file, zones_dir / zone_file.name)
# Service-specific user account files (authoritative source of truth —
# cell_config.json only carries a best-effort sync of these).
svc_user_files = [
(data_dir / 'email' / 'users.json', 'email_users.json'),
(data_dir / 'calendar' / 'users.json', 'calendar_users.json'),
(data_dir / 'calendar' / 'calendars.json', 'calendar_calendars.json'),
]
for src, dest_name in svc_user_files:
if src.exists():
try:
shutil.copy2(src, backup_path / dest_name)
except (PermissionError, OSError) as e:
logger.warning(f"Could not back up {src.name}: {e} (skipping)")
services = ['identity'] + list(self.service_schemas.keys()) services = ['identity'] + list(self.service_schemas.keys())
manifest = { manifest = {
"backup_id": backup_id, "backup_id": backup_id,
@@ -316,6 +330,20 @@ class ConfigManager:
except (PermissionError, OSError) as dir_err: except (PermissionError, OSError) as dir_err:
logger.warning(f"Could not create dns data dir {dns_data}: {dir_err} (skipping)") logger.warning(f"Could not create dns data dir {dns_data}: {dir_err} (skipping)")
# Service-specific user account files
svc_restore_map = [
(backup_path / 'email_users.json', data_dir / 'email' / 'users.json'),
(backup_path / 'calendar_users.json', data_dir / 'calendar' / 'users.json'),
(backup_path / 'calendar_calendars.json', data_dir / 'calendar' / 'calendars.json'),
]
for src, dest in svc_restore_map:
if src.exists():
try:
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dest)
except (PermissionError, OSError) as e:
logger.warning(f"Could not restore {dest.name}: {e} (skipping)")
self.configs = self._load_all_configs() self.configs = self._load_all_configs()
logger.info(f"Restored configuration from backup: {backup_id}") logger.info(f"Restored configuration from backup: {backup_id}")
return True return True
View File
+119
View File
@@ -0,0 +1,119 @@
import logging
from flask import Blueprint, request, jsonify
logger = logging.getLogger('picell')
bp = Blueprint('calendar', __name__)
@bp.route('/api/calendar/users', methods=['GET'])
def get_calendar_users():
"""Get calendar users."""
try:
from app import calendar_manager
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
@bp.route('/api/calendar/users', methods=['POST'])
def create_calendar_user():
"""Create calendar user."""
try:
from app import calendar_manager
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({"error": "Missing required fields: username, password"}), 400
result = calendar_manager.create_calendar_user(username, password)
return jsonify({"created": result})
except Exception as e:
logger.error(f"Error creating calendar user: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/calendar/users/<username>', methods=['DELETE'])
def delete_calendar_user(username):
"""Delete calendar user."""
try:
from app import calendar_manager
result = calendar_manager.delete_calendar_user(username)
return jsonify({"deleted": result})
except Exception as e:
logger.error(f"Error deleting calendar user: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/calendar/calendars', methods=['POST'])
def create_calendar():
"""Create calendar."""
try:
from app import calendar_manager
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
username = data.get('username')
calendar_name = data.get('name') or data.get('calendar_name')
if not username or not calendar_name:
return jsonify({"error": "Missing required fields: username, name"}), 400
result = calendar_manager.create_calendar(
username,
calendar_name,
description=data.get('description', ''),
color=data.get('color', '#4285f4'),
)
return jsonify({"created": result})
except Exception as e:
logger.error(f"Error creating calendar: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/calendar/events', methods=['POST'])
def add_calendar_event():
try:
from app import calendar_manager
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
username = data.get('username')
calendar_name = data.get('calendar_name') or data.get('calendar')
if not username or not calendar_name:
return jsonify({"error": "Missing required fields: username, calendar_name"}), 400
event_data = {k: v for k, v in data.items() if k not in ('username', 'calendar_name', 'calendar')}
result = calendar_manager.add_event(username, calendar_name, event_data)
return jsonify({"created": result})
except Exception as e:
logger.error(f"Error adding calendar event: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/calendar/events/<username>/<calendar_name>', methods=['GET'])
def get_calendar_events(username, calendar_name):
"""Get calendar events."""
try:
from app import calendar_manager
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
@bp.route('/api/calendar/status', methods=['GET'])
def get_calendar_status():
"""Get calendar service status."""
try:
from app import calendar_manager
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
@bp.route('/api/calendar/connectivity', methods=['GET'])
def test_calendar_connectivity():
"""Test calendar connectivity."""
try:
from app import calendar_manager
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
+65
View File
@@ -0,0 +1,65 @@
import logging
import os
from flask import Blueprint, request, jsonify
logger = logging.getLogger('picell')
bp = Blueprint('cells', __name__)
@bp.route('/api/cells/invite', methods=['GET'])
def get_cell_invite():
try:
from app import cell_link_manager, config_manager
identity = config_manager.configs.get('_identity', {})
cell_name = identity.get('cell_name', os.environ.get('CELL_NAME', 'mycell'))
domain = identity.get('domain', os.environ.get('CELL_DOMAIN', 'cell'))
return jsonify(cell_link_manager.generate_invite(cell_name, domain))
except Exception as e:
logger.error(f"Error generating cell invite: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/cells', methods=['GET'])
def list_cell_connections():
try:
from app import cell_link_manager
return jsonify(cell_link_manager.list_connections())
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/cells', methods=['POST'])
def add_cell_connection():
try:
from app import cell_link_manager
data = request.get_json(silent=True)
if not data:
return jsonify({'error': 'No data provided'}), 400
for field in ('cell_name', 'public_key', 'vpn_subnet', 'dns_ip', 'domain'):
if field not in data:
return jsonify({'error': f'Missing field: {field}'}), 400
link = cell_link_manager.add_connection(data)
return jsonify({'message': f"Connected to cell '{data['cell_name']}'", 'link': link}), 201
except ValueError as e:
return jsonify({'error': str(e)}), 400
except Exception as e:
logger.error(f"Error adding cell connection: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/cells/<cell_name>', methods=['DELETE'])
def remove_cell_connection(cell_name):
try:
from app import cell_link_manager
cell_link_manager.remove_connection(cell_name)
return jsonify({'message': f"Cell '{cell_name}' disconnected"})
except ValueError as e:
return jsonify({'error': str(e)}), 404
except Exception as e:
logger.error(f"Error removing cell connection: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/cells/<cell_name>/status', methods=['GET'])
def get_cell_connection_status(cell_name):
try:
from app import cell_link_manager
return jsonify(cell_link_manager.get_connection_status(cell_name))
except ValueError as e:
return jsonify({'error': str(e)}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
+195
View File
@@ -0,0 +1,195 @@
import logging
import os
from flask import Blueprint, request, jsonify, current_app
logger = logging.getLogger('picell')
bp = Blueprint('containers', __name__)
@bp.route('/api/containers', methods=['GET'])
def list_containers():
try:
from app import container_manager, is_local_request
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
return jsonify(container_manager.list_containers())
except Exception as e:
logger.error(f"Error listing containers: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/containers/<name>/start', methods=['POST'])
def start_container(name):
try:
from app import container_manager, is_local_request
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
return jsonify({'started': container_manager.start_container(name)})
except Exception as e:
logger.error(f"Error starting container {name}: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/containers/<name>/stop', methods=['POST'])
def stop_container(name):
try:
from app import container_manager, is_local_request
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
return jsonify({'stopped': container_manager.stop_container(name)})
except Exception as e:
logger.error(f"Error stopping container {name}: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/containers/<name>/restart', methods=['POST'])
def restart_container(name):
try:
from app import container_manager, is_local_request
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
return jsonify({'restarted': container_manager.restart_container(name)})
except Exception as e:
logger.error(f"Error restarting container {name}: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/containers/<name>/logs', methods=['GET'])
def get_container_logs(name):
try:
from app import container_manager, is_local_request
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
tail = request.args.get('tail', default=100, type=int)
return jsonify({'logs': container_manager.get_container_logs(name, tail=tail)})
except Exception as e:
logger.error(f"Error getting logs for container {name}: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/containers/<name>/stats', methods=['GET'])
def get_container_stats(name):
try:
from app import container_manager, is_local_request
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
return jsonify(container_manager.get_container_stats(name))
except Exception as e:
logger.error(f"Error getting stats for container {name}: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/containers', methods=['POST'])
def create_container():
try:
from app import container_manager, is_local_request
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', {})
secrets = data.get('secrets', [])
if secrets:
for secret_name in secrets:
secret_value = current_app.vault_manager.get_secret(secret_name)
if secret_value is not None:
env[secret_name] = secret_value
volumes = data.get('volumes', {})
if volumes:
allowed_prefixes = ('/home/roof/pic/data/', '/home/roof/pic/config/', '/tmp/')
for host_path in volumes.keys():
resolved = os.path.realpath(str(host_path))
if not any(resolved.startswith(p) for p in allowed_prefixes):
return jsonify({'error': f'Volume mount not allowed: {host_path}'}), 403
result = container_manager.create_container(
image=data['image'],
name=name,
env=env,
volumes=volumes,
command=data.get('command', ''),
ports=data.get('ports', {})
)
if 'error' in result:
return jsonify(result), 500
return jsonify(result)
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/containers/<name>', methods=['DELETE'])
def remove_container(name):
try:
from app import container_manager, is_local_request
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
force = request.args.get('force', default=False, type=bool)
return jsonify({'removed': container_manager.remove_container(name, force=force)})
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/images', methods=['GET'])
def list_images():
try:
from app import container_manager, is_local_request
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
return jsonify(container_manager.list_images())
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/images/pull', methods=['POST'])
def pull_image():
try:
from app import container_manager, is_local_request
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)
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/images/<image>', methods=['DELETE'])
def remove_image(image):
try:
from app import container_manager, is_local_request
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
force = request.args.get('force', default=False, type=bool)
return jsonify({'removed': container_manager.remove_image(image, force=force)})
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/volumes', methods=['GET'])
def list_volumes():
try:
from app import container_manager, is_local_request
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
return jsonify(container_manager.list_volumes())
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/volumes', methods=['POST'])
def create_volume():
try:
from app import container_manager, is_local_request
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)
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/volumes/<name>', methods=['DELETE'])
def remove_volume(name):
try:
from app import container_manager, is_local_request
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
force = request.args.get('force', default=False, type=bool)
return jsonify({'removed': container_manager.remove_volume(name, force=force)})
except Exception as e:
return jsonify({'error': str(e)}), 500
+92
View File
@@ -0,0 +1,92 @@
import logging
from flask import Blueprint, request, jsonify
logger = logging.getLogger('picell')
bp = Blueprint('email', __name__)
@bp.route('/api/email/users', methods=['GET'])
def get_email_users():
"""Get email users."""
try:
from app import email_manager
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
@bp.route('/api/email/users', methods=['POST'])
def create_email_user():
"""Create email user."""
try:
from app import email_manager, _configured_domain
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
username = data.get('username')
domain = data.get('domain') or _configured_domain()
password = data.get('password')
if not username or not password:
return jsonify({"error": "Missing required fields: username, password"}), 400
result = email_manager.create_email_user(username, domain, password)
return jsonify({"created": result})
except Exception as e:
logger.error(f"Error creating email user: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/email/users/<username>', methods=['DELETE'])
def delete_email_user(username):
"""Delete email user."""
try:
from app import email_manager, _configured_domain
domain = request.args.get('domain') or _configured_domain()
result = email_manager.delete_email_user(username, domain)
return jsonify({"deleted": result})
except Exception as e:
logger.error(f"Error deleting email user: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/email/status', methods=['GET'])
def get_email_status():
"""Get email service status."""
try:
from app import email_manager
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
@bp.route('/api/email/connectivity', methods=['GET'])
def test_email_connectivity():
"""Test email connectivity."""
try:
from app import email_manager
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
@bp.route('/api/email/send', methods=['POST'])
def send_email():
try:
from app import email_manager
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
@bp.route('/api/email/mailbox/<username>', methods=['GET'])
def get_mailbox_info(username):
"""Get mailbox information."""
try:
from app import email_manager
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
+159
View File
@@ -0,0 +1,159 @@
import logging
from flask import Blueprint, request, jsonify
logger = logging.getLogger('picell')
bp = Blueprint('files', __name__)
@bp.route('/api/files/users', methods=['GET'])
def get_file_users():
"""Get file storage users."""
try:
from app import file_manager
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
@bp.route('/api/files/users', methods=['POST'])
def create_file_user():
"""Create file storage user."""
try:
from app import file_manager
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({"error": "Missing required fields: username, password"}), 400
result = file_manager.create_user(username, password)
return jsonify({"created": result})
except Exception as e:
logger.error(f"Error creating file user: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/files/users/<username>', methods=['DELETE'])
def delete_file_user(username):
"""Delete file storage user."""
try:
from app import file_manager
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
@bp.route('/api/files/folders', methods=['POST'])
def create_folder():
"""Create folder."""
try:
from app import file_manager
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
username = data.get('username')
folder_path = data.get('folder_path') or data.get('path')
if not username or not folder_path:
return jsonify({"error": "Missing required fields: username, folder_path"}), 400
result = file_manager.create_folder(username, folder_path)
return jsonify({"created": result})
except ValueError as e:
return jsonify({"error": str(e)}), 400
except Exception as e:
logger.error(f"Error creating folder: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/files/folders/<username>/<path:folder_path>', methods=['DELETE'])
def delete_folder(username, folder_path):
"""Delete folder."""
try:
from app import file_manager
result = file_manager.delete_folder(username, folder_path)
return jsonify(result)
except ValueError as e:
return jsonify({"error": str(e)}), 400
except Exception as e:
logger.error(f"Error deleting folder: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/files/upload/<username>', methods=['POST'])
def upload_file(username):
"""Upload file."""
try:
from app import file_manager
if 'file' not in request.files:
return jsonify({"error": "No file provided"}), 400
file = request.files['file']
path = request.form.get('path', '') or file.filename or ''
file_data = file.read()
result = file_manager.upload_file(username, path, file_data)
return jsonify({"uploaded": result})
except ValueError as e:
return jsonify({"error": str(e)}), 400
except Exception as e:
logger.error(f"Error uploading file: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/files/download/<username>/<path:file_path>', methods=['GET'])
def download_file(username, file_path):
"""Download file."""
try:
from app import file_manager
result = file_manager.download_file(username, file_path)
return jsonify(result)
except ValueError as e:
return jsonify({"error": str(e)}), 400
except Exception as e:
logger.error(f"Error downloading file: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/files/delete/<username>/<path:file_path>', methods=['DELETE'])
def delete_file(username, file_path):
"""Delete file."""
try:
from app import file_manager
result = file_manager.delete_file(username, file_path)
return jsonify(result)
except ValueError as e:
return jsonify({"error": str(e)}), 400
except Exception as e:
logger.error(f"Error deleting file: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/files/list/<username>', methods=['GET'])
def list_files(username):
"""List files."""
try:
from app import file_manager
folder = request.args.get('folder', '')
result = file_manager.list_files(username, folder)
return jsonify(result)
except ValueError as e:
return jsonify({"error": str(e)}), 400
except Exception as e:
logger.error(f"Error listing files: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/files/status', methods=['GET'])
def get_file_status():
"""Get file service status."""
try:
from app import file_manager
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
@bp.route('/api/files/connectivity', methods=['GET'])
def test_file_connectivity():
"""Test file service connectivity."""
try:
from app import file_manager
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
+109
View File
@@ -0,0 +1,109 @@
import logging
from flask import Blueprint, request, jsonify
logger = logging.getLogger('picell')
bp = Blueprint('network', __name__)
@bp.route('/api/dns/records', methods=['GET'])
def get_dns_records():
try:
from app import network_manager
return jsonify(network_manager.get_dns_records())
except Exception as e:
logger.error(f"Error getting DNS records: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/dns/records', methods=['POST'])
def add_dns_record():
try:
from app import network_manager
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
return jsonify(network_manager.add_dns_record(**data))
except Exception as e:
logger.error(f"Error adding DNS record: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/dns/records', methods=['DELETE'])
def remove_dns_record():
try:
from app import network_manager
data = request.get_json(silent=True)
return jsonify(network_manager.remove_dns_record(**data))
except Exception as e:
logger.error(f"Error removing DNS record: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/dhcp/leases', methods=['GET'])
def get_dhcp_leases():
try:
from app import network_manager
return jsonify(network_manager.get_dhcp_leases())
except Exception as e:
logger.error(f"Error getting DHCP leases: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/dhcp/reservations', methods=['POST'])
def add_dhcp_reservation():
try:
from app import network_manager
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "No data provided"}), 400
for field in ('mac', 'ip'):
if field not in data:
return jsonify({"error": f"Missing required field: {field}"}), 400
result = network_manager.add_dhcp_reservation(data['mac'], data['ip'], data.get('hostname', ''))
return jsonify({"success": result})
except Exception as e:
logger.error(f"Error adding DHCP reservation: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/dhcp/reservations', methods=['DELETE'])
def remove_dhcp_reservation():
try:
from app import network_manager
data = request.get_json(silent=True)
if not data or 'mac' not in data:
return jsonify({"error": "Missing required field: mac"}), 400
result = network_manager.remove_dhcp_reservation(data['mac'])
return jsonify({"success": result})
except Exception as e:
logger.error(f"Error removing DHCP reservation: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/ntp/status', methods=['GET'])
def get_ntp_status():
try:
from app import network_manager
return jsonify(network_manager.get_ntp_status())
except Exception as e:
logger.error(f"Error getting NTP status: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/network/info', methods=['GET'])
def get_network_info():
try:
from app import network_manager
return jsonify(network_manager.get_network_info())
except Exception as e:
logger.error(f"Error getting network info: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/dns/status', methods=['GET'])
def get_dns_status():
try:
from app import network_manager
return jsonify(network_manager.get_dns_status())
except Exception as e:
logger.error(f"Error getting DNS status: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/network/test', methods=['POST'])
def test_network():
try:
from app import network_manager
return jsonify(network_manager.test_connectivity())
except Exception as e:
logger.error(f"Error testing network: {e}")
return jsonify({"error": str(e)}), 500
+115
View File
@@ -0,0 +1,115 @@
import logging
from flask import Blueprint, jsonify, session
logger = logging.getLogger('picell')
bp = Blueprint('peer_dashboard', __name__)
@bp.route('/api/peer/dashboard', methods=['GET'])
def peer_dashboard():
try:
from app import peer_registry, wireguard_manager, _configured_domain
peer_name = session.get('peer_name')
peer = peer_registry.get_peer(peer_name) if peer_name else None
if not peer:
return jsonify({'error': 'Peer not found'}), 404
wg_stats = {'online': None, 'transfer_rx': 0, 'transfer_tx': 0, 'last_handshake': None}
public_key = peer.get('public_key')
if public_key:
try:
wg_stats = wireguard_manager.get_peer_status(public_key)
except Exception:
pass
peer_ip = peer.get('ip', '')
allowed_ips = f"{peer_ip.split('/')[0]}/32" if peer_ip else ''
domain = _configured_domain()
_svc_url_map = {
'calendar': f'http://calendar.{domain}',
'files': f'http://files.{domain}',
'mail': f'http://mail.{domain}',
'webdav': f'http://webdav.{domain}',
}
service_urls = {
svc: _svc_url_map[svc]
for svc in peer.get('service_access', [])
if svc in _svc_url_map
}
return jsonify({
'name': peer_name,
'ip': peer_ip,
'service_access': peer.get('service_access', []),
'service_urls': service_urls,
'online': wg_stats.get('online'),
'transfer_rx': wg_stats.get('transfer_rx', 0),
'transfer_tx': wg_stats.get('transfer_tx', 0),
'last_handshake': wg_stats.get('last_handshake'),
'allowed_ips': peer.get('allowed_ips', allowed_ips),
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/peer/services', methods=['GET'])
def peer_services():
try:
from app import peer_registry, wireguard_manager, config_manager, _configured_domain, _resolve_peer_dns
peer_name = session.get('peer_name')
peer = peer_registry.get_peer(peer_name) if peer_name else None
if not peer:
return jsonify({'error': 'Peer not found'}), 404
domain = _configured_domain()
peer_ip = peer.get('ip', '')
server_public_key = ''
wg_port = 51820
server_endpoint = ''
try:
server_public_key = wireguard_manager.get_keys().get('public_key', '')
wg_port = config_manager.configs.get('_identity', {}).get('wireguard_port', 51820)
srv = wireguard_manager.get_server_config()
server_endpoint = srv.get('endpoint') or '<SERVER_IP>'
except Exception:
pass
wg_config = ''
peer_private_key = peer.get('private_key', '')
if peer_private_key:
try:
internet_access = peer.get('internet_access', True)
allowed_ips = wireguard_manager.FULL_TUNNEL_IPS if internet_access else wireguard_manager.get_split_tunnel_ips()
wg_config = 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,
)
except Exception:
pass
return jsonify({
'username': peer_name,
'wireguard': {
'ip': peer_ip,
'server_public_key': server_public_key,
'endpoint_port': wg_port,
'dns': _resolve_peer_dns(),
'config': wg_config,
},
'email': {
'address': f'{peer_name}@{domain}',
'smtp': {'host': f'mail.{domain}', 'port': 587},
'imap': {'host': f'mail.{domain}', 'port': 993},
},
'caldav': {
'url': f'http://calendar.{domain}',
'username': peer_name,
},
'files': {
'url': f'http://files.{domain}',
'username': peer_name,
},
})
except Exception as e:
return jsonify({'error': str(e)}), 500
+299
View File
@@ -0,0 +1,299 @@
import logging
import ipaddress
from flask import Blueprint, request, jsonify, session
logger = logging.getLogger('picell')
bp = Blueprint('peers', __name__)
def _next_peer_ip() -> str:
"""Auto-assign the next free host address from the configured VPN subnet."""
from app import wireguard_manager, peer_registry
server_addr = wireguard_manager._get_configured_address()
network = ipaddress.ip_network(server_addr, strict=False)
server_ip = str(ipaddress.ip_interface(server_addr).ip)
used = {p.get('ip', '').split('/')[0] for p in peer_registry.list_peers()}
for host in network.hosts():
ip = str(host)
if ip == server_ip:
continue
if ip not in used:
return ip
raise ValueError(f'No free IPs left in {network}')
@bp.route('/api/peers', methods=['GET'])
def get_peers():
try:
from app import peer_registry
return jsonify(peer_registry.list_peers())
except Exception as e:
logger.error(f"Error getting peers: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/peers', methods=['POST'])
def add_peer():
"""Add a peer and auto-provision auth/email/calendar/files accounts."""
try:
from app import (peer_registry, wireguard_manager, firewall_manager,
email_manager, calendar_manager, file_manager, auth_manager,
cell_link_manager, _configured_domain, COREFILE_PATH)
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
for field in ('name', 'public_key'):
if field not in data:
return jsonify({"error": f"Missing required field: {field}"}), 400
password = data.get('password') or ''
if not password:
return jsonify({"error": "Missing required field: password"}), 400
if len(password) < 10:
return jsonify({"error": "password must be at least 10 characters"}), 400
try:
assigned_ip = data.get('ip') or _next_peer_ip()
except ValueError as e:
return jsonify({'error': str(e)}), 409
_valid_services = {'calendar', 'files', 'mail', 'webdav'}
service_access = data.get('service_access', list(_valid_services))
if not isinstance(service_access, list) or not all(s in _valid_services for s in service_access):
return jsonify({"error": f"service_access must be a list of: {sorted(_valid_services)}"}), 400
peer_name = data['name']
if not auth_manager.create_user(peer_name, password, 'peer'):
return jsonify({"error": "Could not create auth account (duplicate name?)"}), 400
provisioned = ['auth']
domain = _configured_domain()
for step_name, step_fn in [
('email', lambda: email_manager.create_email_user(peer_name, domain, password)),
('calendar', lambda: calendar_manager.create_calendar_user(peer_name, password)),
('files', lambda: file_manager.create_user(peer_name, password)),
]:
try:
if step_fn():
provisioned.append(step_name)
else:
logger.warning(f"Peer {peer_name}: {step_name} account creation returned False")
except Exception as e:
logger.warning(f"Peer {peer_name}: {step_name} account creation failed (non-fatal): {e}")
peer_info = {
'peer': peer_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': service_access,
'peer_access': data.get('peer_access', True),
'config_needs_reinstall': False,
}
peer_added_to_registry = False
firewall_applied = False
try:
success = peer_registry.add_peer(peer_info)
if not success:
for svc in ('files', 'calendar', 'email', 'auth'):
try:
if svc == 'files':
file_manager.delete_user(peer_name)
elif svc == 'calendar':
calendar_manager.delete_calendar_user(peer_name)
elif svc == 'email':
email_manager.delete_email_user(peer_name, _configured_domain())
elif svc == 'auth':
auth_manager.delete_user(peer_name)
except Exception:
pass
return jsonify({"error": f"Peer {peer_name} already exists"}), 400
peer_added_to_registry = True
firewall_manager.apply_peer_rules(peer_info['ip'], peer_info)
firewall_applied = True
wg_allowed = f"{assigned_ip}/32" if '/' not in assigned_ip else assigned_ip
try:
wireguard_manager.add_peer(peer_name, data['public_key'], endpoint_ip='', allowed_ips=wg_allowed)
except Exception as wg_err:
logger.warning(f"Peer {peer_name}: WireGuard server config update failed (non-fatal): {wg_err}")
firewall_manager.apply_all_dns_rules(peer_registry.list_peers(), COREFILE_PATH, _configured_domain(),
cell_links=cell_link_manager.list_connections())
return jsonify({"message": f"Peer {peer_name} added successfully", "ip": assigned_ip}), 201
except Exception as e:
if firewall_applied:
try:
firewall_manager.clear_peer_rules(peer_info['ip'])
except Exception:
pass
if peer_added_to_registry:
try:
peer_registry.remove_peer(peer_name)
except Exception:
pass
logger.error(f"Error adding peer {peer_name}: {e}")
return jsonify({'error': str(e)}), 500
except Exception as e:
logger.error(f"Error adding peer: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/peers/<peer_name>', methods=['PUT'])
def update_peer(peer_name):
try:
from app import peer_registry, firewall_manager, cell_link_manager, _configured_domain, COREFILE_PATH
data = request.get_json(silent=True) or {}
existing = peer_registry.get_peer(peer_name)
if not existing:
return jsonify({"error": "Peer not found"}), 404
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:
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, _configured_domain(),
cell_links=cell_link_manager.list_connections())
return jsonify({"message": f"Peer {peer_name} updated", "config_changed": config_changed})
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
@bp.route('/api/peers/<peer_name>/clear-reinstall', methods=['POST'])
def clear_peer_reinstall(peer_name):
try:
from app import peer_registry
peer_registry.clear_reinstall_flag(peer_name)
return jsonify({"message": "Reinstall flag cleared"})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route('/api/peers/<peer_name>', methods=['DELETE'])
def remove_peer(peer_name):
try:
from app import (peer_registry, wireguard_manager, firewall_manager,
email_manager, calendar_manager, file_manager, auth_manager,
cell_link_manager, _configured_domain, COREFILE_PATH)
peer = peer_registry.get_peer(peer_name)
if not peer:
return jsonify({"message": f"Peer {peer_name} not found or already removed"})
peer_ip = peer.get('ip')
peer_pubkey = peer.get('public_key', '')
success = peer_registry.remove_peer(peer_name)
if success:
if peer_ip:
firewall_manager.clear_peer_rules(peer_ip)
firewall_manager.apply_all_dns_rules(peer_registry.list_peers(), COREFILE_PATH, _configured_domain(),
cell_links=cell_link_manager.list_connections())
if peer_pubkey:
try:
wireguard_manager.remove_peer(peer_pubkey)
except Exception as wg_err:
logger.warning(f"Peer {peer_name}: WireGuard removal failed (non-fatal): {wg_err}")
for _cleanup in [
lambda: email_manager.delete_email_user(peer_name, _configured_domain()),
lambda: calendar_manager.delete_calendar_user(peer_name),
lambda: file_manager.delete_user(peer_name),
lambda: auth_manager.delete_user(peer_name),
]:
try:
_cleanup()
except Exception:
pass
return jsonify({"message": f"Peer {peer_name} removed successfully"})
except Exception as e:
logger.error(f"Error removing peer: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/peers/register', methods=['POST'])
def register_peer():
try:
from app import peer_registry
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
return jsonify(peer_registry.register_peer(data))
except Exception as e:
logger.error(f"Error registering peer: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/peers/<peer_name>/unregister', methods=['DELETE'])
def unregister_peer(peer_name):
try:
from app import peer_registry
return jsonify(peer_registry.unregister_peer(peer_name))
except Exception as e:
logger.error(f"Error unregistering peer: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/peers/<peer_name>/update-ip', methods=['PUT'])
def update_peer_ip_registry(peer_name):
try:
from app import peer_registry, routing_manager
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:
try:
routing_manager.update_peer_ip(peer_name, new_ip)
except Exception as e:
logger.warning(f"RoutingManager update_peer_ip failed: {e}")
return jsonify({"message": f"IP update received for {peer_name}"})
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
@bp.route('/api/ip-update', methods=['POST'])
def ip_update():
try:
from app import peer_registry, routing_manager
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:
try:
routing_manager.update_peer_ip(peer_name, new_ip)
except Exception as e:
logger.warning(f"RoutingManager update_peer_ip failed: {e}")
return jsonify({"message": f"IP update received for {peer_name}"})
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
+207
View File
@@ -0,0 +1,207 @@
import logging
from flask import Blueprint, request, jsonify
logger = logging.getLogger('picell')
bp = Blueprint('routing', __name__)
@bp.route('/api/routing/status', methods=['GET'])
def get_routing_status():
try:
from app import routing_manager
return jsonify(routing_manager.get_status())
except Exception as e:
logger.error(f"Error getting routing status: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/setup', methods=['POST'])
def setup_routing():
try:
from app import routing_manager
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
@bp.route('/api/routing/nat', methods=['GET'])
def get_nat_rules():
try:
from app import routing_manager
return jsonify({"nat_rules": routing_manager.get_nat_rules()})
except Exception as e:
logger.error(f"Error getting NAT rules: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/nat', methods=['POST'])
def add_nat_rule():
try:
from app import routing_manager
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
@bp.route('/api/routing/nat/<rule_id>', methods=['DELETE'])
def remove_nat_rule(rule_id):
try:
from app import routing_manager
return jsonify(routing_manager.remove_nat_rule(rule_id))
except Exception as e:
logger.error(f"Error removing NAT rule: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/peers', methods=['GET'])
def get_peer_routes():
try:
from app import routing_manager
return jsonify({"peer_routes": routing_manager.get_peer_routes()})
except Exception as e:
logger.error(f"Error getting peer routes: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/peers', methods=['POST'])
def add_peer_route():
try:
from app import routing_manager
data = request.get_json(silent=True) or {}
peer_name = data.get('peer_name')
peer_ip = data.get('peer_ip')
if not peer_name or not peer_ip:
return jsonify({"error": "Missing required fields: peer_name, peer_ip"}), 400
result = routing_manager.add_peer_route(
peer_name, peer_ip,
data.get('allowed_networks', []),
data.get('route_type', 'lan')
)
return jsonify({"added": result})
except Exception as e:
logger.error(f"Error adding peer route: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/peers/<peer_name>', methods=['DELETE'])
def remove_peer_route(peer_name):
try:
from app import routing_manager
return jsonify(routing_manager.remove_peer_route(peer_name))
except Exception as e:
logger.error(f"Error removing peer route: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/exit-nodes', methods=['POST'])
def add_exit_node():
try:
from app import routing_manager
data = request.get_json(silent=True) or {}
peer_name = data.get('peer_name')
peer_ip = data.get('peer_ip')
if not peer_name or not peer_ip:
return jsonify({"error": "Missing required fields: peer_name, peer_ip"}), 400
return jsonify({"added": routing_manager.add_exit_node(peer_name, peer_ip, data.get('allowed_domains'))})
except Exception as e:
logger.error(f"Error adding exit node: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/bridge', methods=['POST'])
def add_bridge_route():
try:
from app import routing_manager
data = request.get_json(silent=True) or {}
source_peer = data.get('source_peer')
target_peer = data.get('target_peer')
if not source_peer or not target_peer:
return jsonify({"error": "Missing required fields: source_peer, target_peer"}), 400
return jsonify({"added": routing_manager.add_bridge_route(source_peer, target_peer, data.get('allowed_networks', []))})
except Exception as e:
logger.error(f"Error adding bridge route: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/split', methods=['POST'])
def add_split_route():
try:
from app import routing_manager
data = request.get_json(silent=True) or {}
network = data.get('network')
exit_peer = data.get('exit_peer')
if not network or not exit_peer:
return jsonify({"error": "Missing required fields: network, exit_peer"}), 400
return jsonify({"added": routing_manager.add_split_route(network, exit_peer, data.get('fallback_peer'))})
except Exception as e:
logger.error(f"Error adding split route: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/firewall', methods=['GET'])
def get_firewall_rules():
try:
from app import routing_manager
return jsonify({"firewall_rules": routing_manager.get_firewall_rules()})
except Exception as e:
logger.error(f"Error getting firewall rules: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/firewall', methods=['POST'])
def add_firewall_rule():
try:
from app import routing_manager
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
@bp.route('/api/routing/firewall/<rule_id>', methods=['DELETE'])
def remove_firewall_rule(rule_id):
try:
from app import routing_manager
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
@bp.route('/api/routing/live-iptables', methods=['GET'])
def get_live_iptables():
try:
from app import routing_manager
return jsonify(routing_manager.get_live_iptables())
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/routing/connectivity', methods=['POST'])
def test_routing_connectivity():
try:
from app import routing_manager
data = request.get_json(silent=True) or {}
return jsonify(routing_manager.test_routing_connectivity(
data.get('target_ip', '8.8.8.8'),
data.get('via_peer')
))
except Exception as e:
logger.error(f"Error testing routing connectivity: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/logs', methods=['GET'])
def get_routing_logs():
try:
from app import routing_manager
lines = request.args.get('lines', 50, type=int)
return jsonify(routing_manager.get_logs(lines))
except Exception as e:
logger.error(f"Error getting routing logs: {e}")
return jsonify({"error": str(e)}), 500
+291
View File
@@ -0,0 +1,291 @@
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
+165
View File
@@ -0,0 +1,165 @@
import logging
import os
from flask import Blueprint, request, jsonify, current_app
logger = logging.getLogger('picell')
bp = Blueprint('vault', __name__)
@bp.route('/api/vault/status', methods=['GET'])
def get_vault_status():
try:
return jsonify(current_app.vault_manager.get_status())
except Exception as e:
logger.error(f"Error getting vault status: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/vault/certificates', methods=['GET'])
def get_certificates():
try:
return jsonify(current_app.vault_manager.list_certificates())
except Exception as e:
logger.error(f"Error getting certificates: {e}")
return jsonify({"error": str(e)}), 500
@bp.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
@bp.route('/api/vault/certificates/<common_name>', methods=['DELETE'])
def revoke_certificate(common_name):
try:
return jsonify({"revoked": current_app.vault_manager.revoke_certificate(common_name)})
except Exception as e:
logger.error(f"Error revoking certificate: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/vault/ca/certificate', methods=['GET'])
def get_ca_certificate():
try:
return jsonify({"certificate": current_app.vault_manager.get_ca_certificate()})
except Exception as e:
logger.error(f"Error getting CA certificate: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/vault/age/public-key', methods=['GET'])
def get_age_public_key():
try:
return jsonify({"public_key": current_app.vault_manager.get_age_public_key()})
except Exception as e:
logger.error(f"Error getting Age public key: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/vault/trust/keys', methods=['GET'])
def get_trusted_keys():
try:
return jsonify(current_app.vault_manager.get_trusted_keys())
except Exception as e:
logger.error(f"Error getting trusted keys: {e}")
return jsonify({"error": str(e)}), 500
@bp.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
@bp.route('/api/vault/trust/keys/<name>', methods=['DELETE'])
def remove_trusted_key(name):
try:
return jsonify({"removed": current_app.vault_manager.remove_trusted_key(name)})
except Exception as e:
logger.error(f"Error removing trusted key: {e}")
return jsonify({"error": str(e)}), 500
@bp.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
@bp.route('/api/vault/trust/chains', methods=['GET'])
def get_trust_chains():
try:
return jsonify(current_app.vault_manager.get_trust_chains())
except Exception as e:
logger.error(f"Error getting trust chains: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/vault/secrets', methods=['GET'])
def list_secrets():
try:
from app import is_local_request
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
return jsonify({'secrets': current_app.vault_manager.list_secrets()})
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/vault/secrets', methods=['POST'])
def store_secret():
try:
from app import is_local_request
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
current_app.vault_manager.store_secret(data['name'], data['value'])
return jsonify({'stored': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/vault/secrets/<name>', methods=['GET'])
def get_secret(name):
try:
from app import is_local_request
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
value = current_app.vault_manager.get_secret(name)
if value is None:
return jsonify({'error': 'Not found'}), 404
return jsonify({'name': name, 'value': value})
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/vault/secrets/<name>', methods=['DELETE'])
def delete_secret(name):
try:
from app import is_local_request
if not is_local_request():
return jsonify({'error': 'Access denied'}), 403
return jsonify({'deleted': current_app.vault_manager.delete_secret(name)})
except Exception as e:
return jsonify({'error': str(e)}), 500
+236
View File
@@ -0,0 +1,236 @@
import logging
from flask import Blueprint, request, jsonify
logger = logging.getLogger('picell')
bp = Blueprint('wireguard', __name__)
@bp.route('/api/wireguard/keys', methods=['GET'])
def get_wireguard_keys():
try:
from app import wireguard_manager
keys = wireguard_manager.get_keys()
return jsonify({
'public_key': keys.get('public_key', ''),
'has_private_key': bool(keys.get('private_key')),
})
except Exception as e:
logger.error(f"Error getting WireGuard keys: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/keys/peer', methods=['POST'])
def generate_peer_keys():
try:
from app import wireguard_manager
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
return jsonify(wireguard_manager.generate_peer_keys(name))
except Exception as e:
logger.error(f"Error generating peer keys: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/config', methods=['GET'])
def get_wireguard_config():
try:
from app import wireguard_manager
return jsonify(wireguard_manager.get_config())
except Exception as e:
logger.error(f"Error getting WireGuard config: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/peers', methods=['GET'])
def get_wireguard_peers():
try:
from app import wireguard_manager
return jsonify(wireguard_manager.get_peers())
except Exception as e:
logger.error(f"Error getting WireGuard peers: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/peers', methods=['POST'])
def add_wireguard_peer():
try:
from app import wireguard_manager
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
@bp.route('/api/wireguard/peers', methods=['DELETE'])
def remove_wireguard_peer():
try:
from app import wireguard_manager
data = request.get_json(silent=True) or {}
public_key = data.get('public_key') or data.get('name', '')
return jsonify({"success": wireguard_manager.remove_peer(public_key)})
except Exception as e:
logger.error(f"Error removing WireGuard peer: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/status', methods=['GET'])
def get_wireguard_status():
try:
from app import wireguard_manager
return jsonify(wireguard_manager.get_status())
except Exception as e:
logger.error(f"Error getting WireGuard status: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/connectivity', methods=['POST'])
def test_wireguard_connectivity():
try:
from app import wireguard_manager
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
return jsonify(wireguard_manager.test_connectivity(data))
except Exception as e:
logger.error(f"Error testing WireGuard connectivity: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/peers/ip', methods=['PUT'])
def update_peer_ip():
try:
from app import wireguard_manager
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
@bp.route('/api/wireguard/peers/status', methods=['POST'])
def get_peer_status():
try:
from app import wireguard_manager
data = request.get_json(silent=True) or {}
public_key = data.get('public_key', '')
if not public_key:
return jsonify({"error": "Missing public_key"}), 400
return jsonify(wireguard_manager.get_peer_status(public_key))
except Exception as e:
logger.error(f"Error getting peer status: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/peers/statuses', methods=['GET'])
def get_all_peer_statuses():
try:
from app import wireguard_manager
return jsonify(wireguard_manager.get_all_peer_statuses())
except Exception as e:
logger.error(f"Error getting peer statuses: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/network/setup', methods=['POST'])
def setup_network():
try:
from app import wireguard_manager
success = wireguard_manager.setup_network_configuration()
if success:
return jsonify({"message": "Network configuration setup completed successfully"})
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
@bp.route('/api/wireguard/network/status', methods=['GET'])
def get_network_status():
try:
from app import wireguard_manager
return jsonify(wireguard_manager.get_network_status())
except Exception as e:
logger.error(f"Error getting network status: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/peers/config', methods=['POST'])
def get_peer_config():
try:
from app import wireguard_manager, peer_registry
data = request.get_json(silent=True) or {}
peer_name = data.get('name', data.get('peer', ''))
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', '')
server_endpoint = data.get('server_endpoint', '')
if not server_endpoint:
srv = wireguard_manager.get_server_config()
server_endpoint = srv.get('endpoint') or '<SERVER_IP>'
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.get_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
@bp.route('/api/wireguard/server-config', methods=['GET'])
def get_server_config():
try:
from app import wireguard_manager
return jsonify(wireguard_manager.get_server_config())
except Exception as e:
logger.error(f"Error getting server config: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/refresh-ip', methods=['GET', 'POST'])
def refresh_external_ip():
try:
from app import wireguard_manager
ip = wireguard_manager.get_external_ip(force_refresh=True)
port = wireguard_manager._get_configured_port()
return jsonify({
'external_ip': ip,
'port': port,
'endpoint': f'{ip}:{port}' if ip else None,
})
except Exception as e:
logger.error(f"Error refreshing external IP: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/wireguard/apply-enforcement', methods=['POST'])
def apply_wireguard_enforcement():
try:
from app import peer_registry, firewall_manager, cell_link_manager, _configured_domain, COREFILE_PATH
peers = peer_registry.list_peers()
firewall_manager.apply_all_peer_rules(peers)
firewall_manager.apply_all_dns_rules(peers, COREFILE_PATH, _configured_domain(),
cell_links=cell_link_manager.list_connections())
return jsonify({'ok': True, 'peers': len(peers)})
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/wireguard/check-port', methods=['GET', 'POST'])
def check_wireguard_port():
try:
from app import wireguard_manager
port_open = wireguard_manager.check_port_open()
return jsonify({'port_open': port_open, 'port': wireguard_manager._get_configured_port()})
except Exception as e:
return jsonify({"error": str(e)}), 500
+2 -2
View File
@@ -31,7 +31,7 @@ class TestAddPeerSubnetExhaustion(unittest.TestCase):
app.config['TESTING'] = True app.config['TESTING'] = True
self.client = app.test_client() self.client = app.test_client()
@patch('app._next_peer_ip') @patch('routes.peers._next_peer_ip')
@patch('app.auth_manager') @patch('app.auth_manager')
def test_add_peer_returns_409_when_subnet_exhausted(self, mock_auth, mock_next_ip): def test_add_peer_returns_409_when_subnet_exhausted(self, mock_auth, mock_next_ip):
mock_auth.create_user.return_value = True mock_auth.create_user.return_value = True
@@ -50,7 +50,7 @@ class TestAddPeerSubnetExhaustion(unittest.TestCase):
data = json.loads(r.data) data = json.loads(r.data)
self.assertIn('error', data) self.assertIn('error', data)
@patch('app._next_peer_ip') @patch('routes.peers._next_peer_ip')
@patch('app.auth_manager') @patch('app.auth_manager')
def test_add_peer_409_error_message_mentions_ip(self, mock_auth, mock_next_ip): def test_add_peer_409_error_message_mentions_ip(self, mock_auth, mock_next_ip):
mock_auth.create_user.return_value = True mock_auth.create_user.return_value = True