Add managed storage operational probes
This commit is contained in:
@@ -23,6 +23,7 @@ from govoplan_core.core.modules import (
|
||||
PermissionDefinition,
|
||||
RoleTemplate,
|
||||
)
|
||||
from govoplan_core.core.operations import OperationalCheckProviderRegistration
|
||||
from govoplan_core.core.views import ViewSurface
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_files.backend.change_tracking import register_files_change_tracking
|
||||
@@ -710,6 +711,17 @@ manifest = ModuleManifest(
|
||||
CAPABILITY_FILES_ACCESS: lambda context: __import__("govoplan_files.backend.capabilities", fromlist=["access_capability"]).access_capability(context),
|
||||
"files.campaign_attachments": lambda context: __import__("govoplan_files.backend.capabilities", fromlist=["campaign_capability"]).campaign_capability(context),
|
||||
},
|
||||
operational_check_providers=(
|
||||
OperationalCheckProviderRegistration(
|
||||
module_id="files",
|
||||
check_id="files.managed_storage_roundtrip",
|
||||
provider=lambda: __import__(
|
||||
"govoplan_files.backend.operational_checks",
|
||||
fromlist=["managed_storage_roundtrip_check"],
|
||||
).managed_storage_roundtrip_check(),
|
||||
cache_seconds=60,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
from uuid import uuid4
|
||||
|
||||
from govoplan_core.core.operations import OperationalCheck
|
||||
from govoplan_files.backend.storage.backends import (
|
||||
StorageBackendError,
|
||||
get_storage_backend,
|
||||
)
|
||||
|
||||
|
||||
def managed_storage_roundtrip_check() -> OperationalCheck:
|
||||
"""Exercise the configured managed store without retaining probe data."""
|
||||
|
||||
backend = get_storage_backend()
|
||||
key = f".govoplan-health/probes/{uuid4().hex}.bin"
|
||||
payload = secrets.token_bytes(64)
|
||||
expected_digest = hashlib.sha256(payload).hexdigest()
|
||||
delete_error: Exception | None = None
|
||||
try:
|
||||
backend.put_bytes(key, payload, content_type="application/octet-stream")
|
||||
stored = backend.get_bytes(key)
|
||||
info = backend.stat(key)
|
||||
if info.size_bytes != len(payload):
|
||||
raise StorageBackendError("Managed storage returned an unexpected object size")
|
||||
if hashlib.sha256(stored).hexdigest() != expected_digest:
|
||||
raise StorageBackendError("Managed storage returned different bytes than were written")
|
||||
except Exception as exc: # noqa: BLE001 - operational boundary reports provider failures.
|
||||
return OperationalCheck(
|
||||
id="files.managed_storage_roundtrip",
|
||||
label="Managed file storage",
|
||||
state="error",
|
||||
detail=(
|
||||
"The configured managed file store failed a bounded write/read/stat/delete "
|
||||
f"probe ({type(exc).__name__})."
|
||||
),
|
||||
readiness_critical=True,
|
||||
metrics={"backend": backend.name, "probe_bytes": len(payload)},
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
backend.delete(key)
|
||||
except Exception as exc: # noqa: BLE001 - reported below when the data probe passed.
|
||||
delete_error = exc
|
||||
|
||||
if delete_error is not None:
|
||||
return OperationalCheck(
|
||||
id="files.managed_storage_roundtrip",
|
||||
label="Managed file storage",
|
||||
state="error",
|
||||
detail=(
|
||||
"Managed file bytes round-tripped, but probe cleanup failed "
|
||||
f"({type(delete_error).__name__})."
|
||||
),
|
||||
readiness_critical=True,
|
||||
metrics={"backend": backend.name, "probe_bytes": len(payload)},
|
||||
)
|
||||
return OperationalCheck(
|
||||
id="files.managed_storage_roundtrip",
|
||||
label="Managed file storage",
|
||||
state="ok",
|
||||
detail="The configured managed file store passed write, read, stat, integrity, and delete checks.",
|
||||
metrics={"backend": backend.name, "probe_bytes": len(payload)},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from govoplan_files.backend.operational_checks import managed_storage_roundtrip_check
|
||||
from govoplan_files.backend.storage.backends import LocalFilesystemStorageBackend
|
||||
|
||||
|
||||
def test_managed_storage_roundtrip_removes_probe(tmp_path) -> None:
|
||||
backend = LocalFilesystemStorageBackend(tmp_path)
|
||||
|
||||
with patch(
|
||||
"govoplan_files.backend.operational_checks.get_storage_backend",
|
||||
return_value=backend,
|
||||
):
|
||||
result = managed_storage_roundtrip_check()
|
||||
|
||||
assert result.state == "ok"
|
||||
assert result.metrics["backend"] == "local"
|
||||
assert list(tmp_path.rglob("*.bin")) == []
|
||||
|
||||
|
||||
def test_managed_storage_roundtrip_fails_closed() -> None:
|
||||
class BrokenBackend:
|
||||
name = "broken"
|
||||
|
||||
def put_bytes(self, *_args, **_kwargs):
|
||||
raise OSError("private provider detail")
|
||||
|
||||
def delete(self, *_args, **_kwargs):
|
||||
return None
|
||||
|
||||
with patch(
|
||||
"govoplan_files.backend.operational_checks.get_storage_backend",
|
||||
return_value=BrokenBackend(),
|
||||
):
|
||||
result = managed_storage_roundtrip_check()
|
||||
|
||||
assert result.state == "error"
|
||||
assert result.readiness_critical is True
|
||||
assert "private provider detail" not in result.detail
|
||||
|
||||
Reference in New Issue
Block a user