09138fbc18
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>
196 lines
7.8 KiB
Python
196 lines
7.8 KiB
Python
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
|