feat(17-03): bulk add endpoint and service with credential profile support

- POST /tenants/{tenant_id}/devices/bulk endpoint with rate limiting
- bulk_add_with_profile service validates profile ownership and type compatibility
- Duplicate IP check prevents adding same IP twice in one tenant
- TCP reachability check for RouterOS devices, skipped for SNMP (UDP)
- Per-device result reporting with partial success support
- Device model updated with device_type, snmp_port, snmp_version, snmp_profile_id columns
- Audit logging for bulk add operations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-21 18:59:24 -05:00
parent 998fff7d01
commit a231b18d69
3 changed files with 219 additions and 0 deletions

View File

@@ -31,6 +31,8 @@ from app.middleware.tenant_context import CurrentUser, get_current_user
from app.schemas.device import (
BulkAddRequest,
BulkAddResult,
BulkAddWithProfileRequest,
BulkAddWithProfileResult,
DeviceCreate,
DeviceListResponse,
DeviceResponse,
@@ -394,6 +396,54 @@ async def bulk_add_devices(
return BulkAddResult(added=added, failed=failed)
@router.post(
"/tenants/{tenant_id}/devices/bulk",
response_model=BulkAddWithProfileResult,
summary="Bulk add devices using a credential profile",
dependencies=[Depends(require_operator_or_above), require_scope("devices:write")],
)
@limiter.limit("5/minute")
async def bulk_add_with_profile(
request: Request,
tenant_id: uuid.UUID,
data: BulkAddWithProfileRequest,
current_user: CurrentUser = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> BulkAddWithProfileResult:
"""Add multiple devices using a credential profile.
Supports partial success -- individual devices can fail without blocking others.
Returns per-device results with success/failure reasons.
"""
await _check_tenant_access(current_user, tenant_id, db)
result = await device_service.bulk_add_with_profile(
db=db,
tenant_id=tenant_id,
data=data,
user_id=current_user.user_id,
)
# Audit log the bulk add (fire-and-forget)
try:
await log_action(
db,
tenant_id,
current_user.user_id,
"device_bulk_add",
resource_type="device",
details={
"total": result.total,
"succeeded": result.succeeded,
"failed": result.failed,
"device_type": data.device_type,
"credential_profile_id": str(data.credential_profile_id),
},
ip_address=request.client.host if request.client else None,
)
except Exception:
pass
return result
# ---------------------------------------------------------------------------
# Group assignment
# ---------------------------------------------------------------------------