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

Extract 9 route groups out of app.py into routes/ blueprints:
- routes/network.py  — DNS, DHCP, NTP, network info/test (10 routes)
- routes/wireguard.py — WireGuard keys, peers, config, enforcement (18 routes)
- routes/cells.py    — cell-to-cell connections (5 routes)
- routes/peers.py    — peer CRUD + IP update + _next_peer_ip helper (10 routes)
- routes/routing.py  — NAT, peer routes, firewall, iptables (17 routes)
- routes/vault.py    — certs, trust, secrets (19 routes)
- routes/containers.py — containers, images, volumes (14 routes)
- routes/services.py — service bus, logs, services status/connectivity (18 routes)
- routes/peer_dashboard.py — peer-scoped dashboard/services (2 routes)

All blueprints use lazy `from app import X` inside route bodies to preserve
test patch compatibility (patch('app.email_manager', mock) still works).

Also included in this commit:
- A1 fix: backup/restore now includes email/calendar user files
- A2 fix: apply_config sets applying=True flag via helper container
- A3 fix: add_peer rolls back firewall on DNS failure

app.py reduced: 3011 → 1294 lines. 1021 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 06:11:21 -04:00
parent d54844cd44
commit 09138fbc18
16 changed files with 2108 additions and 2072 deletions
+207
View File
@@ -0,0 +1,207 @@
import logging
from flask import Blueprint, request, jsonify
logger = logging.getLogger('picell')
bp = Blueprint('routing', __name__)
@bp.route('/api/routing/status', methods=['GET'])
def get_routing_status():
try:
from app import routing_manager
return jsonify(routing_manager.get_status())
except Exception as e:
logger.error(f"Error getting routing status: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/setup', methods=['POST'])
def setup_routing():
try:
from app import routing_manager
status = routing_manager.get_status()
return jsonify({'success': True, 'message': 'Routing managed by WireGuard PostUp rules', **status})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/nat', methods=['GET'])
def get_nat_rules():
try:
from app import routing_manager
return jsonify({"nat_rules": routing_manager.get_nat_rules()})
except Exception as e:
logger.error(f"Error getting NAT rules: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/nat', methods=['POST'])
def add_nat_rule():
try:
from app import routing_manager
data = request.get_json(silent=True) or {}
result = routing_manager.add_nat_rule(
source_network=data.get('source_network'),
target_interface=data.get('target_interface'),
masquerade=data.get('masquerade', True),
nat_type=data.get('nat_type', 'MASQUERADE'),
protocol=data.get('protocol', 'ALL'),
external_port=data.get('external_port'),
internal_ip=data.get('internal_ip'),
internal_port=data.get('internal_port')
)
return jsonify({'success': result})
except Exception as e:
logger.error(f"Error adding NAT rule: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/nat/<rule_id>', methods=['DELETE'])
def remove_nat_rule(rule_id):
try:
from app import routing_manager
return jsonify(routing_manager.remove_nat_rule(rule_id))
except Exception as e:
logger.error(f"Error removing NAT rule: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/peers', methods=['GET'])
def get_peer_routes():
try:
from app import routing_manager
return jsonify({"peer_routes": routing_manager.get_peer_routes()})
except Exception as e:
logger.error(f"Error getting peer routes: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/peers', methods=['POST'])
def add_peer_route():
try:
from app import routing_manager
data = request.get_json(silent=True) or {}
peer_name = data.get('peer_name')
peer_ip = data.get('peer_ip')
if not peer_name or not peer_ip:
return jsonify({"error": "Missing required fields: peer_name, peer_ip"}), 400
result = routing_manager.add_peer_route(
peer_name, peer_ip,
data.get('allowed_networks', []),
data.get('route_type', 'lan')
)
return jsonify({"added": result})
except Exception as e:
logger.error(f"Error adding peer route: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/peers/<peer_name>', methods=['DELETE'])
def remove_peer_route(peer_name):
try:
from app import routing_manager
return jsonify(routing_manager.remove_peer_route(peer_name))
except Exception as e:
logger.error(f"Error removing peer route: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/exit-nodes', methods=['POST'])
def add_exit_node():
try:
from app import routing_manager
data = request.get_json(silent=True) or {}
peer_name = data.get('peer_name')
peer_ip = data.get('peer_ip')
if not peer_name or not peer_ip:
return jsonify({"error": "Missing required fields: peer_name, peer_ip"}), 400
return jsonify({"added": routing_manager.add_exit_node(peer_name, peer_ip, data.get('allowed_domains'))})
except Exception as e:
logger.error(f"Error adding exit node: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/bridge', methods=['POST'])
def add_bridge_route():
try:
from app import routing_manager
data = request.get_json(silent=True) or {}
source_peer = data.get('source_peer')
target_peer = data.get('target_peer')
if not source_peer or not target_peer:
return jsonify({"error": "Missing required fields: source_peer, target_peer"}), 400
return jsonify({"added": routing_manager.add_bridge_route(source_peer, target_peer, data.get('allowed_networks', []))})
except Exception as e:
logger.error(f"Error adding bridge route: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/split', methods=['POST'])
def add_split_route():
try:
from app import routing_manager
data = request.get_json(silent=True) or {}
network = data.get('network')
exit_peer = data.get('exit_peer')
if not network or not exit_peer:
return jsonify({"error": "Missing required fields: network, exit_peer"}), 400
return jsonify({"added": routing_manager.add_split_route(network, exit_peer, data.get('fallback_peer'))})
except Exception as e:
logger.error(f"Error adding split route: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/firewall', methods=['GET'])
def get_firewall_rules():
try:
from app import routing_manager
return jsonify({"firewall_rules": routing_manager.get_firewall_rules()})
except Exception as e:
logger.error(f"Error getting firewall rules: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/firewall', methods=['POST'])
def add_firewall_rule():
try:
from app import routing_manager
data = request.get_json(silent=True) or {}
result = routing_manager.add_firewall_rule(
rule_type=data.get('rule_type'),
source=data.get('source'),
destination=data.get('destination'),
action=data.get('action', 'ACCEPT'),
port=data.get('port'),
protocol=data.get('protocol', 'ALL'),
port_range=data.get('port_range')
)
return jsonify({'success': result})
except Exception as e:
logger.error(f"Error adding firewall rule: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/firewall/<rule_id>', methods=['DELETE'])
def remove_firewall_rule(rule_id):
try:
from app import routing_manager
result = routing_manager.remove_firewall_rule(rule_id)
return jsonify({'success': result}), (200 if result else 404)
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/routing/live-iptables', methods=['GET'])
def get_live_iptables():
try:
from app import routing_manager
return jsonify(routing_manager.get_live_iptables())
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/routing/connectivity', methods=['POST'])
def test_routing_connectivity():
try:
from app import routing_manager
data = request.get_json(silent=True) or {}
return jsonify(routing_manager.test_routing_connectivity(
data.get('target_ip', '8.8.8.8'),
data.get('via_peer')
))
except Exception as e:
logger.error(f"Error testing routing connectivity: {e}")
return jsonify({"error": str(e)}), 500
@bp.route('/api/routing/logs', methods=['GET'])
def get_routing_logs():
try:
from app import routing_manager
lines = request.args.get('lines', 50, type=int)
return jsonify(routing_manager.get_logs(lines))
except Exception as e:
logger.error(f"Error getting routing logs: {e}")
return jsonify({"error": str(e)}), 500