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:
@@ -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
|
||||
Reference in New Issue
Block a user