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:
662
apps/control-panel-backend/app/api/tenants.py
Normal file
662
apps/control-panel-backend/app/api/tenants.py
Normal file
@@ -0,0 +1,662 @@
|
||||
"""
|
||||
Tenant management API endpoints
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, or_
|
||||
from pydantic import BaseModel, Field, validator
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.auth import JWTHandler, get_current_user
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.user import User
|
||||
from app.services.model_management_service import get_model_management_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/tenants", tags=["tenants"])
|
||||
|
||||
|
||||
# Pydantic models
|
||||
class TenantCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
domain: str = Field(..., min_length=1, max_length=50)
|
||||
template: str = Field(default="standard")
|
||||
max_users: int = Field(default=100, ge=1, le=10000)
|
||||
resource_limits: Optional[Dict[str, Any]] = Field(default_factory=dict)
|
||||
frontend_url: Optional[str] = Field(None, max_length=255, description="Frontend URL for password reset emails (e.g., https://app.company.com)")
|
||||
|
||||
@validator('domain')
|
||||
def validate_domain(cls, v):
|
||||
# Only allow alphanumeric and hyphens
|
||||
import re
|
||||
if not re.match(r'^[a-z0-9-]+$', v):
|
||||
raise ValueError('Domain must contain only lowercase letters, numbers, and hyphens')
|
||||
return v
|
||||
|
||||
@validator('frontend_url')
|
||||
def validate_frontend_url(cls, v):
|
||||
if v is not None and v.strip():
|
||||
import re
|
||||
# Basic URL validation
|
||||
if not re.match(r'^https?://.+', v):
|
||||
raise ValueError('Frontend URL must start with http:// or https://')
|
||||
return v
|
||||
|
||||
|
||||
class TenantUpdate(BaseModel):
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
max_users: Optional[int] = Field(None, ge=1, le=10000)
|
||||
resource_limits: Optional[Dict[str, Any]] = None
|
||||
status: Optional[str] = Field(None, pattern="^(active|suspended|pending|archived)$")
|
||||
frontend_url: Optional[str] = Field(None, max_length=255, description="Frontend URL for password reset emails")
|
||||
|
||||
# Budget configuration
|
||||
monthly_budget_cents: Optional[int] = Field(None, description="Monthly budget in cents (NULL = unlimited)")
|
||||
budget_warning_threshold: Optional[int] = Field(None, ge=1, le=100, description="Warning threshold percentage (1-100)")
|
||||
budget_critical_threshold: Optional[int] = Field(None, ge=1, le=100, description="Critical threshold percentage (1-100)")
|
||||
budget_enforcement_enabled: Optional[bool] = Field(None, description="Enable budget enforcement")
|
||||
|
||||
# Hot tier storage pricing (NULL = use default $0.15/GiB/month)
|
||||
storage_price_dataset_hot: Optional[float] = Field(None, description="Dataset hot storage price per GiB/month")
|
||||
storage_price_conversation_hot: Optional[float] = Field(None, description="Conversation hot storage price per GiB/month")
|
||||
|
||||
# Cold tier: Allocation-based model
|
||||
cold_storage_allocated_tibs: Optional[float] = Field(None, description="Cold storage allocation in TiBs")
|
||||
cold_storage_price_per_tib: Optional[float] = Field(None, description="Cold storage price per TiB/month (default: $10)")
|
||||
|
||||
@validator('frontend_url')
|
||||
def validate_frontend_url(cls, v):
|
||||
if v is not None and v.strip():
|
||||
import re
|
||||
if not re.match(r'^https?://.+', v):
|
||||
raise ValueError('Frontend URL must start with http:// or https://')
|
||||
return v
|
||||
|
||||
|
||||
class TenantResponse(BaseModel):
|
||||
id: int
|
||||
uuid: str
|
||||
name: str
|
||||
domain: str
|
||||
template: str
|
||||
status: str
|
||||
max_users: int
|
||||
resource_limits: Dict[str, Any]
|
||||
namespace: str
|
||||
frontend_url: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
user_count: Optional[int] = 0
|
||||
|
||||
# Budget configuration
|
||||
monthly_budget_cents: Optional[int] = None
|
||||
budget_warning_threshold: Optional[int] = None
|
||||
budget_critical_threshold: Optional[int] = None
|
||||
budget_enforcement_enabled: Optional[bool] = None
|
||||
|
||||
# Hot tier storage pricing
|
||||
storage_price_dataset_hot: Optional[float] = None
|
||||
storage_price_conversation_hot: Optional[float] = None
|
||||
|
||||
# Cold tier allocation
|
||||
cold_storage_allocated_tibs: Optional[float] = None
|
||||
cold_storage_price_per_tib: Optional[float] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TenantListResponse(BaseModel):
|
||||
tenants: List[TenantResponse]
|
||||
total: int
|
||||
page: int
|
||||
limit: int
|
||||
|
||||
|
||||
@router.get("/", response_model=TenantListResponse)
|
||||
async def list_tenants(
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
search: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""List all tenants with pagination and filtering"""
|
||||
try:
|
||||
# Require super_admin only
|
||||
if current_user.user_type != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions"
|
||||
)
|
||||
|
||||
# Build query
|
||||
query = select(Tenant)
|
||||
|
||||
# Apply filters
|
||||
if search:
|
||||
query = query.where(
|
||||
or_(
|
||||
Tenant.name.ilike(f"%{search}%"),
|
||||
Tenant.domain.ilike(f"%{search}%")
|
||||
)
|
||||
)
|
||||
|
||||
if status:
|
||||
query = query.where(Tenant.status == status)
|
||||
|
||||
# Get total count
|
||||
count_query = select(func.count()).select_from(Tenant)
|
||||
if search:
|
||||
count_query = count_query.where(
|
||||
or_(
|
||||
Tenant.name.ilike(f"%{search}%"),
|
||||
Tenant.domain.ilike(f"%{search}%")
|
||||
)
|
||||
)
|
||||
if status:
|
||||
count_query = count_query.where(Tenant.status == status)
|
||||
|
||||
total_result = await db.execute(count_query)
|
||||
total = total_result.scalar() or 0
|
||||
|
||||
# Apply pagination
|
||||
offset = (page - 1) * limit
|
||||
query = query.offset(offset).limit(limit).order_by(Tenant.created_at.desc())
|
||||
|
||||
# Execute query
|
||||
result = await db.execute(query)
|
||||
tenants = result.scalars().all()
|
||||
|
||||
# Get user counts for each tenant
|
||||
tenant_responses = []
|
||||
for tenant in tenants:
|
||||
user_count_query = select(func.count()).select_from(User).where(User.tenant_id == tenant.id)
|
||||
user_count_result = await db.execute(user_count_query)
|
||||
user_count = user_count_result.scalar() or 0
|
||||
|
||||
tenant_dict = {
|
||||
"id": tenant.id,
|
||||
"uuid": tenant.uuid,
|
||||
"name": tenant.name,
|
||||
"domain": tenant.domain,
|
||||
"template": tenant.template,
|
||||
"status": tenant.status,
|
||||
"max_users": tenant.max_users,
|
||||
"resource_limits": tenant.resource_limits or {},
|
||||
"namespace": tenant.namespace,
|
||||
"frontend_url": tenant.frontend_url,
|
||||
"created_at": tenant.created_at,
|
||||
"updated_at": tenant.updated_at,
|
||||
"user_count": user_count,
|
||||
# Budget configuration
|
||||
"monthly_budget_cents": tenant.monthly_budget_cents,
|
||||
"budget_warning_threshold": tenant.budget_warning_threshold,
|
||||
"budget_critical_threshold": tenant.budget_critical_threshold,
|
||||
"budget_enforcement_enabled": tenant.budget_enforcement_enabled,
|
||||
# Hot tier storage pricing
|
||||
"storage_price_dataset_hot": float(tenant.storage_price_dataset_hot) if tenant.storage_price_dataset_hot else None,
|
||||
"storage_price_conversation_hot": float(tenant.storage_price_conversation_hot) if tenant.storage_price_conversation_hot else None,
|
||||
# Cold tier allocation
|
||||
"cold_storage_allocated_tibs": float(tenant.cold_storage_allocated_tibs) if tenant.cold_storage_allocated_tibs else None,
|
||||
"cold_storage_price_per_tib": float(tenant.cold_storage_price_per_tib) if tenant.cold_storage_price_per_tib else 10.00,
|
||||
}
|
||||
tenant_responses.append(TenantResponse(**tenant_dict))
|
||||
|
||||
return TenantListResponse(
|
||||
tenants=tenant_responses,
|
||||
total=total,
|
||||
page=page,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing tenants: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to list tenants"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{tenant_id}", response_model=TenantResponse)
|
||||
async def get_tenant(
|
||||
tenant_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get a specific tenant by ID"""
|
||||
try:
|
||||
# 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"
|
||||
)
|
||||
|
||||
# Get tenant
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.id == tenant_id)
|
||||
)
|
||||
tenant = result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
# Get user count
|
||||
user_count_query = select(func.count()).select_from(User).where(User.tenant_id == tenant.id)
|
||||
user_count_result = await db.execute(user_count_query)
|
||||
user_count = user_count_result.scalar() or 0
|
||||
|
||||
return TenantResponse(
|
||||
id=tenant.id,
|
||||
uuid=tenant.uuid,
|
||||
name=tenant.name,
|
||||
domain=tenant.domain,
|
||||
template=tenant.template,
|
||||
status=tenant.status,
|
||||
max_users=tenant.max_users,
|
||||
resource_limits=tenant.resource_limits or {},
|
||||
namespace=tenant.namespace,
|
||||
created_at=tenant.created_at,
|
||||
updated_at=tenant.updated_at,
|
||||
user_count=user_count,
|
||||
# Budget configuration
|
||||
monthly_budget_cents=tenant.monthly_budget_cents,
|
||||
budget_warning_threshold=tenant.budget_warning_threshold,
|
||||
budget_critical_threshold=tenant.budget_critical_threshold,
|
||||
budget_enforcement_enabled=tenant.budget_enforcement_enabled,
|
||||
# Hot tier storage pricing
|
||||
storage_price_dataset_hot=float(tenant.storage_price_dataset_hot) if tenant.storage_price_dataset_hot else None,
|
||||
storage_price_conversation_hot=float(tenant.storage_price_conversation_hot) if tenant.storage_price_conversation_hot else None,
|
||||
# Cold tier allocation
|
||||
cold_storage_allocated_tibs=float(tenant.cold_storage_allocated_tibs) if tenant.cold_storage_allocated_tibs else None,
|
||||
cold_storage_price_per_tib=float(tenant.cold_storage_price_per_tib) if tenant.cold_storage_price_per_tib else 10.00,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting tenant {tenant_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get tenant"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/", response_model=TenantResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_tenant(
|
||||
tenant_data: TenantCreate,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Create a new tenant"""
|
||||
try:
|
||||
# Require super_admin only
|
||||
if current_user.user_type != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions"
|
||||
)
|
||||
|
||||
# Check if domain already exists
|
||||
existing = await db.execute(
|
||||
select(Tenant).where(Tenant.domain == tenant_data.domain)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Domain already exists"
|
||||
)
|
||||
|
||||
# Create tenant
|
||||
tenant = Tenant(
|
||||
uuid=str(uuid.uuid4()),
|
||||
name=tenant_data.name,
|
||||
domain=tenant_data.domain,
|
||||
template=tenant_data.template,
|
||||
status="pending",
|
||||
max_users=tenant_data.max_users,
|
||||
resource_limits=tenant_data.resource_limits or {},
|
||||
namespace=f"gt-{tenant_data.domain}",
|
||||
subdomain=tenant_data.domain # Set subdomain to match domain
|
||||
)
|
||||
|
||||
db.add(tenant)
|
||||
await db.commit()
|
||||
await db.refresh(tenant)
|
||||
|
||||
# Auto-assign all active models to this new tenant
|
||||
model_service = get_model_management_service(db)
|
||||
assigned_count = await model_service.auto_assign_all_models_to_tenant(tenant.id)
|
||||
logger.info(f"Auto-assigned {assigned_count} models to new tenant {tenant.domain}")
|
||||
|
||||
# Add background task to deploy tenant infrastructure
|
||||
from app.services.tenant_provisioning import deploy_tenant_infrastructure
|
||||
background_tasks.add_task(deploy_tenant_infrastructure, tenant.id)
|
||||
|
||||
return TenantResponse(
|
||||
id=tenant.id,
|
||||
uuid=tenant.uuid,
|
||||
name=tenant.name,
|
||||
domain=tenant.domain,
|
||||
template=tenant.template,
|
||||
status=tenant.status,
|
||||
max_users=tenant.max_users,
|
||||
resource_limits=tenant.resource_limits,
|
||||
namespace=tenant.namespace,
|
||||
created_at=tenant.created_at,
|
||||
updated_at=tenant.updated_at,
|
||||
user_count=0
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating tenant: {str(e)}")
|
||||
await db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to create tenant"
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{tenant_id}", response_model=TenantResponse)
|
||||
async def update_tenant(
|
||||
tenant_id: int,
|
||||
tenant_update: TenantUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Update a tenant"""
|
||||
try:
|
||||
# Require super_admin only
|
||||
if current_user.user_type != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions"
|
||||
)
|
||||
|
||||
# Get tenant
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.id == tenant_id)
|
||||
)
|
||||
tenant = result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
# Update fields
|
||||
update_data = tenant_update.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(tenant, field, value)
|
||||
|
||||
tenant.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(tenant)
|
||||
|
||||
# Get user count
|
||||
user_count_query = select(func.count()).select_from(User).where(User.tenant_id == tenant.id)
|
||||
user_count_result = await db.execute(user_count_query)
|
||||
user_count = user_count_result.scalar() or 0
|
||||
|
||||
return TenantResponse(
|
||||
id=tenant.id,
|
||||
uuid=tenant.uuid,
|
||||
name=tenant.name,
|
||||
domain=tenant.domain,
|
||||
template=tenant.template,
|
||||
status=tenant.status,
|
||||
max_users=tenant.max_users,
|
||||
resource_limits=tenant.resource_limits,
|
||||
namespace=tenant.namespace,
|
||||
created_at=tenant.created_at,
|
||||
updated_at=tenant.updated_at,
|
||||
user_count=user_count,
|
||||
# Budget configuration
|
||||
monthly_budget_cents=tenant.monthly_budget_cents,
|
||||
budget_warning_threshold=tenant.budget_warning_threshold,
|
||||
budget_critical_threshold=tenant.budget_critical_threshold,
|
||||
budget_enforcement_enabled=tenant.budget_enforcement_enabled,
|
||||
# Hot tier storage pricing
|
||||
storage_price_dataset_hot=float(tenant.storage_price_dataset_hot) if tenant.storage_price_dataset_hot else None,
|
||||
storage_price_conversation_hot=float(tenant.storage_price_conversation_hot) if tenant.storage_price_conversation_hot else None,
|
||||
# Cold tier allocation
|
||||
cold_storage_allocated_tibs=float(tenant.cold_storage_allocated_tibs) if tenant.cold_storage_allocated_tibs else None,
|
||||
cold_storage_price_per_tib=float(tenant.cold_storage_price_per_tib) if tenant.cold_storage_price_per_tib else 10.00,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating tenant {tenant_id}: {str(e)}")
|
||||
await db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update tenant"
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{tenant_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_tenant(
|
||||
tenant_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Delete (archive) a tenant"""
|
||||
try:
|
||||
# Require super_admin only
|
||||
if current_user.user_type != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only super admins can delete tenants"
|
||||
)
|
||||
|
||||
# Get tenant
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.id == tenant_id)
|
||||
)
|
||||
tenant = result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
# Archive instead of hard delete
|
||||
tenant.status = "archived"
|
||||
tenant.deleted_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting tenant {tenant_id}: {str(e)}")
|
||||
await db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to delete tenant"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{tenant_id}/deploy", status_code=status.HTTP_202_ACCEPTED)
|
||||
async def deploy_tenant(
|
||||
tenant_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Deploy tenant infrastructure"""
|
||||
try:
|
||||
# Require super_admin only
|
||||
if current_user.user_type != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions"
|
||||
)
|
||||
|
||||
# Get tenant
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.id == tenant_id)
|
||||
)
|
||||
tenant = result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
# Update status
|
||||
tenant.status = "deploying"
|
||||
await db.commit()
|
||||
|
||||
# Add background task to deploy infrastructure
|
||||
from app.services.tenant_provisioning import deploy_tenant_infrastructure
|
||||
background_tasks.add_task(deploy_tenant_infrastructure, tenant_id)
|
||||
|
||||
return {"message": "Deployment initiated", "tenant_id": tenant_id}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deploying tenant {tenant_id}: {str(e)}")
|
||||
await db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to deploy tenant"
|
||||
)
|
||||
|
||||
|
||||
# Optics Feature Toggle
|
||||
class OpticsToggleRequest(BaseModel):
|
||||
enabled: bool = Field(..., description="Whether to enable Optics cost tracking")
|
||||
|
||||
|
||||
class OpticsToggleResponse(BaseModel):
|
||||
tenant_id: int
|
||||
domain: str
|
||||
optics_enabled: bool
|
||||
message: str
|
||||
|
||||
|
||||
@router.put("/{tenant_id}/optics", response_model=OpticsToggleResponse)
|
||||
async def toggle_optics(
|
||||
tenant_id: int,
|
||||
request: OpticsToggleRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Toggle Optics cost tracking for a tenant.
|
||||
|
||||
When enabled, the Optics tab will appear in the tenant's observability dashboard
|
||||
showing inference costs and storage costs.
|
||||
"""
|
||||
try:
|
||||
# Require super_admin only
|
||||
if current_user.user_type != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions"
|
||||
)
|
||||
|
||||
# Get tenant
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.id == tenant_id)
|
||||
)
|
||||
tenant = result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
# Update optics_enabled
|
||||
tenant.optics_enabled = request.enabled
|
||||
tenant.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(tenant)
|
||||
|
||||
action = "enabled" if request.enabled else "disabled"
|
||||
logger.info(f"Optics {action} for tenant {tenant.domain} by {current_user.email}")
|
||||
|
||||
return OpticsToggleResponse(
|
||||
tenant_id=tenant.id,
|
||||
domain=tenant.domain,
|
||||
optics_enabled=tenant.optics_enabled,
|
||||
message=f"Optics cost tracking {action} for {tenant.name}"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error toggling optics for tenant {tenant_id}: {str(e)}")
|
||||
await db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to toggle optics setting"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{tenant_id}/optics")
|
||||
async def get_optics_status(
|
||||
tenant_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get current Optics status for a tenant"""
|
||||
try:
|
||||
# Require super_admin only
|
||||
if current_user.user_type != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions"
|
||||
)
|
||||
|
||||
# Get tenant
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.id == tenant_id)
|
||||
)
|
||||
tenant = result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
return {
|
||||
"tenant_id": tenant.id,
|
||||
"domain": tenant.domain,
|
||||
"optics_enabled": tenant.optics_enabled or False
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting optics status for tenant {tenant_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get optics status"
|
||||
)
|
||||
Reference in New Issue
Block a user