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:
+26
-2070
File diff suppressed because it is too large
Load Diff
@@ -235,6 +235,20 @@ class ConfigManager:
|
||||
for zone_file in dns_data.glob('*.zone'):
|
||||
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())
|
||||
manifest = {
|
||||
"backup_id": backup_id,
|
||||
@@ -316,6 +330,20 @@ class ConfigManager:
|
||||
except (PermissionError, OSError) as dir_err:
|
||||
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()
|
||||
logger.info(f"Restored configuration from backup: {backup_id}")
|
||||
return True
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user