GT AI OS Community v2.0.33 - Add NVIDIA NIM and Nemotron agents
- Updated python_coding_microproject.csv to use NVIDIA NIM Kimi K2 - Updated kali_linux_shell_simulator.csv to use NVIDIA NIM Kimi K2 - Made more general-purpose (flexible targets, expanded tools) - Added nemotron-mini-agent.csv for fast local inference via Ollama - Added nemotron-agent.csv for advanced reasoning via Ollama - Added wiki page: Projects for NVIDIA NIMs and Nemotron
This commit is contained in:
240
apps/control-panel-backend/app/api/v1/analytics.py
Normal file
240
apps/control-panel-backend/app/api/v1/analytics.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
Analytics and Dremio SQL Federation Endpoints
|
||||
"""
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.dremio_service import DremioService
|
||||
from app.core.auth import get_current_user
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/api/v1/analytics", tags=["Analytics"])
|
||||
|
||||
|
||||
class TenantDashboardResponse(BaseModel):
|
||||
"""Response model for tenant dashboard data"""
|
||||
tenant: Dict[str, Any]
|
||||
metrics: Dict[str, Any]
|
||||
analytics: Dict[str, Any]
|
||||
alerts: List[Dict[str, Any]]
|
||||
|
||||
|
||||
class CustomQueryRequest(BaseModel):
|
||||
"""Request model for custom analytics queries"""
|
||||
query_type: str
|
||||
start_date: Optional[datetime] = None
|
||||
end_date: Optional[datetime] = None
|
||||
|
||||
|
||||
class DatasetCreationResponse(BaseModel):
|
||||
"""Response model for dataset creation"""
|
||||
tenant_id: int
|
||||
datasets_created: List[str]
|
||||
status: str
|
||||
|
||||
|
||||
@router.get("/dashboard/{tenant_id}", response_model=TenantDashboardResponse)
|
||||
async def get_tenant_dashboard(
|
||||
tenant_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get comprehensive dashboard data for a tenant using Dremio SQL federation"""
|
||||
|
||||
# Check permissions
|
||||
if current_user.user_type != 'super_admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions to view dashboard"
|
||||
)
|
||||
|
||||
|
||||
service = DremioService(db)
|
||||
|
||||
try:
|
||||
dashboard_data = await service.get_tenant_dashboard_data(tenant_id)
|
||||
return TenantDashboardResponse(**dashboard_data)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch dashboard data: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/query/{tenant_id}")
|
||||
async def execute_custom_analytics(
|
||||
tenant_id: int,
|
||||
request: CustomQueryRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Execute custom analytics queries for a tenant"""
|
||||
|
||||
# Check permissions (only admins)
|
||||
if current_user.user_type != 'super_admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions for analytics queries"
|
||||
)
|
||||
|
||||
|
||||
service = DremioService(db)
|
||||
|
||||
try:
|
||||
results = await service.get_custom_analytics(
|
||||
tenant_id=tenant_id,
|
||||
query_type=request.query_type,
|
||||
start_date=request.start_date,
|
||||
end_date=request.end_date
|
||||
)
|
||||
return {
|
||||
"query_type": request.query_type,
|
||||
"results": results,
|
||||
"count": len(results)
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Query execution failed: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/datasets/create/{tenant_id}", response_model=DatasetCreationResponse)
|
||||
async def create_virtual_datasets(
|
||||
tenant_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Create Dremio virtual datasets for tenant analytics"""
|
||||
|
||||
# Check permissions (only GT admin)
|
||||
if current_user.user_type != 'super_admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only GT admins can create virtual datasets"
|
||||
)
|
||||
|
||||
service = DremioService(db)
|
||||
|
||||
try:
|
||||
result = await service.create_virtual_datasets(tenant_id)
|
||||
return DatasetCreationResponse(**result)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to create datasets: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/metrics/performance/{tenant_id}")
|
||||
async def get_performance_metrics(
|
||||
tenant_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get real-time performance metrics for a tenant"""
|
||||
|
||||
# Check permissions
|
||||
if current_user.user_type != 'super_admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions to view metrics"
|
||||
)
|
||||
|
||||
if current_user.user_type == 'tenant_admin' and current_user.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot view metrics for other tenants"
|
||||
)
|
||||
|
||||
service = DremioService(db)
|
||||
|
||||
try:
|
||||
metrics = await service._get_performance_metrics(tenant_id)
|
||||
return metrics
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch metrics: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/alerts/{tenant_id}")
|
||||
async def get_security_alerts(
|
||||
tenant_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get security and operational alerts for a tenant"""
|
||||
|
||||
# Check permissions
|
||||
if current_user.user_type != 'super_admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions to view alerts"
|
||||
)
|
||||
|
||||
if current_user.user_type == 'tenant_admin' and current_user.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot view alerts for other tenants"
|
||||
)
|
||||
|
||||
service = DremioService(db)
|
||||
|
||||
try:
|
||||
alerts = await service._get_security_alerts(tenant_id)
|
||||
return {
|
||||
"tenant_id": tenant_id,
|
||||
"alerts": alerts,
|
||||
"total": len(alerts),
|
||||
"critical": len([a for a in alerts if a.get('severity') == 'critical']),
|
||||
"warning": len([a for a in alerts if a.get('severity') == 'warning']),
|
||||
"info": len([a for a in alerts if a.get('severity') == 'info'])
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch alerts: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/query-types")
|
||||
async def get_available_query_types(
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get list of available analytics query types"""
|
||||
|
||||
return {
|
||||
"query_types": [
|
||||
{
|
||||
"id": "user_activity",
|
||||
"name": "User Activity Analysis",
|
||||
"description": "Analyze user activity, token usage, and costs"
|
||||
},
|
||||
{
|
||||
"id": "resource_trends",
|
||||
"name": "Resource Usage Trends",
|
||||
"description": "View resource usage trends over time"
|
||||
},
|
||||
{
|
||||
"id": "cost_optimization",
|
||||
"name": "Cost Optimization Report",
|
||||
"description": "Identify cost optimization opportunities"
|
||||
}
|
||||
]
|
||||
}
|
||||
259
apps/control-panel-backend/app/api/v1/api_keys.py
Normal file
259
apps/control-panel-backend/app/api/v1/api_keys.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
API Key Management Endpoints
|
||||
"""
|
||||
from typing import List, Dict, Any, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.api_key_service import APIKeyService
|
||||
from app.core.auth import get_current_user
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/api/v1/api-keys", tags=["API Keys"])
|
||||
|
||||
|
||||
class SetAPIKeyRequest(BaseModel):
|
||||
"""Request model for setting an API key"""
|
||||
tenant_id: int
|
||||
provider: str
|
||||
api_key: str
|
||||
api_secret: Optional[str] = None
|
||||
enabled: bool = True
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class APIKeyResponse(BaseModel):
|
||||
"""Response model for API key operations"""
|
||||
tenant_id: int
|
||||
provider: str
|
||||
enabled: bool
|
||||
updated_at: str
|
||||
|
||||
|
||||
class APIKeyStatusResponse(BaseModel):
|
||||
"""Response model for API key status"""
|
||||
configured: bool
|
||||
enabled: bool
|
||||
updated_at: Optional[str]
|
||||
metadata: Optional[Dict[str, Any]]
|
||||
|
||||
|
||||
class TestAPIKeyResponse(BaseModel):
|
||||
"""Response model for API key testing"""
|
||||
provider: str
|
||||
valid: bool
|
||||
message: str
|
||||
status_code: Optional[int] = None
|
||||
error: Optional[str] = None
|
||||
error_type: Optional[str] = None # auth_failed, rate_limited, invalid_format, insufficient_permissions
|
||||
rate_limit_remaining: Optional[int] = None
|
||||
rate_limit_reset: Optional[str] = None
|
||||
models_available: Optional[int] = None # Count of models accessible with this key
|
||||
|
||||
|
||||
@router.post("/set", response_model=APIKeyResponse)
|
||||
async def set_api_key(
|
||||
request: SetAPIKeyRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Set or update an API key for a tenant"""
|
||||
|
||||
# Check permissions (must be GT admin or tenant admin)
|
||||
if current_user.user_type != 'super_admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions to manage API keys"
|
||||
)
|
||||
|
||||
|
||||
service = APIKeyService(db)
|
||||
|
||||
try:
|
||||
result = await service.set_api_key(
|
||||
tenant_id=request.tenant_id,
|
||||
provider=request.provider,
|
||||
api_key=request.api_key,
|
||||
api_secret=request.api_secret,
|
||||
enabled=request.enabled,
|
||||
metadata=request.metadata
|
||||
)
|
||||
return APIKeyResponse(**result)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to set API key: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenant/{tenant_id}", response_model=Dict[str, APIKeyStatusResponse])
|
||||
async def get_tenant_api_keys(
|
||||
tenant_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get all API keys for a tenant (without decryption)"""
|
||||
|
||||
# Check permissions
|
||||
if current_user.user_type != 'super_admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions to view API keys"
|
||||
)
|
||||
|
||||
|
||||
service = APIKeyService(db)
|
||||
|
||||
try:
|
||||
api_keys = await service.get_api_keys(tenant_id)
|
||||
return {
|
||||
provider: APIKeyStatusResponse(**info)
|
||||
for provider, info in api_keys.items()
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/test/{tenant_id}/{provider}", response_model=TestAPIKeyResponse)
|
||||
async def test_api_key(
|
||||
tenant_id: int,
|
||||
provider: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Test if an API key is valid"""
|
||||
|
||||
# Check permissions
|
||||
if current_user.user_type != 'super_admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions to test API keys"
|
||||
)
|
||||
|
||||
|
||||
service = APIKeyService(db)
|
||||
|
||||
try:
|
||||
result = await service.test_api_key(tenant_id, provider)
|
||||
return TestAPIKeyResponse(**result)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Test failed: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.put("/disable/{tenant_id}/{provider}")
|
||||
async def disable_api_key(
|
||||
tenant_id: int,
|
||||
provider: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Disable an API key without removing it"""
|
||||
|
||||
# Check permissions
|
||||
if current_user.user_type != 'super_admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions to manage API keys"
|
||||
)
|
||||
|
||||
|
||||
service = APIKeyService(db)
|
||||
|
||||
try:
|
||||
success = await service.disable_api_key(tenant_id, provider)
|
||||
return {"success": success, "provider": provider, "enabled": False}
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/remove/{tenant_id}/{provider}")
|
||||
async def remove_api_key(
|
||||
tenant_id: int,
|
||||
provider: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Completely remove an API key"""
|
||||
|
||||
# Check permissions (only GT admin can remove)
|
||||
if current_user.user_type != 'super_admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only GT admins can remove API keys"
|
||||
)
|
||||
|
||||
service = APIKeyService(db)
|
||||
|
||||
try:
|
||||
success = await service.remove_api_key(tenant_id, provider)
|
||||
if success:
|
||||
return {"success": True, "message": f"API key for {provider} removed"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"API key for {provider} not found"
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/providers", response_model=List[Dict[str, Any]])
|
||||
async def get_supported_providers(
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get list of supported API key providers"""
|
||||
|
||||
return APIKeyService.get_supported_providers()
|
||||
|
||||
|
||||
@router.get("/usage/{tenant_id}/{provider}")
|
||||
async def get_api_key_usage(
|
||||
tenant_id: int,
|
||||
provider: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get usage statistics for an API key"""
|
||||
|
||||
# Check permissions
|
||||
if current_user.user_type != 'super_admin':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions to view usage"
|
||||
)
|
||||
|
||||
|
||||
service = APIKeyService(db)
|
||||
|
||||
try:
|
||||
usage = await service.get_api_key_usage(tenant_id, provider)
|
||||
return usage
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e)
|
||||
)
|
||||
1095
apps/control-panel-backend/app/api/v1/models.py
Normal file
1095
apps/control-panel-backend/app/api/v1/models.py
Normal file
File diff suppressed because it is too large
Load Diff
760
apps/control-panel-backend/app/api/v1/resource_management.py
Normal file
760
apps/control-panel-backend/app/api/v1/resource_management.py
Normal file
@@ -0,0 +1,760 @@
|
||||
"""
|
||||
Resource Management API for GT 2.0 Control Panel
|
||||
|
||||
Provides comprehensive resource allocation and monitoring capabilities for admins.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional, Dict, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.auth import get_current_user
|
||||
from app.models.user import User
|
||||
from app.services.resource_allocation import ResourceAllocationService, ResourceType
|
||||
|
||||
router = APIRouter(prefix="/resource-management", tags=["Resource Management"])
|
||||
|
||||
|
||||
# Pydantic models
|
||||
class ResourceAllocationRequest(BaseModel):
|
||||
tenant_id: int
|
||||
template: str = Field(..., description="Resource template (startup, standard, enterprise)")
|
||||
|
||||
|
||||
class ResourceScalingRequest(BaseModel):
|
||||
tenant_id: int
|
||||
resource_type: str = Field(..., description="Resource type to scale")
|
||||
scale_factor: float = Field(..., ge=0.1, le=10.0, description="Scaling factor (1.0 = no change)")
|
||||
|
||||
|
||||
class ResourceUsageUpdateRequest(BaseModel):
|
||||
tenant_id: int
|
||||
resource_type: str
|
||||
usage_delta: float = Field(..., description="Change in usage (positive or negative)")
|
||||
|
||||
|
||||
class ResourceQuotaResponse(BaseModel):
|
||||
id: int
|
||||
tenant_id: int
|
||||
resource_type: str
|
||||
max_value: float
|
||||
current_usage: float
|
||||
usage_percentage: float
|
||||
warning_threshold: float
|
||||
critical_threshold: float
|
||||
unit: str
|
||||
cost_per_unit: float
|
||||
is_active: bool
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
|
||||
class ResourceUsageResponse(BaseModel):
|
||||
resource_type: str
|
||||
current_usage: float
|
||||
max_allowed: float
|
||||
percentage_used: float
|
||||
cost_accrued: float
|
||||
last_updated: str
|
||||
|
||||
|
||||
class ResourceAlertResponse(BaseModel):
|
||||
id: int
|
||||
tenant_id: int
|
||||
resource_type: str
|
||||
alert_level: str
|
||||
message: str
|
||||
current_usage: float
|
||||
max_value: float
|
||||
percentage_used: float
|
||||
acknowledged: bool
|
||||
acknowledged_by: Optional[str]
|
||||
acknowledged_at: Optional[str]
|
||||
created_at: str
|
||||
|
||||
|
||||
class SystemResourceOverviewResponse(BaseModel):
|
||||
timestamp: str
|
||||
resource_overview: Dict[str, Any]
|
||||
total_tenants: int
|
||||
|
||||
|
||||
class TenantCostResponse(BaseModel):
|
||||
tenant_id: int
|
||||
period_start: str
|
||||
period_end: str
|
||||
total_cost: float
|
||||
costs_by_resource: Dict[str, Any]
|
||||
currency: str
|
||||
|
||||
|
||||
@router.post("/allocate", status_code=status.HTTP_201_CREATED)
|
||||
async def allocate_tenant_resources(
|
||||
request: ResourceAllocationRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Allocate initial resources to a tenant based on template.
|
||||
"""
|
||||
# Check admin permissions
|
||||
if current_user.user_type != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Super admin privileges required"
|
||||
)
|
||||
|
||||
try:
|
||||
service = ResourceAllocationService(db)
|
||||
success = await service.allocate_resources(request.tenant_id, request.template)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Failed to allocate resources"
|
||||
)
|
||||
|
||||
return {"message": "Resources allocated successfully", "tenant_id": request.tenant_id}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Resource allocation failed: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenant/{tenant_id}/usage", response_model=Dict[str, ResourceUsageResponse])
|
||||
async def get_tenant_resource_usage(
|
||||
tenant_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get current resource usage for a specific tenant.
|
||||
"""
|
||||
# Check permissions
|
||||
if current_user.user_type != "super_admin":
|
||||
# Regular users can only view their own tenant
|
||||
if current_user.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions"
|
||||
)
|
||||
|
||||
try:
|
||||
service = ResourceAllocationService(db)
|
||||
usage_data = await service.get_tenant_resource_usage(tenant_id)
|
||||
|
||||
# Convert to response format
|
||||
response = {}
|
||||
for resource_type, data in usage_data.items():
|
||||
response[resource_type] = ResourceUsageResponse(
|
||||
resource_type=data.resource_type.value,
|
||||
current_usage=data.current_usage,
|
||||
max_allowed=data.max_allowed,
|
||||
percentage_used=data.percentage_used,
|
||||
cost_accrued=data.cost_accrued,
|
||||
last_updated=data.last_updated.isoformat()
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get resource usage: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/usage/update")
|
||||
async def update_resource_usage(
|
||||
request: ResourceUsageUpdateRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Update resource usage for a tenant (usually called by services).
|
||||
"""
|
||||
# This endpoint is typically called by services, so we allow tenant users for their own tenant
|
||||
if current_user.user_type != "super_admin":
|
||||
if current_user.tenant_id != request.tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions"
|
||||
)
|
||||
|
||||
try:
|
||||
# Validate resource type
|
||||
try:
|
||||
resource_type = ResourceType(request.resource_type)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid resource type: {request.resource_type}"
|
||||
)
|
||||
|
||||
service = ResourceAllocationService(db)
|
||||
success = await service.update_resource_usage(
|
||||
request.tenant_id,
|
||||
resource_type,
|
||||
request.usage_delta
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Failed to update resource usage (quota exceeded or not found)"
|
||||
)
|
||||
|
||||
return {"message": "Resource usage updated successfully"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update resource usage: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/scale")
|
||||
async def scale_tenant_resources(
|
||||
request: ResourceScalingRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Scale tenant resources up or down.
|
||||
"""
|
||||
# Check admin permissions
|
||||
if current_user.user_type != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Super admin privileges required"
|
||||
)
|
||||
|
||||
try:
|
||||
# Validate resource type
|
||||
try:
|
||||
resource_type = ResourceType(request.resource_type)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid resource type: {request.resource_type}"
|
||||
)
|
||||
|
||||
service = ResourceAllocationService(db)
|
||||
success = await service.scale_tenant_resources(
|
||||
request.tenant_id,
|
||||
resource_type,
|
||||
request.scale_factor
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Failed to scale resources"
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Resources scaled successfully",
|
||||
"tenant_id": request.tenant_id,
|
||||
"resource_type": request.resource_type,
|
||||
"scale_factor": request.scale_factor
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to scale resources: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenant/{tenant_id}/costs", response_model=TenantCostResponse)
|
||||
async def get_tenant_costs(
|
||||
tenant_id: int,
|
||||
start_date: Optional[str] = Query(None, description="Start date (ISO format)"),
|
||||
end_date: Optional[str] = Query(None, description="End date (ISO format)"),
|
||||
days: int = Query(30, ge=1, le=365, description="Days back from now if dates not specified"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get cost breakdown for a tenant over a date range.
|
||||
"""
|
||||
# Check permissions
|
||||
if current_user.user_type != "super_admin":
|
||||
if current_user.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions"
|
||||
)
|
||||
|
||||
try:
|
||||
# Parse dates
|
||||
if start_date and end_date:
|
||||
start_dt = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
|
||||
end_dt = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
|
||||
else:
|
||||
end_dt = datetime.utcnow()
|
||||
start_dt = end_dt - timedelta(days=days)
|
||||
|
||||
service = ResourceAllocationService(db)
|
||||
cost_data = await service.get_tenant_costs(tenant_id, start_dt, end_dt)
|
||||
|
||||
if not cost_data:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="No cost data found for tenant"
|
||||
)
|
||||
|
||||
return TenantCostResponse(**cost_data)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get tenant costs: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/alerts", response_model=List[ResourceAlertResponse])
|
||||
async def get_resource_alerts(
|
||||
tenant_id: Optional[int] = Query(None, description="Filter by tenant ID"),
|
||||
hours: int = Query(24, ge=1, le=168, description="Hours back to look for alerts"),
|
||||
alert_level: Optional[str] = Query(None, description="Filter by alert level"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get resource alerts for tenant(s).
|
||||
"""
|
||||
# Check permissions
|
||||
if current_user.user_type != "super_admin":
|
||||
# Regular users can only see their own tenant alerts
|
||||
if tenant_id and current_user.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions"
|
||||
)
|
||||
tenant_id = current_user.tenant_id
|
||||
|
||||
try:
|
||||
service = ResourceAllocationService(db)
|
||||
alerts = await service.get_resource_alerts(tenant_id, hours)
|
||||
|
||||
# Filter by alert level if specified
|
||||
if alert_level:
|
||||
alerts = [alert for alert in alerts if alert['alert_level'] == alert_level]
|
||||
|
||||
return [ResourceAlertResponse(**alert) for alert in alerts]
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get resource alerts: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/system/overview", response_model=SystemResourceOverviewResponse)
|
||||
async def get_system_resource_overview(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get system-wide resource usage overview (admin only).
|
||||
"""
|
||||
# Check admin permissions
|
||||
if current_user.user_type != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Super admin privileges required"
|
||||
)
|
||||
|
||||
try:
|
||||
service = ResourceAllocationService(db)
|
||||
overview = await service.get_system_resource_overview()
|
||||
|
||||
if not overview:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="No system resource data available"
|
||||
)
|
||||
|
||||
return SystemResourceOverviewResponse(**overview)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get system overview: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/alerts/{alert_id}/acknowledge")
|
||||
async def acknowledge_alert(
|
||||
alert_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Acknowledge a resource alert.
|
||||
"""
|
||||
try:
|
||||
from app.models.resource_usage import ResourceAlert
|
||||
from sqlalchemy import select, update
|
||||
|
||||
# Get the alert
|
||||
result = await db.execute(select(ResourceAlert).where(ResourceAlert.id == alert_id))
|
||||
alert = result.scalar_one_or_none()
|
||||
|
||||
if not alert:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Alert not found"
|
||||
)
|
||||
|
||||
# Check permissions
|
||||
if current_user.user_type != "super_admin":
|
||||
if current_user.tenant_id != alert.tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions"
|
||||
)
|
||||
|
||||
# Acknowledge the alert
|
||||
alert.acknowledge(current_user.email)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Alert acknowledged successfully", "alert_id": alert_id}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to acknowledge alert: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/templates")
|
||||
async def get_resource_templates(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get available resource allocation templates.
|
||||
"""
|
||||
try:
|
||||
# Return hardcoded templates for now
|
||||
templates = {
|
||||
"startup": {
|
||||
"name": "startup",
|
||||
"display_name": "Startup",
|
||||
"description": "Basic resources for small teams and development",
|
||||
"monthly_cost": 99.0,
|
||||
"resources": {
|
||||
"cpu": {"limit": 2.0, "unit": "cores"},
|
||||
"memory": {"limit": 4096, "unit": "MB"},
|
||||
"storage": {"limit": 10240, "unit": "MB"},
|
||||
"api_calls": {"limit": 10000, "unit": "calls/hour"},
|
||||
"model_inference": {"limit": 1000, "unit": "tokens"}
|
||||
}
|
||||
},
|
||||
"standard": {
|
||||
"name": "standard",
|
||||
"display_name": "Standard",
|
||||
"description": "Standard resources for production workloads",
|
||||
"monthly_cost": 299.0,
|
||||
"resources": {
|
||||
"cpu": {"limit": 4.0, "unit": "cores"},
|
||||
"memory": {"limit": 8192, "unit": "MB"},
|
||||
"storage": {"limit": 51200, "unit": "MB"},
|
||||
"api_calls": {"limit": 50000, "unit": "calls/hour"},
|
||||
"model_inference": {"limit": 10000, "unit": "tokens"}
|
||||
}
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "enterprise",
|
||||
"display_name": "Enterprise",
|
||||
"description": "High-performance resources for large organizations",
|
||||
"monthly_cost": 999.0,
|
||||
"resources": {
|
||||
"cpu": {"limit": 16.0, "unit": "cores"},
|
||||
"memory": {"limit": 32768, "unit": "MB"},
|
||||
"storage": {"limit": 102400, "unit": "MB"},
|
||||
"api_calls": {"limit": 200000, "unit": "calls/hour"},
|
||||
"model_inference": {"limit": 100000, "unit": "tokens"},
|
||||
"gpu_time": {"limit": 1000, "unit": "minutes"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {"templates": templates}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get resource templates: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# Agent Library Templates Endpoints
|
||||
|
||||
class AssistantTemplateRequest(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
category: str
|
||||
icon: str = "🤖"
|
||||
system_prompt: str
|
||||
capabilities: List[str] = []
|
||||
tags: List[str] = []
|
||||
access_groups: List[str] = []
|
||||
|
||||
|
||||
class AssistantTemplateResponse(BaseModel):
|
||||
id: str
|
||||
template_id: str
|
||||
name: str
|
||||
description: str
|
||||
category: str
|
||||
icon: str
|
||||
version: str
|
||||
status: str
|
||||
access_groups: List[str]
|
||||
deployment_count: int
|
||||
active_instances: int
|
||||
popularity_score: int
|
||||
last_updated: str
|
||||
created_by: str
|
||||
created_at: str
|
||||
capabilities: List[str]
|
||||
prompt_preview: str
|
||||
tags: List[str]
|
||||
compatibility: List[str]
|
||||
|
||||
|
||||
@router.get("/templates/", response_model=dict)
|
||||
async def list_agent_templates(
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
category: Optional[str] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
List agent templates for the agent library.
|
||||
"""
|
||||
try:
|
||||
# Mock data for now - replace with actual database queries
|
||||
mock_templates = [
|
||||
{
|
||||
"id": "1",
|
||||
"template_id": "cybersec_analyst",
|
||||
"name": "Cybersecurity Analyst",
|
||||
"description": "AI agent specialized in cybersecurity analysis, threat detection, and incident response",
|
||||
"category": "cybersecurity",
|
||||
"icon": "🛡️",
|
||||
"version": "1.2.0",
|
||||
"status": "published",
|
||||
"access_groups": ["security_team", "admin"],
|
||||
"deployment_count": 15,
|
||||
"active_instances": 8,
|
||||
"popularity_score": 92,
|
||||
"last_updated": "2024-01-15T10:30:00Z",
|
||||
"created_by": "admin@gt2.com",
|
||||
"created_at": "2024-01-10T14:20:00Z",
|
||||
"capabilities": ["threat_analysis", "log_analysis", "incident_response", "compliance_check"],
|
||||
"prompt_preview": "You are a cybersecurity analyst agent...",
|
||||
"tags": ["security", "analysis", "incident"],
|
||||
"compatibility": ["gpt-4", "claude-3"]
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"template_id": "research_assistant",
|
||||
"name": "Research Agent",
|
||||
"description": "Academic research helper for literature review, data analysis, and paper writing",
|
||||
"category": "research",
|
||||
"icon": "📚",
|
||||
"version": "2.0.1",
|
||||
"status": "published",
|
||||
"access_groups": ["researchers", "academics"],
|
||||
"deployment_count": 23,
|
||||
"active_instances": 12,
|
||||
"popularity_score": 88,
|
||||
"last_updated": "2024-01-12T16:45:00Z",
|
||||
"created_by": "research@gt2.com",
|
||||
"created_at": "2024-01-05T09:15:00Z",
|
||||
"capabilities": ["literature_search", "data_analysis", "citation_help", "writing_assistance"],
|
||||
"prompt_preview": "You are an academic research agent...",
|
||||
"tags": ["research", "academic", "writing"],
|
||||
"compatibility": ["gpt-4", "claude-3", "llama-2"]
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"template_id": "code_reviewer",
|
||||
"name": "Code Reviewer",
|
||||
"description": "AI agent for code review, best practices, and security vulnerability detection",
|
||||
"category": "development",
|
||||
"icon": "💻",
|
||||
"version": "1.5.0",
|
||||
"status": "testing",
|
||||
"access_groups": ["developers", "devops"],
|
||||
"deployment_count": 7,
|
||||
"active_instances": 4,
|
||||
"popularity_score": 85,
|
||||
"last_updated": "2024-01-18T11:20:00Z",
|
||||
"created_by": "dev@gt2.com",
|
||||
"created_at": "2024-01-15T13:30:00Z",
|
||||
"capabilities": ["code_review", "security_scan", "best_practices", "refactoring"],
|
||||
"prompt_preview": "You are a senior code reviewer...",
|
||||
"tags": ["development", "code", "security"],
|
||||
"compatibility": ["gpt-4", "codex"]
|
||||
}
|
||||
]
|
||||
|
||||
# Apply filters
|
||||
filtered_templates = mock_templates
|
||||
if category:
|
||||
filtered_templates = [t for t in filtered_templates if t["category"] == category]
|
||||
if status:
|
||||
filtered_templates = [t for t in filtered_templates if t["status"] == status]
|
||||
|
||||
# Apply pagination
|
||||
start = (page - 1) * limit
|
||||
end = start + limit
|
||||
paginated_templates = filtered_templates[start:end]
|
||||
|
||||
return {
|
||||
"data": {
|
||||
"templates": paginated_templates,
|
||||
"total": len(filtered_templates),
|
||||
"page": page,
|
||||
"limit": limit
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to list agent templates: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/access-groups/", response_model=dict)
|
||||
async def list_access_groups(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
List access groups for agent templates.
|
||||
"""
|
||||
try:
|
||||
# Mock data for now
|
||||
mock_access_groups = [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "security_team",
|
||||
"description": "Cybersecurity team with access to security-focused agents",
|
||||
"tenant_count": 8,
|
||||
"permissions": ["deploy_security", "manage_policies", "view_logs"]
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "researchers",
|
||||
"description": "Academic researchers and data analysts",
|
||||
"tenant_count": 12,
|
||||
"permissions": ["deploy_research", "access_data", "export_results"]
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "developers",
|
||||
"description": "Software development teams",
|
||||
"tenant_count": 15,
|
||||
"permissions": ["deploy_code", "review_access", "ci_cd_integration"]
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"name": "admin",
|
||||
"description": "System administrators with full access",
|
||||
"tenant_count": 3,
|
||||
"permissions": ["full_access", "manage_templates", "system_config"]
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
"data": {
|
||||
"access_groups": mock_access_groups
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to list access groups: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/deployments/", response_model=dict)
|
||||
async def get_deployments(
|
||||
template_id: Optional[str] = Query(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get deployment status for agent templates.
|
||||
"""
|
||||
try:
|
||||
# Mock data for now
|
||||
mock_deployments = [
|
||||
{
|
||||
"id": "1",
|
||||
"template_id": "cybersec_analyst",
|
||||
"tenant_name": "Acme Corp",
|
||||
"tenant_id": "acme-corp",
|
||||
"status": "completed",
|
||||
"deployed_at": "2024-01-16T09:30:00Z",
|
||||
"customizations": {"theme": "dark", "language": "en"}
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"template_id": "research_assistant",
|
||||
"tenant_name": "University Lab",
|
||||
"tenant_id": "uni-lab",
|
||||
"status": "processing",
|
||||
"customizations": {"domain": "biology", "access_level": "restricted"}
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"template_id": "code_reviewer",
|
||||
"tenant_name": "DevTeam Inc",
|
||||
"tenant_id": "devteam-inc",
|
||||
"status": "failed",
|
||||
"error_message": "Insufficient resources available",
|
||||
"customizations": {"languages": ["python", "javascript"]}
|
||||
}
|
||||
]
|
||||
|
||||
# Filter by template_id if provided
|
||||
if template_id:
|
||||
mock_deployments = [d for d in mock_deployments if d["template_id"] == template_id]
|
||||
|
||||
return {
|
||||
"data": {
|
||||
"deployments": mock_deployments
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get deployments: {str(e)}"
|
||||
)
|
||||
531
apps/control-panel-backend/app/api/v1/resources_cbrest.py
Normal file
531
apps/control-panel-backend/app/api/v1/resources_cbrest.py
Normal file
@@ -0,0 +1,531 @@
|
||||
"""
|
||||
GT 2.0 Control Panel - Resources API with CB-REST Standards
|
||||
"""
|
||||
from typing import List, Optional, Dict, Any
|
||||
from fastapi import APIRouter, Depends, Query, BackgroundTasks, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel, Field
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.api_standards import (
|
||||
format_response,
|
||||
format_error,
|
||||
ErrorCode,
|
||||
APIError,
|
||||
require_capability
|
||||
)
|
||||
from app.services.resource_service import ResourceService
|
||||
from app.services.groq_service import groq_service
|
||||
from app.models.ai_resource import AIResource
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/resources", tags=["AI Resources"])
|
||||
|
||||
|
||||
# Request/Response Models
|
||||
class ResourceCreateRequest(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
resource_type: str
|
||||
provider: str
|
||||
model_name: Optional[str] = None
|
||||
personalization_mode: str = "shared"
|
||||
primary_endpoint: Optional[str] = None
|
||||
api_endpoints: List[str] = []
|
||||
failover_endpoints: List[str] = []
|
||||
health_check_url: Optional[str] = None
|
||||
max_requests_per_minute: int = 60
|
||||
max_tokens_per_request: int = 4000
|
||||
cost_per_1k_tokens: float = 0.0
|
||||
configuration: Dict[str, Any] = {}
|
||||
|
||||
|
||||
class ResourceUpdateRequest(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
personalization_mode: Optional[str] = None
|
||||
primary_endpoint: Optional[str] = None
|
||||
api_endpoints: Optional[List[str]] = None
|
||||
failover_endpoints: Optional[List[str]] = None
|
||||
health_check_url: Optional[str] = None
|
||||
max_requests_per_minute: Optional[int] = None
|
||||
max_tokens_per_request: Optional[int] = None
|
||||
cost_per_1k_tokens: Optional[float] = None
|
||||
configuration: Optional[Dict[str, Any]] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class BulkAssignRequest(BaseModel):
|
||||
resource_ids: List[int]
|
||||
tenant_ids: List[int]
|
||||
usage_limits: Optional[Dict[str, Any]] = None
|
||||
custom_config: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_resources(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
resource_type: Optional[str] = Query(None, description="Filter by resource type"),
|
||||
provider: Optional[str] = Query(None, description="Filter by provider"),
|
||||
is_active: Optional[bool] = Query(None, description="Filter by active status"),
|
||||
search: Optional[str] = Query(None, description="Search in name and description"),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
offset: int = Query(0, ge=0)
|
||||
):
|
||||
"""
|
||||
List all AI resources with filtering and pagination
|
||||
|
||||
CB-REST Capability Required: resource:*:read
|
||||
"""
|
||||
try:
|
||||
service = ResourceService(db)
|
||||
|
||||
# Build filters
|
||||
filters = {}
|
||||
if resource_type:
|
||||
filters['resource_type'] = resource_type
|
||||
if provider:
|
||||
filters['provider'] = provider
|
||||
if is_active is not None:
|
||||
filters['is_active'] = is_active
|
||||
if search:
|
||||
filters['search'] = search
|
||||
|
||||
resources = await service.list_resources(
|
||||
filters=filters,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
# Get categories for easier filtering
|
||||
categories = await service.get_resource_categories()
|
||||
|
||||
return format_response(
|
||||
data={
|
||||
"resources": [r.dict() for r in resources],
|
||||
"categories": categories,
|
||||
"total": len(resources),
|
||||
"limit": limit,
|
||||
"offset": offset
|
||||
},
|
||||
capability_used="resource:*:read",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list resources: {e}")
|
||||
return format_error(
|
||||
code=ErrorCode.SYSTEM_ERROR,
|
||||
message="Internal server error",
|
||||
capability_used="resource:*:read",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_resource(
|
||||
request: Request,
|
||||
resource: ResourceCreateRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Create a new AI resource
|
||||
|
||||
CB-REST Capability Required: resource:*:create
|
||||
"""
|
||||
try:
|
||||
service = ResourceService(db)
|
||||
|
||||
# Create resource
|
||||
new_resource = await service.create_resource(
|
||||
name=resource.name,
|
||||
description=resource.description,
|
||||
resource_type=resource.resource_type,
|
||||
provider=resource.provider,
|
||||
model_name=resource.model_name,
|
||||
personalization_mode=resource.personalization_mode,
|
||||
primary_endpoint=resource.primary_endpoint,
|
||||
api_endpoints=resource.api_endpoints,
|
||||
failover_endpoints=resource.failover_endpoints,
|
||||
health_check_url=resource.health_check_url,
|
||||
max_requests_per_minute=resource.max_requests_per_minute,
|
||||
max_tokens_per_request=resource.max_tokens_per_request,
|
||||
cost_per_1k_tokens=resource.cost_per_1k_tokens,
|
||||
configuration=resource.configuration,
|
||||
created_by=getattr(request.state, 'user_email', 'system')
|
||||
)
|
||||
|
||||
# Schedule health check
|
||||
if resource.health_check_url:
|
||||
background_tasks.add_task(
|
||||
service.perform_health_check,
|
||||
new_resource.id
|
||||
)
|
||||
|
||||
return format_response(
|
||||
data={
|
||||
"resource_id": new_resource.id,
|
||||
"uuid": new_resource.uuid,
|
||||
"health_check_scheduled": bool(resource.health_check_url)
|
||||
},
|
||||
capability_used="resource:*:create",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid request for resource creation: {e}", exc_info=True)
|
||||
return format_error(
|
||||
code=ErrorCode.INVALID_REQUEST,
|
||||
message="Invalid request parameters",
|
||||
capability_used="resource:*:create",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create resource: {e}")
|
||||
return format_error(
|
||||
code=ErrorCode.SYSTEM_ERROR,
|
||||
message="Internal server error",
|
||||
capability_used="resource:*:create",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{resource_id}")
|
||||
async def get_resource(
|
||||
request: Request,
|
||||
resource_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get a specific AI resource with full configuration and metrics
|
||||
|
||||
CB-REST Capability Required: resource:{resource_id}:read
|
||||
"""
|
||||
try:
|
||||
service = ResourceService(db)
|
||||
resource = await service.get_resource(resource_id)
|
||||
|
||||
if not resource:
|
||||
return format_error(
|
||||
code=ErrorCode.RESOURCE_NOT_FOUND,
|
||||
message=f"Resource {resource_id} not found",
|
||||
capability_used=f"resource:{resource_id}:read",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
|
||||
# Get additional metrics
|
||||
metrics = await service.get_resource_metrics(resource_id)
|
||||
|
||||
return format_response(
|
||||
data={
|
||||
**resource.dict(),
|
||||
"metrics": metrics
|
||||
},
|
||||
capability_used=f"resource:{resource_id}:read",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get resource {resource_id}: {e}")
|
||||
return format_error(
|
||||
code=ErrorCode.SYSTEM_ERROR,
|
||||
message="Internal server error",
|
||||
capability_used=f"resource:{resource_id}:read",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{resource_id}")
|
||||
async def update_resource(
|
||||
request: Request,
|
||||
resource_id: int,
|
||||
update: ResourceUpdateRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Update an AI resource configuration
|
||||
|
||||
CB-REST Capability Required: resource:{resource_id}:update
|
||||
"""
|
||||
try:
|
||||
service = ResourceService(db)
|
||||
|
||||
# Update resource
|
||||
updated_resource = await service.update_resource(
|
||||
resource_id=resource_id,
|
||||
**update.dict(exclude_unset=True)
|
||||
)
|
||||
|
||||
if not updated_resource:
|
||||
return format_error(
|
||||
code=ErrorCode.RESOURCE_NOT_FOUND,
|
||||
message=f"Resource {resource_id} not found",
|
||||
capability_used=f"resource:{resource_id}:update",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
|
||||
# Schedule health check if endpoint changed
|
||||
if update.primary_endpoint or update.health_check_url:
|
||||
background_tasks.add_task(
|
||||
service.perform_health_check,
|
||||
resource_id
|
||||
)
|
||||
|
||||
return format_response(
|
||||
data={
|
||||
"resource_id": resource_id,
|
||||
"updated_fields": list(update.dict(exclude_unset=True).keys()),
|
||||
"health_check_required": bool(update.primary_endpoint or update.health_check_url)
|
||||
},
|
||||
capability_used=f"resource:{resource_id}:update",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid request for resource update: {e}", exc_info=True)
|
||||
return format_error(
|
||||
code=ErrorCode.INVALID_REQUEST,
|
||||
message="Invalid request parameters",
|
||||
capability_used=f"resource:{resource_id}:update",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update resource {resource_id}: {e}")
|
||||
return format_error(
|
||||
code=ErrorCode.SYSTEM_ERROR,
|
||||
message="Internal server error",
|
||||
capability_used=f"resource:{resource_id}:update",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{resource_id}")
|
||||
async def delete_resource(
|
||||
request: Request,
|
||||
resource_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Archive an AI resource (soft delete)
|
||||
|
||||
CB-REST Capability Required: resource:{resource_id}:delete
|
||||
"""
|
||||
try:
|
||||
service = ResourceService(db)
|
||||
|
||||
# Get affected tenants before deletion
|
||||
affected_tenants = await service.get_resource_tenants(resource_id)
|
||||
|
||||
# Archive resource
|
||||
success = await service.archive_resource(resource_id)
|
||||
|
||||
if not success:
|
||||
return format_error(
|
||||
code=ErrorCode.RESOURCE_NOT_FOUND,
|
||||
message=f"Resource {resource_id} not found",
|
||||
capability_used=f"resource:{resource_id}:delete",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
|
||||
return format_response(
|
||||
data={
|
||||
"archived": True,
|
||||
"affected_tenants": len(affected_tenants)
|
||||
},
|
||||
capability_used=f"resource:{resource_id}:delete",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete resource {resource_id}: {e}")
|
||||
return format_error(
|
||||
code=ErrorCode.SYSTEM_ERROR,
|
||||
message="Internal server error",
|
||||
capability_used=f"resource:{resource_id}:delete",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{resource_id}/health-check")
|
||||
async def check_resource_health(
|
||||
request: Request,
|
||||
resource_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Perform health check on a resource
|
||||
|
||||
CB-REST Capability Required: resource:{resource_id}:health
|
||||
"""
|
||||
try:
|
||||
service = ResourceService(db)
|
||||
|
||||
# Perform health check
|
||||
health_result = await service.perform_health_check(resource_id)
|
||||
|
||||
if not health_result:
|
||||
return format_error(
|
||||
code=ErrorCode.RESOURCE_NOT_FOUND,
|
||||
message=f"Resource {resource_id} not found",
|
||||
capability_used=f"resource:{resource_id}:health",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
|
||||
return format_response(
|
||||
data=health_result,
|
||||
capability_used=f"resource:{resource_id}:health",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check health for resource {resource_id}: {e}")
|
||||
return format_error(
|
||||
code=ErrorCode.SYSTEM_ERROR,
|
||||
message="Internal server error",
|
||||
capability_used=f"resource:{resource_id}:health",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/types")
|
||||
async def get_resource_types(request: Request):
|
||||
"""
|
||||
Get all available resource types and their access groups
|
||||
|
||||
CB-REST Capability Required: resource:*:read
|
||||
"""
|
||||
try:
|
||||
resource_types = {
|
||||
"ai_ml": {
|
||||
"name": "AI/ML Models",
|
||||
"subtypes": ["llm", "embedding", "image_generation", "function_calling", "custom_model"],
|
||||
"access_groups": ["ai_advanced", "ai_basic"]
|
||||
},
|
||||
"rag_engine": {
|
||||
"name": "RAG Engines",
|
||||
"subtypes": ["document_processor", "vector_database", "retrieval_strategy"],
|
||||
"access_groups": ["knowledge_management", "document_processing"]
|
||||
},
|
||||
"agentic_workflow": {
|
||||
"name": "Agentic Workflows",
|
||||
"subtypes": ["single_agent", "multi_agent", "workflow_chain", "collaborative_agent"],
|
||||
"access_groups": ["advanced_workflows", "automation"]
|
||||
},
|
||||
"app_integration": {
|
||||
"name": "App Integrations",
|
||||
"subtypes": ["communication_app", "development_app", "project_management_app", "database_connector"],
|
||||
"access_groups": ["integration_tools", "development_tools"]
|
||||
},
|
||||
"external_service": {
|
||||
"name": "External Web Services",
|
||||
"subtypes": ["educational_service", "cybersecurity_service", "development_service", "remote_access_service"],
|
||||
"access_groups": ["external_platforms", "remote_labs"]
|
||||
},
|
||||
"ai_literacy": {
|
||||
"name": "AI Literacy & Cognitive Skills",
|
||||
"subtypes": ["strategic_game", "logic_puzzle", "philosophical_dilemma", "educational_content"],
|
||||
"access_groups": ["ai_literacy", "educational_tools"]
|
||||
}
|
||||
}
|
||||
|
||||
return format_response(
|
||||
data={
|
||||
"resource_types": resource_types,
|
||||
"access_groups": list(set(
|
||||
group
|
||||
for rt in resource_types.values()
|
||||
for group in rt["access_groups"]
|
||||
))
|
||||
},
|
||||
capability_used="resource:*:read",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get resource types: {e}")
|
||||
return format_error(
|
||||
code=ErrorCode.SYSTEM_ERROR,
|
||||
message="Internal server error",
|
||||
capability_used="resource:*:read",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/bulk/assign")
|
||||
async def bulk_assign_resources(
|
||||
request: Request,
|
||||
assignment: BulkAssignRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Bulk assign resources to tenants
|
||||
|
||||
CB-REST Capability Required: resource:*:assign
|
||||
"""
|
||||
try:
|
||||
service = ResourceService(db)
|
||||
|
||||
results = await service.bulk_assign_resources(
|
||||
resource_ids=assignment.resource_ids,
|
||||
tenant_ids=assignment.tenant_ids,
|
||||
usage_limits=assignment.usage_limits,
|
||||
custom_config=assignment.custom_config,
|
||||
assigned_by=getattr(request.state, 'user_email', 'system')
|
||||
)
|
||||
|
||||
return format_response(
|
||||
data={
|
||||
"operation_id": str(uuid.uuid4()),
|
||||
"assigned": results["assigned"],
|
||||
"failed": results["failed"]
|
||||
},
|
||||
capability_used="resource:*:assign",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to bulk assign resources: {e}")
|
||||
return format_error(
|
||||
code=ErrorCode.SYSTEM_ERROR,
|
||||
message="Internal server error",
|
||||
capability_used="resource:*:assign",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/bulk/health-check")
|
||||
async def bulk_health_check(
|
||||
request: Request,
|
||||
resource_ids: List[int],
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Schedule health checks for multiple resources
|
||||
|
||||
CB-REST Capability Required: resource:*:health
|
||||
"""
|
||||
try:
|
||||
service = ResourceService(db)
|
||||
|
||||
# Schedule health checks
|
||||
for resource_id in resource_ids:
|
||||
background_tasks.add_task(
|
||||
service.perform_health_check,
|
||||
resource_id
|
||||
)
|
||||
|
||||
return format_response(
|
||||
data={
|
||||
"operation_id": str(uuid.uuid4()),
|
||||
"scheduled_checks": len(resource_ids)
|
||||
},
|
||||
capability_used="resource:*:health",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to schedule bulk health checks: {e}")
|
||||
return format_error(
|
||||
code=ErrorCode.SYSTEM_ERROR,
|
||||
message="Internal server error",
|
||||
capability_used="resource:*:health",
|
||||
request_id=getattr(request.state, 'request_id', None)
|
||||
)
|
||||
580
apps/control-panel-backend/app/api/v1/system.py
Normal file
580
apps/control-panel-backend/app/api/v1/system.py
Normal file
@@ -0,0 +1,580 @@
|
||||
"""
|
||||
System Management API Endpoints
|
||||
"""
|
||||
import asyncio
|
||||
import subprocess
|
||||
import json
|
||||
import shutil
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc, text
|
||||
from pydantic import BaseModel, Field
|
||||
import structlog
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.auth import get_current_user
|
||||
from app.models.user import User
|
||||
from app.models.system import SystemVersion
|
||||
from app.services.update_service import UpdateService
|
||||
from app.services.backup_service import BackupService
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
router = APIRouter(prefix="/api/v1/system", tags=["System Management"])
|
||||
|
||||
|
||||
# Request/Response Models
|
||||
class VersionResponse(BaseModel):
|
||||
"""Response model for version information"""
|
||||
version: str
|
||||
installed_at: str
|
||||
installed_by: Optional[str]
|
||||
is_current: bool
|
||||
git_commit: Optional[str]
|
||||
|
||||
|
||||
class SystemInfoResponse(BaseModel):
|
||||
"""Response model for system information"""
|
||||
current_version: str
|
||||
version: str = "" # Alias for frontend compatibility - will be set from current_version
|
||||
installation_date: str
|
||||
container_count: Optional[int] = None
|
||||
database_status: str = "healthy"
|
||||
|
||||
|
||||
class CheckUpdateResponse(BaseModel):
|
||||
"""Response model for update check"""
|
||||
update_available: bool
|
||||
available: bool = False # Alias for frontend compatibility
|
||||
current_version: str
|
||||
latest_version: Optional[str]
|
||||
update_type: Optional[str] = None # "major", "minor", or "patch"
|
||||
release_notes: Optional[str]
|
||||
published_at: Optional[str]
|
||||
released_at: Optional[str] = None # Alias for frontend compatibility
|
||||
download_url: Optional[str]
|
||||
checked_at: str # Timestamp when the check was performed
|
||||
|
||||
|
||||
class ValidationCheckResult(BaseModel):
|
||||
"""Individual validation check result"""
|
||||
name: str
|
||||
passed: bool
|
||||
message: str
|
||||
details: Dict[str, Any] = {}
|
||||
|
||||
|
||||
class ValidateUpdateResponse(BaseModel):
|
||||
"""Response model for update validation"""
|
||||
valid: bool
|
||||
checks: List[ValidationCheckResult]
|
||||
warnings: List[str] = []
|
||||
errors: List[str] = []
|
||||
|
||||
|
||||
class ValidateUpdateRequest(BaseModel):
|
||||
"""Request model for validating an update"""
|
||||
target_version: str = Field(..., description="Target version to validate")
|
||||
|
||||
|
||||
class StartUpdateRequest(BaseModel):
|
||||
"""Request model for starting an update"""
|
||||
target_version: str = Field(..., description="Version to update to")
|
||||
create_backup: bool = Field(default=True, description="Create backup before update")
|
||||
|
||||
|
||||
class StartUpdateResponse(BaseModel):
|
||||
"""Response model for starting an update"""
|
||||
update_id: str
|
||||
target_version: str
|
||||
message: str = "Update initiated"
|
||||
|
||||
|
||||
class UpdateStatusResponse(BaseModel):
|
||||
"""Response model for update status"""
|
||||
update_id: str
|
||||
target_version: str
|
||||
status: str
|
||||
started_at: str
|
||||
completed_at: Optional[str]
|
||||
current_stage: Optional[str]
|
||||
logs: List[Dict[str, Any]] = []
|
||||
error_message: Optional[str]
|
||||
backup_id: Optional[int]
|
||||
|
||||
|
||||
class RollbackRequest(BaseModel):
|
||||
"""Request model for rollback"""
|
||||
reason: Optional[str] = Field(None, description="Reason for rollback")
|
||||
|
||||
|
||||
class BackupResponse(BaseModel):
|
||||
"""Response model for backup information"""
|
||||
id: int
|
||||
uuid: str
|
||||
backup_type: str
|
||||
created_at: str
|
||||
size_mb: Optional[float] # Keep for backward compatibility
|
||||
size: Optional[int] = None # Size in bytes for frontend
|
||||
version: Optional[str]
|
||||
description: Optional[str]
|
||||
is_valid: bool
|
||||
download_url: Optional[str] = None # Download URL if available
|
||||
|
||||
|
||||
class CreateBackupRequest(BaseModel):
|
||||
"""Request model for creating a backup"""
|
||||
backup_type: str = Field(default="manual", description="Type of backup")
|
||||
description: Optional[str] = Field(None, description="Backup description")
|
||||
|
||||
|
||||
class RestoreBackupRequest(BaseModel):
|
||||
"""Request model for restoring a backup"""
|
||||
backup_id: str = Field(..., description="UUID of backup to restore")
|
||||
components: Optional[List[str]] = Field(None, description="Components to restore")
|
||||
|
||||
|
||||
class ContainerStatus(BaseModel):
|
||||
"""Container status from Docker"""
|
||||
name: str
|
||||
cluster: str # "admin", "tenant", "resource"
|
||||
state: str # "running", "exited", "paused"
|
||||
health: str # "healthy", "unhealthy", "starting", "none"
|
||||
uptime: str
|
||||
ports: List[str] = []
|
||||
|
||||
|
||||
class DatabaseStats(BaseModel):
|
||||
"""PostgreSQL database statistics"""
|
||||
connections_active: int
|
||||
connections_max: int
|
||||
cache_hit_ratio: float
|
||||
database_size: str
|
||||
transactions_committed: int
|
||||
|
||||
|
||||
class ClusterSummary(BaseModel):
|
||||
"""Cluster health summary"""
|
||||
name: str
|
||||
healthy: int
|
||||
unhealthy: int
|
||||
total: int
|
||||
|
||||
|
||||
class SystemHealthDetailedResponse(BaseModel):
|
||||
"""Detailed system health response"""
|
||||
overall_status: str
|
||||
containers: List[ContainerStatus]
|
||||
clusters: List[ClusterSummary]
|
||||
database: DatabaseStats
|
||||
version: str
|
||||
|
||||
|
||||
# Helper Functions
|
||||
async def _get_container_status() -> List[ContainerStatus]:
|
||||
"""Get container status from Docker Compose"""
|
||||
try:
|
||||
# Run docker compose ps with JSON format
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"docker", "compose", "ps", "--format", "json",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd="/Users/hackweasel/Documents/GT-2.0"
|
||||
)
|
||||
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
logger.error("docker_compose_ps_failed", stderr=stderr.decode())
|
||||
return []
|
||||
|
||||
# Parse JSON output (one JSON object per line)
|
||||
containers = []
|
||||
for line in stdout.decode().strip().split('\n'):
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
container_data = json.loads(line)
|
||||
name = container_data.get("Name", "")
|
||||
state = container_data.get("State", "unknown")
|
||||
health = container_data.get("Health", "none")
|
||||
|
||||
# Map container name to cluster
|
||||
cluster = "unknown"
|
||||
if "controlpanel" in name.lower():
|
||||
cluster = "admin"
|
||||
elif "tenant" in name.lower() and "controlpanel" not in name.lower():
|
||||
cluster = "tenant"
|
||||
elif "resource" in name.lower() or "vllm" in name.lower():
|
||||
cluster = "resource"
|
||||
|
||||
# Extract ports
|
||||
ports = []
|
||||
publishers = container_data.get("Publishers", [])
|
||||
if publishers:
|
||||
for pub in publishers:
|
||||
if pub.get("PublishedPort"):
|
||||
ports.append(f"{pub.get('PublishedPort')}:{pub.get('TargetPort')}")
|
||||
|
||||
# Get uptime from status
|
||||
status_text = container_data.get("Status", "")
|
||||
uptime = status_text if status_text else "unknown"
|
||||
|
||||
containers.append(ContainerStatus(
|
||||
name=name,
|
||||
cluster=cluster,
|
||||
state=state,
|
||||
health=health if health else "none",
|
||||
uptime=uptime,
|
||||
ports=ports
|
||||
))
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning("failed_to_parse_container_json", line=line, error=str(e))
|
||||
continue
|
||||
|
||||
return containers
|
||||
|
||||
except Exception as e:
|
||||
# Docker is not available inside the container - this is expected behavior
|
||||
logger.debug("docker_not_available", error=str(e))
|
||||
return []
|
||||
|
||||
|
||||
async def _get_database_stats(db: AsyncSession) -> DatabaseStats:
|
||||
"""Get PostgreSQL database statistics"""
|
||||
try:
|
||||
# Get connection and transaction stats
|
||||
stats_query = text("""
|
||||
SELECT
|
||||
numbackends as active_connections,
|
||||
xact_commit as transactions_committed,
|
||||
ROUND(100.0 * blks_hit / NULLIF(blks_read + blks_hit, 0), 1) as cache_hit_ratio
|
||||
FROM pg_stat_database
|
||||
WHERE datname = current_database()
|
||||
""")
|
||||
|
||||
stats_result = await db.execute(stats_query)
|
||||
stats = stats_result.fetchone()
|
||||
|
||||
# Get database size
|
||||
size_query = text("SELECT pg_size_pretty(pg_database_size(current_database()))")
|
||||
size_result = await db.execute(size_query)
|
||||
size = size_result.scalar()
|
||||
|
||||
# Get max connections
|
||||
max_conn_query = text("SELECT current_setting('max_connections')::int")
|
||||
max_conn_result = await db.execute(max_conn_query)
|
||||
max_connections = max_conn_result.scalar()
|
||||
|
||||
return DatabaseStats(
|
||||
connections_active=stats[0] if stats else 0,
|
||||
connections_max=max_connections if max_connections else 100,
|
||||
cache_hit_ratio=float(stats[2]) if stats and stats[2] else 0.0,
|
||||
database_size=size if size else "0 bytes",
|
||||
transactions_committed=stats[1] if stats else 0
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("failed_to_get_database_stats", error=str(e))
|
||||
# Return default stats on error
|
||||
return DatabaseStats(
|
||||
connections_active=0,
|
||||
connections_max=100,
|
||||
cache_hit_ratio=0.0,
|
||||
database_size="unknown",
|
||||
transactions_committed=0
|
||||
)
|
||||
|
||||
|
||||
def _aggregate_clusters(containers: List[ContainerStatus]) -> List[ClusterSummary]:
|
||||
"""Aggregate container health by cluster"""
|
||||
cluster_data = {}
|
||||
|
||||
for container in containers:
|
||||
cluster_name = container.cluster
|
||||
|
||||
if cluster_name not in cluster_data:
|
||||
cluster_data[cluster_name] = {"healthy": 0, "unhealthy": 0, "total": 0}
|
||||
|
||||
cluster_data[cluster_name]["total"] += 1
|
||||
|
||||
# Consider container healthy if running and health is healthy/none
|
||||
if container.state == "running" and container.health in ["healthy", "none"]:
|
||||
cluster_data[cluster_name]["healthy"] += 1
|
||||
else:
|
||||
cluster_data[cluster_name]["unhealthy"] += 1
|
||||
|
||||
# Convert to ClusterSummary objects
|
||||
summaries = []
|
||||
for cluster_name, data in cluster_data.items():
|
||||
summaries.append(ClusterSummary(
|
||||
name=cluster_name,
|
||||
healthy=data["healthy"],
|
||||
unhealthy=data["unhealthy"],
|
||||
total=data["total"]
|
||||
))
|
||||
|
||||
return summaries
|
||||
|
||||
|
||||
# Dependency for admin-only access
|
||||
async def require_admin(current_user: User = Depends(get_current_user)):
|
||||
"""Ensure user is a super admin"""
|
||||
if current_user.user_type != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Administrator access required"
|
||||
)
|
||||
return current_user
|
||||
|
||||
|
||||
# Version Endpoints
|
||||
@router.get("/version", response_model=SystemInfoResponse)
|
||||
async def get_system_version(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Get current system version and information"""
|
||||
# Get current version
|
||||
stmt = select(SystemVersion).where(
|
||||
SystemVersion.is_current == True
|
||||
).order_by(desc(SystemVersion.installed_at)).limit(1)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
current = result.scalar_one_or_none()
|
||||
|
||||
if not current:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="System version not found. Please run database migrations: alembic upgrade head"
|
||||
)
|
||||
|
||||
return SystemInfoResponse(
|
||||
current_version=current.version,
|
||||
version=current.version, # Set version same as current_version for frontend compatibility
|
||||
installation_date=current.installed_at.isoformat(),
|
||||
database_status="healthy"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/health-detailed", response_model=SystemHealthDetailedResponse)
|
||||
async def get_detailed_health(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Get comprehensive system health with real container and database metrics"""
|
||||
# Get current version
|
||||
stmt = select(SystemVersion).where(
|
||||
SystemVersion.is_current == True
|
||||
).order_by(desc(SystemVersion.installed_at)).limit(1)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
current_version = result.scalar_one_or_none()
|
||||
version_str = current_version.version if current_version else "unknown"
|
||||
|
||||
# Gather system metrics concurrently
|
||||
containers = await _get_container_status()
|
||||
database_stats = await _get_database_stats(db)
|
||||
cluster_summaries = _aggregate_clusters(containers)
|
||||
|
||||
# Determine overall status
|
||||
unhealthy_count = sum(cluster.unhealthy for cluster in cluster_summaries)
|
||||
overall_status = "healthy" if unhealthy_count == 0 else "degraded"
|
||||
|
||||
return SystemHealthDetailedResponse(
|
||||
overall_status=overall_status,
|
||||
containers=containers,
|
||||
clusters=cluster_summaries,
|
||||
database=database_stats,
|
||||
version=version_str
|
||||
)
|
||||
|
||||
|
||||
# Update Endpoints
|
||||
@router.get("/check-update", response_model=CheckUpdateResponse)
|
||||
async def check_for_updates(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Check for available system updates"""
|
||||
service = UpdateService(db)
|
||||
return await service.check_for_updates()
|
||||
|
||||
|
||||
@router.post("/validate-update", response_model=ValidateUpdateResponse)
|
||||
async def validate_update(
|
||||
request: ValidateUpdateRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Run pre-update validation checks"""
|
||||
service = UpdateService(db)
|
||||
return await service.validate_update(request.target_version)
|
||||
|
||||
|
||||
@router.post("/update", response_model=StartUpdateResponse)
|
||||
async def start_update(
|
||||
request: StartUpdateRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Start system update process"""
|
||||
service = UpdateService(db)
|
||||
update_id = await service.execute_update(
|
||||
target_version=request.target_version,
|
||||
create_backup=request.create_backup,
|
||||
started_by=current_user.email
|
||||
)
|
||||
|
||||
return StartUpdateResponse(
|
||||
update_id=update_id,
|
||||
target_version=request.target_version
|
||||
)
|
||||
|
||||
|
||||
@router.get("/update/{update_id}/status", response_model=UpdateStatusResponse)
|
||||
async def get_update_status(
|
||||
update_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Get status of an update job"""
|
||||
service = UpdateService(db)
|
||||
status_data = await service.get_update_status(update_id)
|
||||
|
||||
return UpdateStatusResponse(
|
||||
update_id=status_data["uuid"],
|
||||
target_version=status_data["target_version"],
|
||||
status=status_data["status"],
|
||||
started_at=status_data["started_at"],
|
||||
completed_at=status_data.get("completed_at"),
|
||||
current_stage=status_data.get("current_stage"),
|
||||
logs=status_data.get("logs", []),
|
||||
error_message=status_data.get("error_message"),
|
||||
backup_id=status_data.get("backup_id")
|
||||
)
|
||||
|
||||
|
||||
@router.post("/update/{update_id}/rollback")
|
||||
async def rollback_update(
|
||||
update_id: str,
|
||||
request: RollbackRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Rollback a failed update"""
|
||||
service = UpdateService(db)
|
||||
return await service.rollback(update_id, request.reason)
|
||||
|
||||
|
||||
# Backup Endpoints
|
||||
@router.get("/backups", response_model=Dict[str, Any])
|
||||
async def list_backups(
|
||||
limit: int = Query(default=50, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
backup_type: Optional[str] = Query(default=None, description="Filter by backup type"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""List available backups with storage information"""
|
||||
service = BackupService(db)
|
||||
backup_data = await service.list_backups(limit=limit, offset=offset, backup_type=backup_type)
|
||||
|
||||
# Add storage information
|
||||
backup_dir = service.BACKUP_DIR
|
||||
try:
|
||||
# Create backup directory if it doesn't exist
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
disk_usage = shutil.disk_usage(backup_dir)
|
||||
storage = {
|
||||
"used": backup_data.get("storage_used", 0), # From service
|
||||
"total": disk_usage.total,
|
||||
"available": disk_usage.free
|
||||
}
|
||||
except Exception as e:
|
||||
logger.debug("backup_dir_unavailable", error=str(e))
|
||||
storage = {"used": 0, "total": 0, "available": 0}
|
||||
|
||||
backup_data["storage"] = storage
|
||||
return backup_data
|
||||
|
||||
|
||||
@router.post("/backups", response_model=BackupResponse)
|
||||
async def create_backup(
|
||||
request: CreateBackupRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Create a new system backup"""
|
||||
service = BackupService(db)
|
||||
backup_data = await service.create_backup(
|
||||
backup_type=request.backup_type,
|
||||
description=request.description,
|
||||
created_by=current_user.email
|
||||
)
|
||||
|
||||
return BackupResponse(
|
||||
id=backup_data["id"],
|
||||
uuid=backup_data["uuid"],
|
||||
backup_type=backup_data["backup_type"],
|
||||
created_at=backup_data["created_at"],
|
||||
size_mb=backup_data.get("size_mb"),
|
||||
size=backup_data.get("size"),
|
||||
version=backup_data.get("version"),
|
||||
description=backup_data.get("description"),
|
||||
is_valid=backup_data["is_valid"],
|
||||
download_url=backup_data.get("download_url")
|
||||
)
|
||||
|
||||
|
||||
@router.get("/backups/{backup_id}", response_model=BackupResponse)
|
||||
async def get_backup(
|
||||
backup_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Get details of a specific backup"""
|
||||
service = BackupService(db)
|
||||
backup_data = await service.get_backup(backup_id)
|
||||
|
||||
return BackupResponse(
|
||||
id=backup_data["id"],
|
||||
uuid=backup_data["uuid"],
|
||||
backup_type=backup_data["backup_type"],
|
||||
created_at=backup_data["created_at"],
|
||||
size_mb=backup_data.get("size_mb"),
|
||||
size=backup_data.get("size"),
|
||||
version=backup_data.get("version"),
|
||||
description=backup_data.get("description"),
|
||||
is_valid=backup_data["is_valid"],
|
||||
download_url=backup_data.get("download_url")
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/backups/{backup_id}")
|
||||
async def delete_backup(
|
||||
backup_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Delete a backup"""
|
||||
service = BackupService(db)
|
||||
return await service.delete_backup(backup_id)
|
||||
|
||||
|
||||
@router.post("/restore")
|
||||
async def restore_backup(
|
||||
request: RestoreBackupRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Restore system from a backup"""
|
||||
service = BackupService(db)
|
||||
return await service.restore_backup(
|
||||
backup_id=request.backup_id,
|
||||
components=request.components
|
||||
)
|
||||
133
apps/control-panel-backend/app/api/v1/templates.py
Normal file
133
apps/control-panel-backend/app/api/v1/templates.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
GT 2.0 Tenant Templates API
|
||||
Manage and apply tenant configuration templates
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, delete
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.tenant_template import TenantTemplate
|
||||
from app.services.template_service import TemplateService
|
||||
|
||||
router = APIRouter(prefix="/api/v1/templates", tags=["templates"])
|
||||
|
||||
|
||||
class CreateTemplateRequest(BaseModel):
|
||||
tenant_id: int
|
||||
name: str
|
||||
description: str = ""
|
||||
|
||||
|
||||
class ApplyTemplateRequest(BaseModel):
|
||||
template_id: int
|
||||
tenant_id: int
|
||||
|
||||
|
||||
class TemplateResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
description: str
|
||||
is_default: bool
|
||||
resource_counts: dict
|
||||
created_at: str
|
||||
|
||||
|
||||
@router.get("/", response_model=List[TemplateResponse])
|
||||
async def list_templates(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""List all tenant templates"""
|
||||
result = await db.execute(select(TenantTemplate).order_by(TenantTemplate.name))
|
||||
templates = result.scalars().all()
|
||||
|
||||
return [TemplateResponse(**template.get_summary()) for template in templates]
|
||||
|
||||
|
||||
@router.get("/{template_id}")
|
||||
async def get_template(
|
||||
template_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get template details including full configuration"""
|
||||
template = await db.get(TenantTemplate, template_id)
|
||||
|
||||
if not template:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
|
||||
return template.to_dict()
|
||||
|
||||
|
||||
@router.post("/export")
|
||||
async def export_template(
|
||||
request: CreateTemplateRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Export existing tenant configuration as a new template"""
|
||||
try:
|
||||
service = TemplateService()
|
||||
template = await service.export_tenant_as_template(
|
||||
tenant_id=request.tenant_id,
|
||||
template_name=request.name,
|
||||
template_description=request.description,
|
||||
control_panel_db=db
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Template '{request.name}' created successfully",
|
||||
"template": template.get_summary()
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to export template: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/apply")
|
||||
async def apply_template(
|
||||
request: ApplyTemplateRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Apply a template to an existing tenant"""
|
||||
try:
|
||||
service = TemplateService()
|
||||
results = await service.apply_template(
|
||||
template_id=request.template_id,
|
||||
tenant_id=request.tenant_id,
|
||||
control_panel_db=db
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Template applied successfully",
|
||||
"results": results
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to apply template: {str(e)}")
|
||||
|
||||
|
||||
@router.delete("/{template_id}")
|
||||
async def delete_template(
|
||||
template_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Delete a template"""
|
||||
template = await db.get(TenantTemplate, template_id)
|
||||
|
||||
if not template:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
|
||||
await db.delete(template)
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Template '{template.name}' deleted successfully"
|
||||
}
|
||||
362
apps/control-panel-backend/app/api/v1/tenant_models.py
Normal file
362
apps/control-panel-backend/app/api/v1/tenant_models.py
Normal file
@@ -0,0 +1,362 @@
|
||||
"""
|
||||
Tenant Model Management API for GT 2.0 Admin Control Panel
|
||||
|
||||
Provides endpoints for managing which models are available to which tenants,
|
||||
with tenant-specific permissions and rate limits.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel, Field
|
||||
import logging
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.model_management_service import get_model_management_service
|
||||
from app.models.tenant_model_config import TenantModelConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/tenants", tags=["Tenant Model Management"])
|
||||
|
||||
|
||||
# Request/Response Models
|
||||
class TenantModelAssignRequest(BaseModel):
|
||||
model_id: str = Field(..., description="Model ID to assign")
|
||||
rate_limits: Optional[Dict[str, Any]] = Field(None, description="Custom rate limits")
|
||||
capabilities: Optional[Dict[str, Any]] = Field(None, description="Tenant-specific capabilities")
|
||||
usage_constraints: Optional[Dict[str, Any]] = Field(None, description="Usage restrictions")
|
||||
priority: int = Field(1, ge=1, le=10, description="Priority level (1-10)")
|
||||
|
||||
model_config = {"protected_namespaces": ()}
|
||||
|
||||
|
||||
class TenantModelUpdateRequest(BaseModel):
|
||||
is_enabled: Optional[bool] = Field(None, description="Enable/disable model for tenant")
|
||||
rate_limits: Optional[Dict[str, Any]] = Field(None, description="Updated rate limits")
|
||||
tenant_capabilities: Optional[Dict[str, Any]] = Field(None, description="Updated capabilities")
|
||||
usage_constraints: Optional[Dict[str, Any]] = Field(None, description="Updated usage restrictions")
|
||||
priority: Optional[int] = Field(None, ge=1, le=10, description="Updated priority level")
|
||||
|
||||
|
||||
class ModelAccessCheckRequest(BaseModel):
|
||||
user_capabilities: Optional[List[str]] = Field(None, description="User capabilities")
|
||||
user_id: Optional[str] = Field(None, description="User identifier")
|
||||
|
||||
|
||||
class TenantModelResponse(BaseModel):
|
||||
id: int
|
||||
tenant_id: int
|
||||
model_id: str
|
||||
is_enabled: bool
|
||||
tenant_capabilities: Dict[str, Any]
|
||||
rate_limits: Dict[str, Any]
|
||||
usage_constraints: Dict[str, Any]
|
||||
priority: int
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
|
||||
class ModelWithTenantConfigResponse(BaseModel):
|
||||
model_id: str
|
||||
name: str
|
||||
provider: str
|
||||
model_type: str
|
||||
endpoint: str
|
||||
tenant_config: TenantModelResponse
|
||||
|
||||
|
||||
@router.post("/{tenant_id}/models", response_model=TenantModelResponse)
|
||||
async def assign_model_to_tenant(
|
||||
tenant_id: int,
|
||||
request: TenantModelAssignRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Assign a model to a tenant with specific configuration"""
|
||||
try:
|
||||
service = get_model_management_service(db)
|
||||
|
||||
tenant_model_config = await service.assign_model_to_tenant(
|
||||
tenant_id=tenant_id,
|
||||
model_id=request.model_id,
|
||||
rate_limits=request.rate_limits,
|
||||
capabilities=request.capabilities,
|
||||
usage_constraints=request.usage_constraints,
|
||||
priority=request.priority
|
||||
)
|
||||
|
||||
return TenantModelResponse(**tenant_model_config.to_dict())
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Error assigning model to tenant: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{tenant_id}/models/{model_id:path}")
|
||||
async def remove_model_from_tenant(
|
||||
tenant_id: int,
|
||||
model_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Remove model access from a tenant"""
|
||||
try:
|
||||
service = get_model_management_service(db)
|
||||
|
||||
success = await service.remove_model_from_tenant(tenant_id, model_id)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Model assignment not found")
|
||||
|
||||
return {"message": f"Model {model_id} removed from tenant {tenant_id}"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing model from tenant: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.patch("/{tenant_id}/models/{model_id:path}", response_model=TenantModelResponse)
|
||||
async def update_tenant_model_config(
|
||||
tenant_id: int,
|
||||
model_id: str,
|
||||
request: TenantModelUpdateRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Update tenant-specific model configuration"""
|
||||
try:
|
||||
service = get_model_management_service(db)
|
||||
|
||||
# Convert request to dict, excluding None values
|
||||
updates = {k: v for k, v in request.dict().items() if v is not None}
|
||||
|
||||
tenant_model_config = await service.update_tenant_model_config(
|
||||
tenant_id=tenant_id,
|
||||
model_id=model_id,
|
||||
updates=updates
|
||||
)
|
||||
|
||||
if not tenant_model_config:
|
||||
raise HTTPException(status_code=404, detail="Tenant model configuration not found")
|
||||
|
||||
return TenantModelResponse(**tenant_model_config.to_dict())
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating tenant model config: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{tenant_id}/models", response_model=List[ModelWithTenantConfigResponse])
|
||||
async def get_tenant_models(
|
||||
tenant_id: int,
|
||||
enabled_only: bool = Query(False, description="Only return enabled models"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get all models available to a tenant"""
|
||||
try:
|
||||
service = get_model_management_service(db)
|
||||
|
||||
models = await service.get_tenant_models(
|
||||
tenant_id=tenant_id,
|
||||
enabled_only=enabled_only
|
||||
)
|
||||
|
||||
# Format response
|
||||
response_models = []
|
||||
for model in models:
|
||||
tenant_config = model.pop("tenant_config")
|
||||
response_models.append({
|
||||
**model,
|
||||
"tenant_config": TenantModelResponse(**tenant_config)
|
||||
})
|
||||
|
||||
return response_models
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting tenant models: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{tenant_id}/models/{model_id}/check-access")
|
||||
async def check_tenant_model_access(
|
||||
tenant_id: int,
|
||||
model_id: str,
|
||||
request: ModelAccessCheckRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Check if a tenant/user can access a specific model"""
|
||||
try:
|
||||
service = get_model_management_service(db)
|
||||
|
||||
access_info = await service.check_tenant_model_access(
|
||||
tenant_id=tenant_id,
|
||||
model_id=model_id,
|
||||
user_capabilities=request.user_capabilities,
|
||||
user_id=request.user_id
|
||||
)
|
||||
|
||||
return access_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking tenant model access: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{tenant_id}/models/stats")
|
||||
async def get_tenant_model_stats(
|
||||
tenant_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get statistics about models for a tenant"""
|
||||
try:
|
||||
service = get_model_management_service(db)
|
||||
|
||||
stats = await service.get_tenant_model_stats(tenant_id)
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting tenant model stats: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# Additional endpoints for model-centric views
|
||||
@router.get("/models/{model_id:path}/tenants")
|
||||
async def get_model_tenants(
|
||||
model_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get all tenants that have access to a model"""
|
||||
try:
|
||||
service = get_model_management_service(db)
|
||||
|
||||
tenants = await service.get_model_tenants(model_id)
|
||||
|
||||
return {
|
||||
"model_id": model_id,
|
||||
"tenants": tenants,
|
||||
"total_tenants": len(tenants)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting model tenants: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# Global tenant model configuration endpoints
|
||||
@router.get("/all")
|
||||
async def get_all_tenant_model_configs(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get all tenant model configurations with joined tenant and model data"""
|
||||
try:
|
||||
service = get_model_management_service(db)
|
||||
|
||||
# This would need to be implemented in the service
|
||||
configs = await service.get_all_tenant_model_configs()
|
||||
|
||||
return configs
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting all tenant model configs: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# Bulk operations
|
||||
@router.post("/{tenant_id}/models/bulk-assign")
|
||||
async def bulk_assign_models_to_tenant(
|
||||
tenant_id: int,
|
||||
model_ids: List[str],
|
||||
default_config: Optional[TenantModelAssignRequest] = None,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Assign multiple models to a tenant with the same configuration"""
|
||||
try:
|
||||
service = get_model_management_service(db)
|
||||
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
for model_id in model_ids:
|
||||
try:
|
||||
config = default_config if default_config else TenantModelAssignRequest(model_id=model_id)
|
||||
|
||||
tenant_model_config = await service.assign_model_to_tenant(
|
||||
tenant_id=tenant_id,
|
||||
model_id=model_id,
|
||||
rate_limits=config.rate_limits,
|
||||
capabilities=config.capabilities,
|
||||
usage_constraints=config.usage_constraints,
|
||||
priority=config.priority
|
||||
)
|
||||
|
||||
results.append({
|
||||
"model_id": model_id,
|
||||
"status": "success",
|
||||
"config": tenant_model_config.to_dict()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
errors.append({
|
||||
"model_id": model_id,
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
return {
|
||||
"tenant_id": tenant_id,
|
||||
"total_requested": len(model_ids),
|
||||
"successful": len(results),
|
||||
"failed": len(errors),
|
||||
"results": results,
|
||||
"errors": errors
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error bulk assigning models: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{tenant_id}/models/bulk-remove")
|
||||
async def bulk_remove_models_from_tenant(
|
||||
tenant_id: int,
|
||||
model_ids: List[str],
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Remove multiple models from a tenant"""
|
||||
try:
|
||||
service = get_model_management_service(db)
|
||||
|
||||
results = []
|
||||
|
||||
for model_id in model_ids:
|
||||
try:
|
||||
success = await service.remove_model_from_tenant(tenant_id, model_id)
|
||||
results.append({
|
||||
"model_id": model_id,
|
||||
"status": "success" if success else "not_found",
|
||||
"removed": success
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
results.append({
|
||||
"model_id": model_id,
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
successful = sum(1 for r in results if r["status"] == "success")
|
||||
|
||||
return {
|
||||
"tenant_id": tenant_id,
|
||||
"total_requested": len(model_ids),
|
||||
"successful": successful,
|
||||
"results": results
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error bulk removing models: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
Reference in New Issue
Block a user