mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-10-31 04:02:03 +01:00
387 lines
11 KiB
Python
387 lines
11 KiB
Python
"""API CLI commands."""
|
|
|
|
import click
|
|
|
|
|
|
@click.command()
|
|
@click.option(
|
|
"--host",
|
|
type=str,
|
|
default="0.0.0.0",
|
|
envvar="API_HOST",
|
|
help="API server host",
|
|
)
|
|
@click.option(
|
|
"--port",
|
|
type=int,
|
|
default=8000,
|
|
envvar="API_PORT",
|
|
help="API server port",
|
|
)
|
|
@click.option(
|
|
"--data-home",
|
|
type=str,
|
|
default=None,
|
|
envvar="DATA_HOME",
|
|
help="Base data directory (default: ./data)",
|
|
)
|
|
@click.option(
|
|
"--database-url",
|
|
type=str,
|
|
default=None,
|
|
envvar="DATABASE_URL",
|
|
help="Database connection URL (default: the configured PostgreSQL connection from DATABASE_URL / DATABASE_* vars)",
|
|
)
|
|
@click.option(
|
|
"--read-key",
|
|
type=str,
|
|
default=None,
|
|
envvar="API_READ_KEY",
|
|
help="Read-only API key (optional, enables read-level auth)",
|
|
)
|
|
@click.option(
|
|
"--admin-key",
|
|
type=str,
|
|
default=None,
|
|
envvar="API_ADMIN_KEY",
|
|
help="Admin API key (optional, enables admin-level auth)",
|
|
)
|
|
@click.option(
|
|
"--mqtt-host",
|
|
type=str,
|
|
default="localhost",
|
|
envvar="MQTT_HOST",
|
|
help="MQTT broker host for commands",
|
|
)
|
|
@click.option(
|
|
"--mqtt-port",
|
|
type=int,
|
|
default=1883,
|
|
envvar="MQTT_PORT",
|
|
help="MQTT broker port",
|
|
)
|
|
@click.option(
|
|
"--mqtt-username",
|
|
type=str,
|
|
default=None,
|
|
envvar="MQTT_USERNAME",
|
|
help="MQTT username",
|
|
)
|
|
@click.option(
|
|
"--mqtt-password",
|
|
type=str,
|
|
default=None,
|
|
envvar="MQTT_PASSWORD",
|
|
help="MQTT password",
|
|
)
|
|
@click.option(
|
|
"--mqtt-prefix",
|
|
type=str,
|
|
default="meshcore",
|
|
envvar=["MQTT_PREFIX", "MQTT_TOPIC_PREFIX"],
|
|
help="MQTT topic prefix",
|
|
)
|
|
@click.option(
|
|
"--mqtt-tls",
|
|
is_flag=True,
|
|
default=False,
|
|
envvar="MQTT_TLS",
|
|
help="Enable TLS/SSL for MQTT connection",
|
|
)
|
|
@click.option(
|
|
"--mqtt-transport",
|
|
type=click.Choice(["tcp", "websockets"], case_sensitive=False),
|
|
default="websockets",
|
|
envvar="MQTT_TRANSPORT",
|
|
help="MQTT transport protocol",
|
|
)
|
|
@click.option(
|
|
"--mqtt-ws-path",
|
|
type=str,
|
|
default="/",
|
|
envvar="MQTT_WS_PATH",
|
|
help="MQTT WebSocket path (used when transport=websockets)",
|
|
)
|
|
@click.option(
|
|
"--cors-origins",
|
|
type=str,
|
|
default=None,
|
|
envvar="CORS_ORIGINS",
|
|
help="Comma-separated list of allowed CORS origins",
|
|
)
|
|
@click.option(
|
|
"--metrics-enabled/--no-metrics",
|
|
default=True,
|
|
envvar="METRICS_ENABLED",
|
|
help="Enable Prometheus metrics endpoint at /metrics",
|
|
)
|
|
@click.option(
|
|
"--metrics-cache-ttl",
|
|
type=int,
|
|
default=60,
|
|
envvar="METRICS_CACHE_TTL",
|
|
help="Seconds to cache metrics output (reduces database load)",
|
|
)
|
|
@click.option(
|
|
"--metrics-public/--no-metrics-public",
|
|
default=False,
|
|
envvar="METRICS_PUBLIC",
|
|
help=(
|
|
"Allow unauthenticated /metrics access when no read key is "
|
|
"configured (default: deny)"
|
|
),
|
|
)
|
|
@click.option(
|
|
"--redis-enabled/--no-redis",
|
|
default=False,
|
|
envvar="REDIS_ENABLED",
|
|
help="Enable Redis API response caching",
|
|
)
|
|
@click.option(
|
|
"--redis-host",
|
|
type=str,
|
|
default="localhost",
|
|
envvar="REDIS_HOST",
|
|
help="Redis server host",
|
|
)
|
|
@click.option(
|
|
"--redis-port",
|
|
type=int,
|
|
default=6379,
|
|
envvar="REDIS_PORT",
|
|
help="Redis server port",
|
|
)
|
|
@click.option(
|
|
"--redis-db",
|
|
type=int,
|
|
default=0,
|
|
envvar="REDIS_DB",
|
|
help="Redis database number",
|
|
)
|
|
@click.option(
|
|
"--redis-password",
|
|
type=str,
|
|
default=None,
|
|
envvar="REDIS_PASSWORD",
|
|
help="Redis password (optional)",
|
|
)
|
|
@click.option(
|
|
"--redis-key-prefix",
|
|
type=str,
|
|
default="hub",
|
|
envvar="REDIS_KEY_PREFIX",
|
|
help="Prefix for all Redis cache keys",
|
|
)
|
|
@click.option(
|
|
"--redis-cache-ttl",
|
|
type=int,
|
|
default=30,
|
|
envvar="REDIS_CACHE_TTL",
|
|
help="Default cache TTL in seconds",
|
|
)
|
|
@click.option(
|
|
"--redis-cache-ttl-dashboard",
|
|
type=int,
|
|
default=3600,
|
|
envvar="REDIS_CACHE_TTL_DASHBOARD",
|
|
help=(
|
|
"Cache TTL in seconds for dashboard endpoints, /routes/{id} detail, "
|
|
"and per-route health history"
|
|
),
|
|
)
|
|
@click.option(
|
|
"--api-cache-control-enabled/--no-api-cache-control",
|
|
default=True,
|
|
envvar="API_CACHE_CONTROL_ENABLED",
|
|
help=(
|
|
"Emit HTTP Cache-Control headers on /api/v1/* responses and ETag / "
|
|
"If-None-Match handling on @cached endpoints"
|
|
),
|
|
)
|
|
@click.option(
|
|
"--reload",
|
|
is_flag=True,
|
|
default=False,
|
|
help="Enable auto-reload for development",
|
|
)
|
|
@click.option(
|
|
"--workers",
|
|
type=int,
|
|
default=1,
|
|
envvar="API_WORKERS",
|
|
help=(
|
|
"Number of worker processes (default: 1). Values >1 run multiple "
|
|
"processes for multi-core concurrency; workers are built from the "
|
|
"environment via the app factory, so configure via env vars (not "
|
|
"CLI flags) when scaling. Ignored in --reload mode."
|
|
),
|
|
)
|
|
@click.pass_context
|
|
def api(
|
|
ctx: click.Context,
|
|
host: str,
|
|
port: int,
|
|
data_home: str | None,
|
|
database_url: str | None,
|
|
read_key: str | None,
|
|
admin_key: str | None,
|
|
mqtt_host: str,
|
|
mqtt_port: int,
|
|
mqtt_username: str | None,
|
|
mqtt_password: str | None,
|
|
mqtt_prefix: str,
|
|
mqtt_tls: bool,
|
|
mqtt_transport: str,
|
|
mqtt_ws_path: str,
|
|
cors_origins: str | None,
|
|
metrics_enabled: bool,
|
|
metrics_cache_ttl: int,
|
|
metrics_public: bool,
|
|
redis_enabled: bool,
|
|
redis_host: str,
|
|
redis_port: int,
|
|
redis_db: int,
|
|
redis_password: str | None,
|
|
redis_key_prefix: str,
|
|
redis_cache_ttl: int,
|
|
redis_cache_ttl_dashboard: int,
|
|
api_cache_control_enabled: bool,
|
|
reload: bool,
|
|
workers: int,
|
|
) -> None:
|
|
"""Run the REST API server.
|
|
|
|
Provides REST API endpoints for querying mesh network data and sending
|
|
commands to devices via MQTT.
|
|
|
|
Examples:
|
|
|
|
# Run with defaults (no auth)
|
|
meshcore-hub api
|
|
|
|
# Run with authentication
|
|
meshcore-hub api --read-key secret --admin-key supersecret
|
|
|
|
# Run with CORS for web frontend
|
|
meshcore-hub api --cors-origins "http://localhost:8080,http://localhost:3000"
|
|
|
|
# Development mode with auto-reload
|
|
meshcore-hub api --reload
|
|
"""
|
|
import uvicorn
|
|
|
|
from meshcore_hub.common.config import get_api_settings
|
|
from meshcore_hub.api.app import create_app
|
|
|
|
# Get settings to compute effective values
|
|
settings = get_api_settings()
|
|
|
|
# Override data_home if provided
|
|
if data_home:
|
|
settings = settings.model_copy(update={"data_home": data_home})
|
|
|
|
# Use effective database URL if not explicitly provided
|
|
effective_db_url = database_url if database_url else settings.effective_database_url
|
|
effective_data_home = data_home or settings.data_home
|
|
|
|
click.echo("=" * 50)
|
|
click.echo("MeshCore Hub API Server")
|
|
click.echo("=" * 50)
|
|
click.echo(f"Host: {host}")
|
|
click.echo(f"Port: {port}")
|
|
click.echo(f"Data home: {effective_data_home}")
|
|
click.echo(f"Database: {effective_db_url}")
|
|
click.echo(f"MQTT: {mqtt_host}:{mqtt_port} (prefix: {mqtt_prefix})")
|
|
click.echo(f"MQTT transport: {mqtt_transport} (ws_path: {mqtt_ws_path})")
|
|
click.echo(f"Read key configured: {read_key is not None}")
|
|
click.echo(f"Admin key configured: {admin_key is not None}")
|
|
click.echo(f"CORS origins: {cors_origins or 'none'}")
|
|
click.echo(f"Metrics enabled: {metrics_enabled}")
|
|
click.echo(f"Metrics cache TTL: {metrics_cache_ttl}s")
|
|
click.echo(f"Metrics public (no key): {metrics_public}")
|
|
click.echo(f"Redis enabled: {redis_enabled}")
|
|
if redis_enabled:
|
|
click.echo(f"Redis: {redis_host}:{redis_port}/{redis_db}")
|
|
click.echo(f"Redis key prefix: {redis_key_prefix}")
|
|
click.echo(
|
|
f"Redis cache TTL: {redis_cache_ttl}s "
|
|
f"(dashboard: {redis_cache_ttl_dashboard}s)"
|
|
)
|
|
click.echo(f"API Cache-Control enabled: {api_cache_control_enabled}")
|
|
click.echo(
|
|
f"Spam detection: {settings.spam_detection_enabled} "
|
|
f"(threshold: {settings.spam_score_threshold})"
|
|
)
|
|
click.echo(f"Reload mode: {reload}")
|
|
click.echo(f"Workers: {workers}")
|
|
click.echo("=" * 50)
|
|
|
|
# Parse CORS origins
|
|
origins_list: list[str] | None = None
|
|
if cors_origins:
|
|
origins_list = [o.strip() for o in cors_origins.split(",")]
|
|
|
|
if reload:
|
|
# For development, use uvicorn's reload feature
|
|
# We need to pass app as string for reload to work
|
|
click.echo("\nStarting in development mode with auto-reload...")
|
|
click.echo("Note: Using default settings for reload mode.")
|
|
click.echo("Note: Redis defaults to disabled in reload mode.")
|
|
|
|
uvicorn.run(
|
|
"meshcore_hub.api.app:create_app",
|
|
host=host,
|
|
port=port,
|
|
reload=True,
|
|
factory=True,
|
|
)
|
|
elif workers > 1:
|
|
# Multiple worker processes require an import string so uvicorn can
|
|
# re-import the app in each forked worker. Workers rebuild the app from
|
|
# the environment via the factory, so configuration must come from env
|
|
# vars — CLI flags do not propagate to the workers.
|
|
click.echo(f"\nStarting API server with {workers} workers...")
|
|
uvicorn.run(
|
|
"meshcore_hub.api.app:create_app_from_env",
|
|
host=host,
|
|
port=port,
|
|
workers=workers,
|
|
factory=True,
|
|
)
|
|
else:
|
|
# Single process: build the app directly so CLI flags apply.
|
|
app = create_app(
|
|
database_url=effective_db_url,
|
|
read_key=read_key,
|
|
admin_key=admin_key,
|
|
mqtt_host=mqtt_host,
|
|
mqtt_port=mqtt_port,
|
|
mqtt_username=mqtt_username,
|
|
mqtt_password=mqtt_password,
|
|
mqtt_prefix=mqtt_prefix,
|
|
mqtt_tls=mqtt_tls,
|
|
mqtt_transport=mqtt_transport,
|
|
mqtt_ws_path=mqtt_ws_path,
|
|
cors_origins=origins_list,
|
|
metrics_enabled=metrics_enabled,
|
|
metrics_cache_ttl=metrics_cache_ttl,
|
|
metrics_public=metrics_public,
|
|
redis_enabled=redis_enabled,
|
|
redis_host=redis_host,
|
|
redis_port=redis_port,
|
|
redis_db=redis_db,
|
|
redis_password=redis_password,
|
|
redis_key_prefix=redis_key_prefix,
|
|
redis_cache_ttl=redis_cache_ttl,
|
|
redis_cache_ttl_dashboard=redis_cache_ttl_dashboard,
|
|
api_cache_control_enabled=api_cache_control_enabled,
|
|
spam_detection_enabled=settings.spam_detection_enabled,
|
|
spam_score_threshold=settings.spam_score_threshold,
|
|
oidc_role_admin=settings.oidc_role_admin,
|
|
oidc_role_operator=settings.oidc_role_operator,
|
|
oidc_role_member=settings.oidc_role_member,
|
|
)
|
|
|
|
click.echo("\nStarting API server...")
|
|
uvicorn.run(app, host=host, port=port)
|