Files
ProxMenux/AppImage/scripts/flask_oci_routes.py
2026-03-16 17:46:34 +01:00

546 lines
14 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ProxMenux OCI Routes
REST API endpoints for OCI container app management.
"""
import logging
from flask import Blueprint, jsonify, request
import oci_manager
from jwt_middleware import require_auth
# Logging
logger = logging.getLogger("proxmenux.oci.routes")
# Blueprint
oci_bp = Blueprint("oci", __name__, url_prefix="/api/oci")
# =================================================================
# Catalog Endpoints
# =================================================================
@oci_bp.route("/catalog", methods=["GET"])
@require_auth
def get_catalog():
"""
List all available apps from the catalog.
Returns:
List of apps with basic info and installation status.
"""
try:
apps = oci_manager.list_available_apps()
return jsonify({
"success": True,
"apps": apps
})
except Exception as e:
logger.error(f"Failed to get catalog: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
@oci_bp.route("/catalog/<app_id>", methods=["GET"])
@require_auth
def get_app_definition(app_id: str):
"""
Get the full definition for a specific app.
Args:
app_id: The app identifier
Returns:
Full app definition including config schema.
"""
try:
app_def = oci_manager.get_app_definition(app_id)
if not app_def:
return jsonify({
"success": False,
"message": f"App '{app_id}' not found in catalog"
}), 404
return jsonify({
"success": True,
"app": app_def,
"installed": oci_manager.is_installed(app_id)
})
except Exception as e:
logger.error(f"Failed to get app definition: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
@oci_bp.route("/storages", methods=["GET"])
@require_auth
def get_storages():
"""
Get list of available storages for LXC rootfs.
Returns:
List of storages with capacity info and recommendations.
"""
try:
storages = oci_manager.get_available_storages()
return jsonify({
"success": True,
"storages": storages
})
except Exception as e:
logger.error(f"Failed to get storages: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
@oci_bp.route("/catalog/<app_id>/schema", methods=["GET"])
@require_auth
def get_app_schema(app_id: str):
"""
Get only the config schema for an app.
Args:
app_id: The app identifier
Returns:
Config schema for building dynamic forms.
"""
try:
app_def = oci_manager.get_app_definition(app_id)
if not app_def:
return jsonify({
"success": False,
"message": f"App '{app_id}' not found in catalog"
}), 404
return jsonify({
"success": True,
"app_id": app_id,
"name": app_def.get("name", app_id),
"schema": app_def.get("config_schema", {})
})
except Exception as e:
logger.error(f"Failed to get app schema: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
# =================================================================
# Installed Apps Endpoints
# =================================================================
@oci_bp.route("/installed", methods=["GET"])
@require_auth
def list_installed():
"""
List all installed apps with their current status.
Returns:
List of installed apps with status info.
"""
try:
apps = oci_manager.list_installed_apps()
return jsonify({
"success": True,
"instances": apps
})
except Exception as e:
logger.error(f"Failed to list installed apps: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
@oci_bp.route("/installed/<app_id>", methods=["GET"])
@require_auth
def get_installed_app(app_id: str):
"""
Get details of an installed app including current status.
Args:
app_id: The app identifier
Returns:
Installed app details with container info and status.
"""
try:
app = oci_manager.get_installed_app(app_id)
if not app:
return jsonify({
"success": False,
"message": f"App '{app_id}' is not installed"
}), 404
return jsonify({
"success": True,
"instance": app
})
except Exception as e:
logger.error(f"Failed to get installed app: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
@oci_bp.route("/installed/<app_id>/logs", methods=["GET"])
@require_auth
def get_app_logs(app_id: str):
"""
Get recent logs from an app's container.
Args:
app_id: The app identifier
Query params:
lines: Number of lines to return (default 100)
Returns:
Container logs.
"""
try:
lines = request.args.get("lines", 100, type=int)
result = oci_manager.get_app_logs(app_id, lines=lines)
if not result.get("success"):
return jsonify(result), 404 if "not installed" in result.get("message", "") else 500
return jsonify(result)
except Exception as e:
logger.error(f"Failed to get app logs: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
# =================================================================
# Deployment Endpoint
# =================================================================
@oci_bp.route("/deploy", methods=["POST"])
@require_auth
def deploy_app():
"""
Deploy an OCI app with the given configuration.
Body:
{
"app_id": "secure-gateway",
"config": {
"auth_key": "tskey-auth-xxx",
"hostname": "proxmox-gateway",
...
}
}
Returns:
Deployment result with container ID if successful.
"""
try:
data = request.get_json()
if not data:
return jsonify({
"success": False,
"message": "Request body is required"
}), 400
app_id = data.get("app_id")
config = data.get("config", {})
if not app_id:
return jsonify({
"success": False,
"message": "app_id is required"
}), 400
logger.info(f"Deploy request: app_id={app_id}, config_keys={list(config.keys())}")
result = oci_manager.deploy_app(app_id, config, installed_by="web")
logger.info(f"Deploy result: {result}")
status_code = 200 if result.get("success") else 400
return jsonify(result), status_code
except Exception as e:
logger.error(f"Failed to deploy app: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
# =================================================================
# Lifecycle Action Endpoints
# =================================================================
@oci_bp.route("/installed/<app_id>/start", methods=["POST"])
@require_auth
def start_app(app_id: str):
"""Start an installed app's container."""
try:
result = oci_manager.start_app(app_id)
status_code = 200 if result.get("success") else 400
return jsonify(result), status_code
except Exception as e:
logger.error(f"Failed to start app: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
@oci_bp.route("/installed/<app_id>/stop", methods=["POST"])
@require_auth
def stop_app(app_id: str):
"""Stop an installed app's container."""
try:
result = oci_manager.stop_app(app_id)
status_code = 200 if result.get("success") else 400
return jsonify(result), status_code
except Exception as e:
logger.error(f"Failed to stop app: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
@oci_bp.route("/installed/<app_id>/restart", methods=["POST"])
@require_auth
def restart_app(app_id: str):
"""Restart an installed app's container."""
try:
result = oci_manager.restart_app(app_id)
status_code = 200 if result.get("success") else 400
return jsonify(result), status_code
except Exception as e:
logger.error(f"Failed to restart app: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
@oci_bp.route("/installed/<app_id>", methods=["DELETE"])
@require_auth
def remove_app(app_id: str):
"""
Remove an installed app.
Query params:
remove_data: If true, also remove persistent data (default false)
"""
try:
remove_data = request.args.get("remove_data", "false").lower() == "true"
result = oci_manager.remove_app(app_id, remove_data=remove_data)
status_code = 200 if result.get("success") else 400
return jsonify(result), status_code
except Exception as e:
logger.error(f"Failed to remove app: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
# =================================================================
# Configuration Update Endpoint
# =================================================================
@oci_bp.route("/installed/<app_id>/config", methods=["PUT"])
@require_auth
def update_app_config(app_id: str):
"""
Update an app's configuration and recreate the container.
Body:
{
"config": { ... new config values ... }
}
"""
try:
data = request.get_json()
if not data or "config" not in data:
return jsonify({
"success": False,
"message": "config is required in request body"
}), 400
result = oci_manager.update_app_config(app_id, data["config"])
status_code = 200 if result.get("success") else 400
return jsonify(result), status_code
except Exception as e:
logger.error(f"Failed to update app config: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
# =================================================================
# Utility Endpoints
# =================================================================
@oci_bp.route("/networks", methods=["GET"])
@require_auth
def get_networks():
"""
Get available networks for VPN routing.
Returns:
List of detected network interfaces with their subnets.
"""
try:
networks = oci_manager.detect_networks()
return jsonify({
"success": True,
"networks": networks
})
except Exception as e:
logger.error(f"Failed to detect networks: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
@oci_bp.route("/runtime", methods=["GET"])
@require_auth
def get_runtime():
"""
Get container runtime information.
Returns:
Runtime type (podman/docker), version, and availability.
"""
try:
runtime_info = oci_manager.detect_runtime()
return jsonify({
"success": True,
**runtime_info
})
except Exception as e:
logger.error(f"Failed to detect runtime: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
@oci_bp.route("/runtime/install-script", methods=["GET"])
@require_auth
def get_runtime_install_script():
"""
Get the path to the runtime installation script.
Returns:
Script path for installing Podman.
"""
import os
# Check possible paths for the install script
possible_paths = [
"/usr/local/share/proxmenux/scripts/oci/install_runtime.sh",
os.path.join(os.path.dirname(__file__), "..", "..", "Scripts", "oci", "install_runtime.sh"),
]
for script_path in possible_paths:
if os.path.exists(script_path):
return jsonify({
"success": True,
"script_path": os.path.abspath(script_path)
})
return jsonify({
"success": False,
"message": "Runtime installation script not found"
}), 404
@oci_bp.route("/status/<app_id>", methods=["GET"])
@require_auth
def get_app_status(app_id: str):
"""
Get the current status of an app's container.
Returns:
Container state, health, and uptime.
"""
try:
status = oci_manager.get_app_status(app_id)
return jsonify({
"success": True,
"app_id": app_id,
"status": status
})
except Exception as e:
logger.error(f"Failed to get app status: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500
@oci_bp.route("/installed/<app_id>/update-auth-key", methods=["POST"])
@require_auth
def update_auth_key(app_id: str):
"""
Update the Tailscale auth key for an installed gateway.
This is useful when the auth key expires and the gateway needs to re-authenticate.
Body:
{
"auth_key": "tskey-auth-xxx"
}
Returns:
Success status and message.
"""
try:
data = request.get_json()
if not data or "auth_key" not in data:
return jsonify({
"success": False,
"message": "auth_key is required in request body"
}), 400
auth_key = data["auth_key"]
if not auth_key.startswith("tskey-"):
return jsonify({
"success": False,
"message": "Invalid auth key format. Should start with 'tskey-'"
}), 400
result = oci_manager.update_auth_key(app_id, auth_key)
status_code = 200 if result.get("success") else 400
return jsonify(result), status_code
except Exception as e:
logger.error(f"Failed to update auth key: {e}")
return jsonify({
"success": False,
"message": str(e)
}), 500