Skip to content

Commit 17a67e1

Browse files
committed
use simplyblock v2 API for communication
1 parent 0f88ebf commit 17a67e1

File tree

3 files changed

+124
-146
lines changed

3 files changed

+124
-146
lines changed

src/api/resources.py

Lines changed: 14 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
from ..deployment import (
1515
get_db_vmi_identity,
1616
kube_service,
17-
load_simplyblock_credentials,
1817
resolve_database_volume_identifiers,
1918
resolve_storage_volume_identifiers,
2019
)
2120
from ..deployment.kubernetes._util import custom_api_client
21+
from ..deployment.simplyblock_api import create_simplyblock_api
2222
from ..models._util import Identifier
2323
from ..models.branch import Branch, BranchServiceStatus, ResourceUsageDefinition
2424
from ..models.project import Project
@@ -60,8 +60,6 @@
6060

6161
router = APIRouter(tags=["resource"])
6262

63-
SIMPLYBLOCK_API_TIMEOUT_SECONDS = 10.0
64-
6563

6664
# ---------------------------
6765
# Helper functions
@@ -83,46 +81,6 @@
8381
logger = logging.getLogger(__name__)
8482

8583

86-
def _require_int_stat(stats: dict[str, Any], field: str) -> int:
87-
if field not in stats:
88-
raise ValueError(f"Simplyblock IO stats missing required field {field!r}")
89-
value = stats[field]
90-
try:
91-
return int(value)
92-
except (TypeError, ValueError) as exc:
93-
raise ValueError(f"Simplyblock IO stat {field!r} with value {value!r} is not an integer") from exc
94-
95-
96-
async def _fetch_volume_stats(
97-
*,
98-
client: httpx.AsyncClient,
99-
endpoint: str,
100-
cluster_id: str,
101-
cluster_secret: str,
102-
volume_uuid: str,
103-
required_fields: tuple[str, ...],
104-
) -> dict[str, int]:
105-
url = f"{endpoint}/lvol/iostats/{volume_uuid}"
106-
headers = {
107-
"Authorization": f"{cluster_id} {cluster_secret}",
108-
"Accept": "application/json",
109-
}
110-
111-
response = await client.get(url, headers=headers, timeout=SIMPLYBLOCK_API_TIMEOUT_SECONDS)
112-
response.raise_for_status()
113-
114-
payload = response.json()
115-
116-
stats = payload.get("stats")
117-
if not isinstance(stats, list) or not stats:
118-
raise ValueError(f"Simplyblock IO stats payload missing stats list for volume {volume_uuid}")
119-
entry = stats[0]
120-
if not isinstance(entry, dict):
121-
raise ValueError(f"Simplyblock IO stats entry malformed for volume {volume_uuid}")
122-
123-
return {field: _require_int_stat(entry, field) for field in required_fields}
124-
125-
12684
# ---------------------------
12785
# Provisioning endpoints
12886
# ---------------------------
@@ -414,113 +372,41 @@ async def _resolve_volume_stats(
414372
*,
415373
volume_identifier_resolver: Callable[[str], Awaitable[tuple[str, str | None]]],
416374
namespace: str,
417-
branch: Branch,
418-
resource_label: str,
419-
sb_client: httpx.AsyncClient,
420-
endpoint: str,
421-
cluster_id: str,
422-
cluster_secret: str,
423-
required_fields: tuple[str, ...],
424375
) -> dict[str, int]:
425-
volume_uuid, pv_cluster_id = await volume_identifier_resolver(namespace)
426-
427-
if pv_cluster_id and pv_cluster_id != cluster_id:
428-
logger.warning(
429-
"Cluster mismatch for branch %s %s volume %s: PV cluster %s != credentials cluster %s",
430-
branch.id,
431-
resource_label,
432-
volume_uuid,
433-
pv_cluster_id,
434-
cluster_id,
435-
)
436-
raise ValueError(
437-
f"Cluster mismatch for branch {branch.id} {resource_label} volume {volume_uuid}: "
438-
f"PV cluster {pv_cluster_id} != credentials cluster {cluster_id}"
439-
)
376+
volume_uuid, _ = await volume_identifier_resolver(namespace)
440377

441-
return await _fetch_volume_stats(
442-
client=sb_client,
443-
endpoint=endpoint,
444-
cluster_id=cluster_id,
445-
cluster_secret=cluster_secret,
446-
volume_uuid=volume_uuid,
447-
required_fields=required_fields,
448-
)
378+
async with httpx.AsyncClient() as sb_client:
379+
sb_api = await create_simplyblock_api(sb_client)
380+
return await sb_api.volume_iostats(volume_uuid=volume_uuid)
449381

450382

451-
async def _collect_database_volume_usage(
452-
*,
453-
namespace: str,
454-
branch: Branch,
455-
sb_client: httpx.AsyncClient,
456-
endpoint: str,
457-
cluster_id: str,
458-
cluster_secret: str,
459-
) -> tuple[int, int]:
383+
async def _collect_database_volume_usage(namespace: str) -> tuple[int, int]:
460384
stats = await _resolve_volume_stats(
461385
volume_identifier_resolver=resolve_database_volume_identifiers,
462386
namespace=namespace,
463-
branch=branch,
464-
resource_label="database",
465-
sb_client=sb_client,
466-
endpoint=endpoint,
467-
cluster_id=cluster_id,
468-
cluster_secret=cluster_secret,
469-
required_fields=("size_used", "read_io_ps", "write_io_ps"),
470387
)
471388
nvme_bytes = stats["size_used"]
472389
read_iops = stats["read_io_ps"]
473390
write_iops = stats["write_io_ps"]
474391
return nvme_bytes, read_iops + write_iops
475392

476393

477-
async def _collect_storage_volume_usage(
478-
*,
479-
namespace: str,
480-
branch: Branch,
481-
sb_client: httpx.AsyncClient,
482-
endpoint: str,
483-
cluster_id: str,
484-
cluster_secret: str,
485-
) -> int:
394+
async def _collect_storage_volume_usage(namespace: str) -> int:
486395
stats = await _resolve_volume_stats(
487396
volume_identifier_resolver=resolve_storage_volume_identifiers,
488397
namespace=namespace,
489-
branch=branch,
490-
resource_label="storage",
491-
sb_client=sb_client,
492-
endpoint=endpoint,
493-
cluster_id=cluster_id,
494-
cluster_secret=cluster_secret,
495-
required_fields=("size_used",),
496398
)
497399
return stats["size_used"]
498400

499401

500402
async def _collect_branch_volume_usage(branch: Branch, namespace: str) -> tuple[int, int, int | None]:
501-
endpoint, cluster_id, cluster_secret = await load_simplyblock_credentials()
502-
async with httpx.AsyncClient() as sb_client:
503-
db_task = _collect_database_volume_usage(
504-
namespace=namespace,
505-
branch=branch,
506-
sb_client=sb_client,
507-
endpoint=endpoint,
508-
cluster_id=cluster_id,
509-
cluster_secret=cluster_secret,
510-
)
511-
if branch.enable_file_storage:
512-
storage_task = _collect_storage_volume_usage(
513-
namespace=namespace,
514-
branch=branch,
515-
sb_client=sb_client,
516-
endpoint=endpoint,
517-
cluster_id=cluster_id,
518-
cluster_secret=cluster_secret,
519-
)
520-
(nvme_bytes, iops), storage_bytes = await asyncio.gather(db_task, storage_task)
521-
else:
522-
nvme_bytes, iops = await db_task
523-
storage_bytes = None
403+
db_task = _collect_database_volume_usage(namespace)
404+
if branch.enable_file_storage:
405+
storage_task = _collect_storage_volume_usage(namespace)
406+
(nvme_bytes, iops), storage_bytes = await asyncio.gather(db_task, storage_task)
407+
else:
408+
nvme_bytes, iops = await db_task
409+
storage_bytes = None
524410

525411
return nvme_bytes, iops, storage_bytes
526412

src/deployment/__init__.py

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from .kubernetes.kubevirt import get_virtualmachine_status
4545
from .logflare import create_branch_logflare_objects, delete_branch_logflare_objects
4646
from .settings import get_settings
47+
from .simplyblock_api import SIMPLYBLOCK_API_TIMEOUT_SECONDS, create_simplyblock_api
4748

4849
if TYPE_CHECKING:
4950
from cloudflare.types.dns.record_list_params import Name as CloudflareRecordName
@@ -271,32 +272,20 @@ async def resolve_storage_volume_identifiers(namespace: str) -> tuple[str, str |
271272
async def update_branch_volume_iops(branch_id: Identifier, iops: int) -> None:
272273
namespace = deployment_namespace(branch_id)
273274

274-
endpoint, cluster_id, cluster_secret = await load_simplyblock_credentials()
275-
volume_uuid, pv_cluster_id = await resolve_database_volume_identifiers(namespace)
276-
if pv_cluster_id and pv_cluster_id != cluster_id:
277-
raise VelaDeploymentError(
278-
f"Cluster ID mismatch for Simplyblock volume {volume_uuid!r}: PV reports {pv_cluster_id}, "
279-
f"but credentials reference {cluster_id}"
280-
)
281-
url = f"{endpoint}/lvol/{volume_uuid}"
282-
headers = {
283-
"Content-Type": "application/json",
284-
"Authorization": f"{cluster_id} {cluster_secret}",
285-
}
286-
275+
volume_uuid, _ = await resolve_database_volume_identifiers(namespace)
287276
try:
288-
async with httpx.AsyncClient(timeout=10.0) as client:
289-
response = await client.put(url, headers=headers, json={"max-rw-iops": iops})
290-
response.raise_for_status()
277+
async with httpx.AsyncClient(timeout=SIMPLYBLOCK_API_TIMEOUT_SECONDS) as client:
278+
sb_api = await create_simplyblock_api(client)
279+
await sb_api.update_volume(volume_uuid=volume_uuid, payload={"max-rw-iops": iops})
291280
except httpx.HTTPStatusError as exc:
292281
detail = exc.response.text.strip() or exc.response.reason_phrase or str(exc)
293282
raise VelaDeploymentError(
294283
f"Simplyblock volume API rejected IOPS update for volume {volume_uuid!r}: {detail}"
295284
) from exc
296285
except httpx.HTTPError as exc:
297-
raise VelaDeploymentError(f"Failed to reach Simplyblock volume API at {url!r}") from exc
286+
raise VelaDeploymentError("Failed to reach Simplyblock volume API") from exc
298287

299-
logger.info("Updated Simplyblock volume %s IOPS to %s using endpoint %s", volume_uuid, iops, endpoint)
288+
logger.info("Updated Simplyblock volume %s IOPS to %s", volume_uuid, iops)
300289

301290

302291
async def ensure_branch_storage_class(branch_id: Identifier, *, iops: int) -> str:

src/deployment/simplyblock_api.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any
4+
from uuid import UUID
5+
6+
if TYPE_CHECKING:
7+
import httpx
8+
9+
SIMPLYBLOCK_API_TIMEOUT_SECONDS = 10.0
10+
SIMPLYBLOCK_STORAGE_POOL_NAME = "testing1"
11+
12+
13+
class SimplyblockApi:
14+
def __init__(
15+
self,
16+
client: httpx.AsyncClient,
17+
endpoint: str,
18+
cluster_id: str,
19+
cluster_secret: str,
20+
) -> None:
21+
self._client = client
22+
self._endpoint = endpoint.rstrip("/")
23+
self._cluster_id = cluster_id
24+
self._cluster_secret = cluster_secret
25+
self._pool_id_cache: dict[str, UUID] = {}
26+
27+
@property
28+
def _cluster_base(self) -> str:
29+
return f"{self._endpoint}/api/v2/clusters/{self._cluster_id}"
30+
31+
def _headers(self) -> dict[str, str]:
32+
return {
33+
"Authorization": f"Bearer {self._cluster_secret}",
34+
"Accept": "application/json",
35+
}
36+
37+
async def _cluster_pool_base(self) -> str:
38+
pool_id = await self.pool_id()
39+
return f"{self._endpoint}/api/v2/clusters/{self._cluster_id}/storage-pools/{pool_id}"
40+
41+
async def pool(self, name: str = SIMPLYBLOCK_STORAGE_POOL_NAME) -> dict[str, Any]:
42+
url = f"{self._cluster_base}/storage-pools"
43+
response = await self._client.get(url, headers=self._headers(), timeout=SIMPLYBLOCK_API_TIMEOUT_SECONDS)
44+
response.raise_for_status()
45+
46+
pools = response.json()
47+
if isinstance(pools, list):
48+
for pool in pools:
49+
if isinstance(pool, dict) and pool.get("name") == name:
50+
return pool
51+
raise KeyError(f"Storage pool {name!r} not found")
52+
53+
async def pool_id(self, name: str = SIMPLYBLOCK_STORAGE_POOL_NAME) -> UUID:
54+
cached = self._pool_id_cache.get(name)
55+
if cached:
56+
return cached
57+
pool = await self.pool(name)
58+
identifier = UUID(str(pool["id"]))
59+
self._pool_id_cache[name] = identifier
60+
return identifier
61+
62+
async def volume_iostats(self, volume_uuid: str) -> dict[str, Any]:
63+
base_url = await self._cluster_pool_base()
64+
url = f"{base_url}/volumes/{volume_uuid}/iostats"
65+
response = await self._client.get(url, headers=self._headers(), timeout=SIMPLYBLOCK_API_TIMEOUT_SECONDS)
66+
response.raise_for_status()
67+
payload = response.json()
68+
stats = payload.get("stats")
69+
if not isinstance(stats, list) or not stats:
70+
raise ValueError(f"Simplyblock IO stats payload missing stats list for volume {volume_uuid}")
71+
entry = stats[0]
72+
if not isinstance(entry, dict):
73+
raise ValueError(f"Simplyblock IO stats entry malformed for volume {volume_uuid}")
74+
return entry
75+
76+
async def update_volume(
77+
self,
78+
volume_uuid: str,
79+
payload: dict[str, Any],
80+
) -> None:
81+
headers = self._headers()
82+
headers["Content-Type"] = "application/json"
83+
base_url = await self._cluster_pool_base()
84+
url = f"{base_url}/volumes/{volume_uuid}"
85+
response = await self._client.put(
86+
url,
87+
headers=headers,
88+
json=payload,
89+
timeout=SIMPLYBLOCK_API_TIMEOUT_SECONDS,
90+
)
91+
response.raise_for_status()
92+
93+
94+
async def create_simplyblock_api(client: httpx.AsyncClient) -> SimplyblockApi:
95+
from . import load_simplyblock_credentials
96+
97+
endpoint, cluster_id, cluster_secret = await load_simplyblock_credentials()
98+
return SimplyblockApi(
99+
client=client,
100+
endpoint=endpoint,
101+
cluster_id=cluster_id,
102+
cluster_secret=cluster_secret,
103+
)

0 commit comments

Comments
 (0)