225 lines
8.3 KiB
Python
225 lines
8.3 KiB
Python
"""
|
|
Entry point for the xCloudify Flask application.
|
|
|
|
* Loads environment variables from a `.env` file.
|
|
* Configures Flask, SQLAlchemy, Celery, and Cloudflare settings.
|
|
* Masks sensitive values in logs for security.
|
|
"""
|
|
|
|
print("-----In init----------")
|
|
|
|
import uuid
|
|
import os
|
|
import pymysql
|
|
from flask import Flask, g, jsonify, request
|
|
from flask_sqlalchemy import SQLAlchemy
|
|
from flask_migrate import Migrate
|
|
from celery import Celery, Task
|
|
from flasgger import Swagger
|
|
from logger import logger
|
|
from app.utils.standard_responses import api_response
|
|
from flask_cors import CORS
|
|
|
|
from runtime_urls import API_BASE_URL, APP_ENV, BASE_DOMAIN, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_ID, DATABASE_URL, JWT_SECRET_KEY, PING_HEARTBEAT_TIMEOUT_SECONDS, REDIS_BROKER_URL, REDIS_RESULT_BACKEND_URL, REDIS_URL, SCHEDULER_MAX_ALLOCATION_ATTEMPTS, SCHEDULER_RETRY_BACKOFF_FACTOR, SCHEDULER_RETRY_BASE_DELAY_SECONDS, SCHEDULER_RETRY_JITTER_SECONDS, SCHEDULER_RETRY_MAX_DELAY_SECONDS, TUNNEL_DOMAIN, VNC_BASE_URL, VNC_PROXY_HOST, VNC_PROXY_PORT, VNC_PROXY_WS_PATH, VNC_SECRET_KEY, WEBSOCKET_SERVER_URL
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 1. Flask application & database #
|
|
# --------------------------------------------------------------------------- #
|
|
app: Flask = Flask(__name__)
|
|
CORS(app)
|
|
|
|
# Set APP_ENV=development in prod .env to disable /apidocs and /apispec.json.
|
|
_env = APP_ENV.lower()
|
|
_swagger_enabled = _env == "development"
|
|
|
|
if _swagger_enabled:
|
|
swagger = Swagger(app, config={
|
|
"headers": [],
|
|
"specs": [
|
|
{
|
|
"endpoint": "apispec",
|
|
"route": "/apispec.json",
|
|
"rule_filter": lambda rule: True,
|
|
"model_filter": lambda tag: True,
|
|
}
|
|
],
|
|
"static_url_path": "/flasgger_static",
|
|
"swagger_ui": True,
|
|
"specs_route": "/apidocs",
|
|
}, template={
|
|
"info": {
|
|
"title": "xCloudify API",
|
|
"description": "REST API for the xCloudify platform.",
|
|
"version": "1.0.0",
|
|
"contact": {
|
|
"name": "xCloudify",
|
|
},
|
|
},
|
|
"securityDefinitions": {
|
|
"BearerAuth": {
|
|
"type": "apiKey",
|
|
"in": "header",
|
|
"name": "Authorization",
|
|
"description": "JWT bearer token. Format: `Bearer <token>`",
|
|
}
|
|
},
|
|
"security": [{"BearerAuth": []}],
|
|
"consumes": ["application/json"],
|
|
"produces": ["application/json"],
|
|
})
|
|
logger.info("Swagger UI enabled at /apidocs (APP_ENV=%s)", _env)
|
|
else:
|
|
logger.info("Swagger UI disabled (APP_ENV=%s)", _env)
|
|
|
|
app.config.update(SQLALCHEMY_DATABASE_URI=DATABASE_URL, WEBSOCKET_SERVER_URL=f"{WEBSOCKET_SERVER_URL}/api/create_task", broker_url=REDIS_BROKER_URL, result_backend=REDIS_RESULT_BACKEND_URL)
|
|
app.config["SCHEDULER_MAX_ALLOCATION_ATTEMPTS"] = SCHEDULER_MAX_ALLOCATION_ATTEMPTS
|
|
app.config["SCHEDULER_RETRY_BASE_DELAY_SECONDS"] = SCHEDULER_RETRY_BASE_DELAY_SECONDS
|
|
app.config["SCHEDULER_RETRY_BACKOFF_FACTOR"] = SCHEDULER_RETRY_BACKOFF_FACTOR
|
|
app.config["SCHEDULER_RETRY_JITTER_SECONDS"] = SCHEDULER_RETRY_JITTER_SECONDS
|
|
app.config["SCHEDULER_RETRY_MAX_DELAY_SECONDS"] = SCHEDULER_RETRY_MAX_DELAY_SECONDS
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 2. Cloudflare configuration #
|
|
# --------------------------------------------------------------------------- #
|
|
def _mask(value: str, visible: int = 4) -> str:
|
|
"""
|
|
Return a partially masked version of a sensitive string.
|
|
|
|
Args:
|
|
value (str): The string to mask.
|
|
visible (int): Number of visible characters to keep at the start.
|
|
|
|
Returns:
|
|
str: Masked string suitable for logging.
|
|
"""
|
|
if not value:
|
|
return ""
|
|
return f"{value[:visible]}{'*' * (len(value) - visible)}"
|
|
|
|
app.config["CLOUDFLARE_API_TOKEN"] = CLOUDFLARE_API_TOKEN
|
|
app.config["CLOUDFLARE_ACCOUNT_ID"] = CLOUDFLARE_ACCOUNT_ID
|
|
app.config["CLOUDFLARE_ZONE_ID"] = CLOUDFLARE_ZONE_ID
|
|
app.config["PING_HEARTBEAT_TIMEOUT_SECONDS"] = PING_HEARTBEAT_TIMEOUT_SECONDS # How long before a Websocket PING\PONG is classed as a failure and triggers a worker offline event
|
|
app.config["REDIS_URL"] = REDIS_URL # Redis Database 2 for operational tasks like websocket server and PING logging
|
|
app.config["VNC_SECRET_KEY"] = VNC_SECRET_KEY
|
|
app.config["API_BASE_URL"] = API_BASE_URL
|
|
app.config["BASE_DOMAIN"] = BASE_DOMAIN
|
|
app.config["TUNNEL_DOMAIN"] = TUNNEL_DOMAIN
|
|
app.config["VNC_BASE_URL"] = VNC_BASE_URL
|
|
app.config["VNC_PROXY_HOST"] = VNC_PROXY_HOST
|
|
app.config["VNC_PROXY_PORT"] = VNC_PROXY_PORT
|
|
app.config["VNC_PROXY_WS_PATH"] = VNC_PROXY_WS_PATH
|
|
# JWT secret used for decoding Authorization bearer tokens for audit attribution
|
|
app.config["JWT_SECRET_KEY"] = JWT_SECRET_KEY
|
|
|
|
logger.info(
|
|
"Cloudflare configuration set "
|
|
f"(token={_mask(app.config['CLOUDFLARE_API_TOKEN'])}, "
|
|
f"account_id={_mask(app.config['CLOUDFLARE_ACCOUNT_ID'])})"
|
|
f"zone_id={_mask(app.config['CLOUDFLARE_ZONE_ID'])})"
|
|
)
|
|
|
|
pymysql.install_as_MySQLdb()
|
|
|
|
db: SQLAlchemy = SQLAlchemy(app)
|
|
migrate: Migrate = Migrate(app, db)
|
|
app.secret_key = "your_secret_key_here"
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 2. Celery initialisation #
|
|
# --------------------------------------------------------------------------- #
|
|
def _make_celery(flask_app: Flask) -> Celery:
|
|
"""
|
|
Create a Celery instance that shares the Flask application context.
|
|
|
|
Every task will inherit `current_app` and can use the database session
|
|
without manual `app.app_context()` juggling.
|
|
"""
|
|
celery = Celery(
|
|
flask_app.import_name,
|
|
broker=flask_app.config["broker_url"],
|
|
backend=flask_app.config["result_backend"],
|
|
)
|
|
celery.conf.update(flask_app.config)
|
|
|
|
class ContextTask(Task):
|
|
"""Celery Task base-class that wraps task execution in app_context."""
|
|
abstract = True
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
with flask_app.app_context():
|
|
return super().__call__(*args, **kwargs)
|
|
|
|
celery.Task = ContextTask
|
|
return celery
|
|
|
|
|
|
celery_app: Celery = _make_celery(app)
|
|
celery_app.autodiscover_tasks(["app.tasks"], force=True)
|
|
app.extensions["celery"] = celery_app
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 3. Blueprints and standard middleware #
|
|
# --------------------------------------------------------------------------- #
|
|
from app.controller import api_bp, ui_bp, celery_bp # noqa: E402 circular OK
|
|
app.register_blueprint(api_bp)
|
|
app.register_blueprint(ui_bp)
|
|
app.register_blueprint(celery_bp)
|
|
|
|
# ---------- Error-handlers, request IDs, context processors --------------- #
|
|
@app.errorhandler(500)
|
|
def handle_internal_server_error(error):
|
|
logger.error(
|
|
"Unhandled exception for %s %s (request_id=%s)",
|
|
request.method,
|
|
request.path,
|
|
getattr(g, "request_id", "n/a"),
|
|
exc_info=True,
|
|
)
|
|
return api_response(
|
|
success=False,
|
|
status=500,
|
|
message="Internal server error",
|
|
error_type="INTERNAL_SERVER_ERROR",
|
|
error_details={"path": request.path},
|
|
)
|
|
|
|
|
|
@app.errorhandler(404)
|
|
def not_found(e):
|
|
return api_response(
|
|
success=False, status=404, message=str(e), error_type="RESOURCE_NOT_FOUND"
|
|
)
|
|
|
|
|
|
@app.before_request
|
|
def assign_request_id():
|
|
g.request_id = str(uuid.uuid4())
|
|
|
|
|
|
@app.after_request
|
|
def log_request_id(response):
|
|
response.headers["X-Request-ID"] = g.request_id
|
|
return response
|
|
|
|
|
|
@app.context_processor
|
|
def inject_user_roles():
|
|
try:
|
|
user_details = {"name": "user.name", "roles": ["user_roles"]}
|
|
if "current_user" in g:
|
|
user_details = {
|
|
"id": str(g.current_user.id),
|
|
"friendly_id": g.current_user.friendly_id,
|
|
"email": g.current_user.email,
|
|
"name": g.current_user.name,
|
|
"roles": g.current_user.roles_text,
|
|
}
|
|
return dict(user_details=user_details)
|
|
except Exception as exc:
|
|
logger.error("inject_user_roles - %s", exc)
|
|
return {}
|
|
|
|
|
|
logger.debug("__init__ complete") |