You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
122 lines
4.5 KiB
122 lines
4.5 KiB
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import UTC, datetime
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.exceptions import ConflictError, NotFoundError, ValidationError
|
|
from app.core.ids import new_id
|
|
from app.models.entities import Backend
|
|
from app.repositories.backend_repository import BackendRepository
|
|
from app.services.storage_service import StorageService
|
|
|
|
|
|
class BackendNotFoundError(NotFoundError):
|
|
code = "backend_not_found"
|
|
message = "Backend does not exist."
|
|
|
|
|
|
class BackendConfigError(ValidationError):
|
|
code = "backend_config_invalid"
|
|
message = "Backend configuration is invalid."
|
|
|
|
|
|
SUPPORTED_BACKEND_TYPES = {"local", "s3"}
|
|
SUPPORTED_STABILITY_CLASSES = {"local", "stable", "opportunistic"}
|
|
|
|
|
|
class BackendService:
|
|
def __init__(self, session: AsyncSession) -> None:
|
|
self.session = session
|
|
self.repository = BackendRepository(session)
|
|
self.storage_service = StorageService(session)
|
|
|
|
async def list_backends(self) -> list[Backend]:
|
|
return await self.repository.list_all()
|
|
|
|
async def create_backend(
|
|
self,
|
|
*,
|
|
name: str,
|
|
backend_type: str,
|
|
stability_class: str,
|
|
read_priority: int,
|
|
write_priority: int,
|
|
config: dict,
|
|
) -> Backend:
|
|
cleaned_name = name.strip()
|
|
if not cleaned_name:
|
|
raise BackendConfigError("Backend name must not be empty.")
|
|
if backend_type not in SUPPORTED_BACKEND_TYPES:
|
|
raise BackendConfigError(f"Unsupported backend type: {backend_type}.")
|
|
if stability_class not in SUPPORTED_STABILITY_CLASSES:
|
|
raise BackendConfigError(f"Unsupported stability class: {stability_class}.")
|
|
if await self.repository.get_by_name(cleaned_name) is not None:
|
|
raise ConflictError("A backend with this name already exists.")
|
|
|
|
normalized_config = self._validate_backend_config(backend_type, config)
|
|
backend = Backend(
|
|
id=new_id("bkd"),
|
|
name=cleaned_name,
|
|
type=backend_type,
|
|
stability_class=stability_class,
|
|
read_priority=read_priority,
|
|
write_priority=write_priority,
|
|
is_enabled=True,
|
|
config_json=json.dumps(normalized_config),
|
|
)
|
|
created = await self.repository.create(backend)
|
|
await self.session.commit()
|
|
return created
|
|
|
|
async def check_backend(self, backend_id: str) -> tuple[Backend, str, str | None]:
|
|
backend = await self._get_backend(backend_id)
|
|
try:
|
|
status, detail = await self.storage_service.check_backend(backend)
|
|
except Exception as exc:
|
|
backend.last_health_status = "unhealthy"
|
|
backend.last_health_checked_at = datetime.now(UTC)
|
|
await self.repository.save(backend)
|
|
await self.session.commit()
|
|
return backend, "unhealthy", str(exc)
|
|
|
|
backend.last_health_status = status
|
|
backend.last_health_checked_at = datetime.now(UTC)
|
|
await self.repository.save(backend)
|
|
await self.session.commit()
|
|
return backend, status, detail
|
|
|
|
async def disable_backend(self, backend_id: str) -> Backend:
|
|
backend = await self._get_backend(backend_id)
|
|
backend.is_enabled = False
|
|
saved = await self.repository.save(backend)
|
|
await self.session.commit()
|
|
return saved
|
|
|
|
async def _get_backend(self, backend_id: str) -> Backend:
|
|
backend = await self.repository.get_by_id(backend_id)
|
|
if backend is None:
|
|
raise BackendNotFoundError()
|
|
return backend
|
|
|
|
@staticmethod
|
|
def _validate_backend_config(backend_type: str, config: dict) -> dict:
|
|
if backend_type == "local":
|
|
base_path = config.get("base_path")
|
|
if not isinstance(base_path, str) or not base_path.strip():
|
|
raise BackendConfigError("Local backend config requires a non-empty base_path.")
|
|
return {"base_path": base_path}
|
|
|
|
if backend_type == "s3":
|
|
bucket = config.get("bucket")
|
|
if not isinstance(bucket, str) or not bucket.strip():
|
|
raise BackendConfigError("S3 backend config requires a non-empty bucket.")
|
|
normalized = {"bucket": bucket}
|
|
for key in ("region", "endpoint_url", "access_key_id", "secret_access_key"):
|
|
value = config.get(key)
|
|
if value is not None:
|
|
normalized[key] = value
|
|
return normalized
|
|
|
|
raise BackendConfigError(f"Unsupported backend type: {backend_type}.")
|
|
|