from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
from typing import List, Dict, Any, Optional
import yaml
from pathlib import Path
from pydantic import BaseModel
import logging
import json
import asyncio
import importlib.metadata as md
# Import config utilities
from ..config import load_proxies_config
router = APIRouter(prefix="/api/petal-proxies-control", tags=["petal-proxies-control"])
_logger: Optional[logging.Logger] = None
def _set_logger(logger: logging.Logger):
"""Set the logger for api endpoints."""
global _logger
_logger = logger
[docs]
def get_logger() -> logging.Logger:
"""Get the logger instance."""
global _logger
if not _logger:
_logger = logging.getLogger("ConfigAPI")
return _logger
[docs]
class PetalControlRequest(BaseModel):
petals: List[str]
action: str # "ON" or "OFF"
[docs]
class ConfigResponse(BaseModel):
enabled_proxies: List[str]
enabled_petals: List[str]
petal_dependencies: Dict[str, List[str]]
proxy_dependencies: Dict[str, List[str]]
[docs]
class PetalInfo(BaseModel):
name: str
enabled: bool
dependencies: List[str]
[docs]
class ProxyInfo(BaseModel):
name: str
enabled: bool
dependencies: List[str]
dependents: List[str] # Proxies and petals that depend on this proxy
[docs]
class AllComponentsResponse(BaseModel):
petals: List[PetalInfo]
proxies: List[ProxyInfo]
total_petals: int
total_proxies: int
[docs]
@router.get("/status")
async def get_status() -> ConfigResponse:
"""Get current configuration status"""
logger = get_logger()
logger.debug("Processing status request")
try:
config_path = Path(__file__).parent.parent.parent.parent / "proxies.yaml"
config = load_proxies_config(config_path)
logger.debug(f"Successfully loaded configuration from {config_path}")
logger.info("Retrieved current configuration status")
return ConfigResponse(
enabled_proxies=config.get("enabled_proxies", []),
enabled_petals=config.get("enabled_petals", []),
petal_dependencies=config.get("petal_dependencies", {}),
proxy_dependencies=config.get("proxy_dependencies", {})
)
except Exception as e:
logger.error(f"Error reading configuration: {e}")
logger.debug(f"Configuration error details: {type(e).__name__}: {str(e)}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Error reading configuration: {str(e)}"
)
[docs]
@router.post("/petals/control")
async def control_petals(request: PetalControlRequest) -> Dict[str, Any]:
"""Enable or disable one or more petals"""
logger = get_logger()
logger.debug(f"Received petals control request: action='{request.action}', petals={request.petals}")
# Validate action
if request.action.upper() not in ["ON", "OFF"]:
logger.error(f"Invalid action '{request.action}' provided. Expected 'ON' or 'OFF'")
raise HTTPException(
status_code=400,
detail="Action must be either 'ON' or 'OFF'"
)
# Validate petals list
if not request.petals:
logger.error("Empty petals list provided in request")
raise HTTPException(
status_code=400,
detail="At least one petal name must be provided"
)
action = request.action.upper()
enable_petals = action == "ON"
logger.info(f"Processing {action} action for {len(request.petals)} petals: {request.petals}")
try:
config_path = Path(__file__).parent.parent.parent.parent / "proxies.yaml"
logger.debug(f"Loading configuration from: {config_path}")
# Read current configuration (auto-creates if missing)
config = load_proxies_config(config_path)
enabled_petals = set(config.get("enabled_petals", []) or [])
enabled_proxies = set(config.get("enabled_proxies", []) or [])
petal_dependencies = config.get("petal_dependencies", {})
logger.debug(f"Current enabled petals: {list(enabled_petals)}")
logger.debug(f"Current enabled proxies: {list(enabled_proxies)}")
logger.debug(f"Petal dependencies: {petal_dependencies}")
results = []
errors = []
for petal_name in request.petals:
logger.debug(f"Processing petal: {petal_name}")
try:
if enable_petals:
logger.debug(f"Attempting to enable petal: {petal_name}")
# Check if dependencies are met before enabling
required_deps = petal_dependencies.get(petal_name, [])
logger.debug(f"Petal {petal_name} requires dependencies: {required_deps}")
missing_deps = [dep for dep in required_deps if dep not in enabled_proxies]
if missing_deps:
error_msg = (
f"Cannot enable {petal_name}: missing dependencies {missing_deps}. "
f"Enable those proxies first."
)
errors.append(error_msg)
logger.error(f"DEPENDENCY ERROR: {error_msg}")
continue
if petal_name in enabled_petals:
logger.debug(f"Petal {petal_name} is already enabled, skipping")
else:
enabled_petals.add(petal_name)
results.append(f"Enabled petal: {petal_name}")
logger.info(f"Successfully enabled petal: {petal_name}")
else:
logger.debug(f"Attempting to disable petal: {petal_name}")
# Disable petal
if petal_name in enabled_petals:
enabled_petals.discard(petal_name)
results.append(f"Disabled petal: {petal_name}")
logger.info(f"Successfully disabled petal: {petal_name}")
else:
logger.debug(f"Petal {petal_name} was already disabled, skipping")
except Exception as e:
error_msg = f"Error processing {petal_name}: {str(e)}"
errors.append(error_msg)
logger.error(f"PROCESSING ERROR: {error_msg}")
logger.debug(f"Exception details for {petal_name}: {type(e).__name__}: {str(e)}", exc_info=True)
# Update configuration
config["enabled_petals"] = list(enabled_petals)
logger.debug(f"Updated enabled petals: {list(enabled_petals)}")
# Write back to file
logger.debug(f"Writing configuration back to: {config_path}")
with open(config_path, "w") as f:
yaml.safe_dump(config, f, default_flow_style=False)
logger.info(f"Configuration updated successfully with {len(results)} changes")
if errors:
logger.warning(f"Request completed with {len(errors)} errors: {errors}")
if enable_petals:
success = all(p in enabled_petals for p in request.petals)
else:
success = all(p not in enabled_petals for p in request.petals)
response = {
"success": success,
"action": action,
"processed_petals": request.petals,
"results": results,
"message": f"Configuration updated. {len(results)} petals switched {action.lower()} successfully."
}
if errors:
response["errors"] = errors
response["partial_success"] = len(results) > 0
logger.debug(f"Returning response: {response}")
return response
except Exception as e:
logger.critical(f"CRITICAL ERROR updating petal configuration: {e}")
logger.debug(f"Critical error details: {type(e).__name__}: {str(e)}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Failed to update configuration: {str(e)}"
)
[docs]
@router.post("/proxies/control")
async def control_proxies(request: PetalControlRequest) -> Dict[str, Any]:
"""Enable or disable one or more proxies"""
logger = get_logger()
logger.debug(f"Received proxies control request: action='{request.action}', proxies={request.petals}")
# Validate action
if request.action.upper() not in ["ON", "OFF"]:
logger.error(f"Invalid action '{request.action}' provided. Expected 'ON' or 'OFF'")
raise HTTPException(
status_code=400,
detail="Action must be either 'ON' or 'OFF'"
)
logger.info(f"Processing {request.action.upper()} action for {len(request.petals)} proxies: {request.petals}")
# Validate proxies list (reusing petals field name for consistency)
if not request.petals:
logger.error("Empty proxies list provided in request")
raise HTTPException(
status_code=400,
detail="At least one proxy name must be provided"
)
action = request.action.upper()
enable_proxies = action == "ON"
try:
config_path = Path(__file__).parent.parent.parent.parent / "proxies.yaml"
logger.debug(f"Loading configuration from: {config_path}")
# Read current configuration (auto-creates if missing)
config = load_proxies_config(config_path)
enabled_proxies = set(config.get("enabled_proxies", []) or [])
enabled_petals = set(config.get("enabled_petals", []) or [])
petal_dependencies = config.get("petal_dependencies", {})
proxy_dependencies = config.get("proxy_dependencies", {})
logger.debug(f"Current enabled proxies: {list(enabled_proxies)}")
logger.debug(f"Current enabled petals: {list(enabled_petals)}")
logger.debug(f"Proxy dependencies: {proxy_dependencies}")
results = []
errors = []
for proxy_name in request.petals: # Using petals field for proxy names
logger.debug(f"Processing proxy: {proxy_name}")
try:
if enable_proxies:
# Check if dependencies are met before enabling
required_deps = proxy_dependencies.get(proxy_name, [])
missing_deps = [dep for dep in required_deps if dep not in enabled_proxies]
if missing_deps:
error_msg = (
f"Cannot enable {proxy_name}: missing proxy dependencies {missing_deps}. "
f"Enable those proxies first."
)
errors.append(error_msg)
logger.error(f"PROXY DEPENDENCY ERROR: {error_msg}")
continue
enabled_proxies.add(proxy_name)
results.append(f"Enabled proxy: {proxy_name}")
logger.info(f"Enabled proxy: {proxy_name}")
else:
# Check if any enabled petals depend on this proxy
dependent_petals = []
for petal, deps in petal_dependencies.items():
if petal in enabled_petals and proxy_name in deps:
dependent_petals.append(petal)
# Check if any enabled proxies depend on this proxy
dependent_proxies = []
for proxy, deps in proxy_dependencies.items():
if proxy in enabled_proxies and proxy_name in deps:
dependent_proxies.append(proxy)
if dependent_petals or dependent_proxies:
dependencies = []
if dependent_petals:
dependencies.append(f"petals {dependent_petals}")
if dependent_proxies:
dependencies.append(f"proxies {dependent_proxies}")
error_msg = (
f"Cannot disable {proxy_name}: required by {' and '.join(dependencies)}. "
f"Disable those first."
)
errors.append(error_msg)
logger.warning(error_msg)
continue
enabled_proxies.discard(proxy_name)
results.append(f"Disabled proxy: {proxy_name}")
logger.info(f"Disabled proxy: {proxy_name}")
except Exception as e:
error_msg = f"Error processing {proxy_name}: {str(e)}"
errors.append(error_msg)
logger.error(error_msg)
# Update configuration
config["enabled_proxies"] = list(enabled_proxies)
# Write back to file
with open(config_path, "w") as f:
yaml.safe_dump(config, f, default_flow_style=False)
logger.info(f"Configuration updated with {len(results)} successful changes")
response = {
"success": len(results) > 0,
"action": action,
"processed_proxies": request.petals, # Using petals field for proxy names
"results": results,
"message": f"Configuration updated. {len(results)} proxies switched {action.lower()} successfully."
}
if errors:
response["errors"] = errors
response["partial_success"] = len(results) > 0
return response
except Exception as e:
logger.critical(f"CRITICAL ERROR updating proxy configuration: {e}")
logger.debug(f"Critical error details: {type(e).__name__}: {str(e)}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Failed to update configuration: {str(e)}"
)
[docs]
@router.get("/components/list", response_model=AllComponentsResponse)
async def list_all_components():
"""List all available petals and proxies, regardless of their enabled/disabled state"""
logger = get_logger()
logger.debug("Processing components list request")
try:
config_path = Path(__file__).parent.parent.parent.parent / "proxies.yaml"
logger.debug(f"Loading configuration from: {config_path}")
config = load_proxies_config(config_path)
enabled_proxies = set(config.get("enabled_proxies", []) or [])
enabled_petals = set(config.get("enabled_petals", []) or [])
petal_dependencies = config.get("petal_dependencies", {})
proxy_dependencies = config.get("proxy_dependencies", {})
logger.debug(f"Configuration loaded - enabled proxies: {list(enabled_proxies)}, enabled petals: {list(enabled_petals)}")
# Discover all available petals from entry points
available_petals = []
discovered_petal_names = set()
try:
logger.debug("Discovering petals from entry points...")
entry_points = list(md.entry_points(group="petal.plugins"))
logger.debug(f"Found {len(entry_points)} petal entry points")
for ep in entry_points:
# Skip if we've already processed this petal name
if ep.name in discovered_petal_names:
logger.debug(f"Skipping duplicate entry point for petal: {ep.name}")
continue
logger.debug(f"Processing petal: {ep.name} (enabled: {ep.name in enabled_petals})")
petal_info = PetalInfo(
name=ep.name,
enabled=ep.name in enabled_petals,
dependencies=petal_dependencies.get(ep.name, [])
)
available_petals.append(petal_info)
discovered_petal_names.add(ep.name)
except Exception as e:
logger.warning(f"Error discovering petals from entry points: {e}")
logger.debug(f"Petal discovery error details: {type(e).__name__}: {str(e)}", exc_info=True)
# Add any petals from configuration that weren't discovered via entry_points
for petal_name in petal_dependencies.keys():
if petal_name not in discovered_petal_names:
petal_info = PetalInfo(
name=petal_name,
enabled=petal_name in enabled_petals,
dependencies=petal_dependencies.get(petal_name, [])
)
available_petals.append(petal_info)
discovered_petal_names.add(petal_name)
# Also add any enabled petals that might not be in dependencies
for petal_name in enabled_petals:
if petal_name not in discovered_petal_names:
petal_info = PetalInfo(
name=petal_name,
enabled=True,
dependencies=petal_dependencies.get(petal_name, [])
)
available_petals.append(petal_info)
discovered_petal_names.add(petal_name)
# Define all known proxy types
# This is based on the proxy types defined in main.py
known_proxy_types = [
"ext_mavlink",
"redis",
"db",
"cloud",
"bucket",
"ftp_mavlink"
]
# Build proxy info with dependencies and dependents
available_proxies = []
for proxy_name in known_proxy_types:
# Find what depends on this proxy
dependents = []
# Check petals that depend on this proxy
for petal, deps in petal_dependencies.items():
if proxy_name in deps:
dependents.append(f"petal:{petal}")
# Check proxies that depend on this proxy
for proxy, deps in proxy_dependencies.items():
if proxy_name in deps:
dependents.append(f"proxy:{proxy}")
proxy_info = ProxyInfo(
name=proxy_name,
enabled=proxy_name in enabled_proxies,
dependencies=proxy_dependencies.get(proxy_name, []),
dependents=dependents
)
available_proxies.append(proxy_info)
# Add any additional proxies from configuration that aren't in known types
for proxy_name in enabled_proxies:
if proxy_name not in known_proxy_types:
# Find dependents for unknown proxy
dependents = []
for petal, deps in petal_dependencies.items():
if proxy_name in deps:
dependents.append(f"petal:{petal}")
for proxy, deps in proxy_dependencies.items():
if proxy_name in deps:
dependents.append(f"proxy:{proxy}")
proxy_info = ProxyInfo(
name=proxy_name,
enabled=True, # It's in enabled_proxies
dependencies=proxy_dependencies.get(proxy_name, []),
dependents=dependents
)
available_proxies.append(proxy_info)
# Sort by name for consistent ordering
available_petals.sort(key=lambda x: x.name)
available_proxies.sort(key=lambda x: x.name)
# Count enabled components for more informative logging
enabled_petal_count = len([p for p in available_petals if p.enabled])
enabled_proxy_count = len([p for p in available_proxies if p.enabled])
logger.info(f"Listed {len(available_petals)} petals ({enabled_petal_count} enabled) and {len(available_proxies)} proxies ({enabled_proxy_count} enabled)")
return AllComponentsResponse(
petals=available_petals,
proxies=available_proxies,
total_petals=len(available_petals),
total_proxies=len(available_proxies)
)
except Exception as e:
logger.error(f"Error listing components: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to list components: {str(e)}"
)