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