waldur-site-agent-cscs-dwdi 0.1.0__py3-none-any.whl → 0.7.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of waldur-site-agent-cscs-dwdi might be problematic. Click here for more details.
- waldur_site_agent_cscs_dwdi/backend.py +307 -4
- waldur_site_agent_cscs_dwdi/client.py +188 -12
- waldur_site_agent_cscs_dwdi-0.7.3.dist-info/METADATA +284 -0
- waldur_site_agent_cscs_dwdi-0.7.3.dist-info/RECORD +7 -0
- waldur_site_agent_cscs_dwdi-0.7.3.dist-info/entry_points.txt +3 -0
- waldur_site_agent_cscs_dwdi-0.1.0.dist-info/METADATA +0 -240
- waldur_site_agent_cscs_dwdi-0.1.0.dist-info/RECORD +0 -7
- waldur_site_agent_cscs_dwdi-0.1.0.dist-info/entry_points.txt +0 -2
- {waldur_site_agent_cscs_dwdi-0.1.0.dist-info → waldur_site_agent_cscs_dwdi-0.7.3.dist-info}/WHEEL +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""CSCS-DWDI backend
|
|
1
|
+
"""CSCS-DWDI backend implementations for compute and storage usage reporting."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from datetime import datetime, timezone
|
|
@@ -13,8 +13,8 @@ from .client import CSCSDWDIClient
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class
|
|
17
|
-
"""Backend for reporting usage from CSCS-DWDI API."""
|
|
16
|
+
class CSCSDWDIComputeBackend(BaseBackend):
|
|
17
|
+
"""Backend for reporting compute usage from CSCS-DWDI API."""
|
|
18
18
|
|
|
19
19
|
def __init__(
|
|
20
20
|
self, backend_settings: dict[str, Any], backend_components: dict[str, dict]
|
|
@@ -26,7 +26,7 @@ class CSCSDWDIBackend(BaseBackend):
|
|
|
26
26
|
backend_components: Component configuration from the offering
|
|
27
27
|
"""
|
|
28
28
|
super().__init__(backend_settings, backend_components)
|
|
29
|
-
self.backend_type = "cscs-dwdi"
|
|
29
|
+
self.backend_type = "cscs-dwdi-compute"
|
|
30
30
|
|
|
31
31
|
# Extract CSCS-DWDI specific configuration
|
|
32
32
|
self.api_url = backend_settings.get("cscs_dwdi_api_url", "")
|
|
@@ -37,6 +37,9 @@ class CSCSDWDIBackend(BaseBackend):
|
|
|
37
37
|
self.oidc_token_url = backend_settings.get("cscs_dwdi_oidc_token_url", "")
|
|
38
38
|
self.oidc_scope = backend_settings.get("cscs_dwdi_oidc_scope")
|
|
39
39
|
|
|
40
|
+
# Optional SOCKS proxy configuration
|
|
41
|
+
self.socks_proxy = backend_settings.get("socks_proxy")
|
|
42
|
+
|
|
40
43
|
if not all([self.api_url, self.client_id, self.client_secret, self.oidc_token_url]):
|
|
41
44
|
msg = (
|
|
42
45
|
"CSCS-DWDI backend requires cscs_dwdi_api_url, cscs_dwdi_client_id, "
|
|
@@ -50,8 +53,12 @@ class CSCSDWDIBackend(BaseBackend):
|
|
|
50
53
|
client_secret=self.client_secret,
|
|
51
54
|
oidc_token_url=self.oidc_token_url,
|
|
52
55
|
oidc_scope=self.oidc_scope,
|
|
56
|
+
socks_proxy=self.socks_proxy,
|
|
53
57
|
)
|
|
54
58
|
|
|
59
|
+
if self.socks_proxy:
|
|
60
|
+
logger.info("CSCS-DWDI Compute Backend: Using SOCKS proxy: %s", self.socks_proxy)
|
|
61
|
+
|
|
55
62
|
def ping(self, raise_exception: bool = False) -> bool: # noqa: ARG002
|
|
56
63
|
"""Check if CSCS-DWDI API is accessible.
|
|
57
64
|
|
|
@@ -360,3 +367,299 @@ class CSCSDWDIBackend(BaseBackend):
|
|
|
360
367
|
"""Not implemented for reporting-only backend."""
|
|
361
368
|
msg = "CSCS-DWDI backend is reporting-only and does not support resource downscaling"
|
|
362
369
|
raise NotImplementedError(msg)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class CSCSDWDIStorageBackend(BaseBackend):
|
|
373
|
+
"""Backend for reporting storage usage from CSCS-DWDI API."""
|
|
374
|
+
|
|
375
|
+
def __init__(
|
|
376
|
+
self, backend_settings: dict[str, Any], backend_components: dict[str, dict]
|
|
377
|
+
) -> None:
|
|
378
|
+
"""Initialize CSCS-DWDI storage backend.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
backend_settings: Backend-specific settings from the offering
|
|
382
|
+
backend_components: Component configuration from the offering
|
|
383
|
+
"""
|
|
384
|
+
super().__init__(backend_settings, backend_components)
|
|
385
|
+
self.backend_type = "cscs-dwdi-storage"
|
|
386
|
+
|
|
387
|
+
# Extract CSCS-DWDI specific configuration
|
|
388
|
+
self.api_url = backend_settings.get("cscs_dwdi_api_url", "")
|
|
389
|
+
self.client_id = backend_settings.get("cscs_dwdi_client_id", "")
|
|
390
|
+
self.client_secret = backend_settings.get("cscs_dwdi_client_secret", "")
|
|
391
|
+
|
|
392
|
+
# Required OIDC configuration
|
|
393
|
+
self.oidc_token_url = backend_settings.get("cscs_dwdi_oidc_token_url", "")
|
|
394
|
+
self.oidc_scope = backend_settings.get("cscs_dwdi_oidc_scope")
|
|
395
|
+
|
|
396
|
+
# Storage-specific settings
|
|
397
|
+
self.filesystem = backend_settings.get("storage_filesystem", "")
|
|
398
|
+
self.data_type = backend_settings.get("storage_data_type", "")
|
|
399
|
+
self.tenant = backend_settings.get("storage_tenant", "")
|
|
400
|
+
self.path_mapping = backend_settings.get("storage_path_mapping", {})
|
|
401
|
+
|
|
402
|
+
# Optional SOCKS proxy configuration
|
|
403
|
+
self.socks_proxy = backend_settings.get("socks_proxy")
|
|
404
|
+
|
|
405
|
+
if not all([self.api_url, self.client_id, self.client_secret, self.oidc_token_url]):
|
|
406
|
+
msg = (
|
|
407
|
+
"CSCS-DWDI storage backend requires cscs_dwdi_api_url, cscs_dwdi_client_id, "
|
|
408
|
+
"cscs_dwdi_client_secret, and cscs_dwdi_oidc_token_url in backend_settings"
|
|
409
|
+
)
|
|
410
|
+
raise ValueError(msg)
|
|
411
|
+
|
|
412
|
+
if not all([self.filesystem, self.data_type]):
|
|
413
|
+
msg = (
|
|
414
|
+
"CSCS-DWDI storage backend requires storage_filesystem and storage_data_type "
|
|
415
|
+
"in backend_settings"
|
|
416
|
+
)
|
|
417
|
+
raise ValueError(msg)
|
|
418
|
+
|
|
419
|
+
self.cscs_client = CSCSDWDIClient(
|
|
420
|
+
api_url=self.api_url,
|
|
421
|
+
client_id=self.client_id,
|
|
422
|
+
client_secret=self.client_secret,
|
|
423
|
+
oidc_token_url=self.oidc_token_url,
|
|
424
|
+
oidc_scope=self.oidc_scope,
|
|
425
|
+
socks_proxy=self.socks_proxy,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
if self.socks_proxy:
|
|
429
|
+
logger.info("CSCS-DWDI Storage Backend: Using SOCKS proxy: %s", self.socks_proxy)
|
|
430
|
+
|
|
431
|
+
def ping(self, raise_exception: bool = False) -> bool: # noqa: ARG002
|
|
432
|
+
"""Check if CSCS-DWDI API is accessible.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
raise_exception: Whether to raise an exception on failure
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
True if API is accessible, False otherwise
|
|
439
|
+
"""
|
|
440
|
+
return self.cscs_client.ping_storage()
|
|
441
|
+
|
|
442
|
+
def _get_usage_report(
|
|
443
|
+
self, resource_backend_ids: list[str]
|
|
444
|
+
) -> dict[str, dict[str, dict[str, float]]]:
|
|
445
|
+
"""Get storage usage report for specified resources.
|
|
446
|
+
|
|
447
|
+
This method queries the CSCS-DWDI storage API for the current month's usage
|
|
448
|
+
and formats it according to Waldur's expected structure.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
resource_backend_ids: List of resource identifiers (paths or mapped IDs)
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Dictionary mapping resource IDs to usage data:
|
|
455
|
+
{
|
|
456
|
+
"resource1": {
|
|
457
|
+
"TOTAL_ACCOUNT_USAGE": {
|
|
458
|
+
"storage_space": 1234.56, # GB
|
|
459
|
+
"storage_inodes": 50000
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
...
|
|
463
|
+
}
|
|
464
|
+
"""
|
|
465
|
+
if not resource_backend_ids:
|
|
466
|
+
logger.warning("No resource backend IDs provided for storage usage report")
|
|
467
|
+
return {}
|
|
468
|
+
|
|
469
|
+
# Get current month for reporting
|
|
470
|
+
today = datetime.now(tz=timezone.utc).date()
|
|
471
|
+
exact_month = today.strftime("%Y-%m")
|
|
472
|
+
|
|
473
|
+
logger.info(
|
|
474
|
+
"Fetching storage usage report for %d resources for month %s",
|
|
475
|
+
len(resource_backend_ids),
|
|
476
|
+
exact_month,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
usage_report = {}
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
# Map resource IDs to storage paths
|
|
483
|
+
paths_to_query = []
|
|
484
|
+
id_to_path_map = {}
|
|
485
|
+
|
|
486
|
+
for resource_id in resource_backend_ids:
|
|
487
|
+
# Check if we have a path mapping for this resource ID
|
|
488
|
+
if resource_id in self.path_mapping:
|
|
489
|
+
path = self.path_mapping[resource_id]
|
|
490
|
+
paths_to_query.append(path)
|
|
491
|
+
id_to_path_map[path] = resource_id
|
|
492
|
+
else:
|
|
493
|
+
# Assume the resource_id is the path itself
|
|
494
|
+
paths_to_query.append(resource_id)
|
|
495
|
+
id_to_path_map[resource_id] = resource_id
|
|
496
|
+
|
|
497
|
+
if not paths_to_query:
|
|
498
|
+
logger.warning("No paths to query after mapping")
|
|
499
|
+
return {}
|
|
500
|
+
|
|
501
|
+
# Query CSCS-DWDI storage API
|
|
502
|
+
response = self.cscs_client.get_storage_usage_for_month(
|
|
503
|
+
paths=paths_to_query,
|
|
504
|
+
tenant=self.tenant,
|
|
505
|
+
filesystem=self.filesystem,
|
|
506
|
+
data_type=self.data_type,
|
|
507
|
+
exact_month=exact_month,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Process the response
|
|
511
|
+
storage_data = response.get("storage", [])
|
|
512
|
+
|
|
513
|
+
for storage_entry in storage_data:
|
|
514
|
+
path = storage_entry.get("path")
|
|
515
|
+
if not path:
|
|
516
|
+
logger.warning("Storage entry missing path, skipping")
|
|
517
|
+
continue
|
|
518
|
+
|
|
519
|
+
# Map path back to resource ID
|
|
520
|
+
resource_id = id_to_path_map.get(path, path)
|
|
521
|
+
|
|
522
|
+
# Extract storage metrics
|
|
523
|
+
space_used_bytes = storage_entry.get("spaceUsed", 0)
|
|
524
|
+
inodes_used = storage_entry.get("inodesUsed", 0)
|
|
525
|
+
|
|
526
|
+
# Convert bytes to configured units (typically GB)
|
|
527
|
+
storage_usage = {}
|
|
528
|
+
for component_name, component_config in self.backend_components.items():
|
|
529
|
+
if (
|
|
530
|
+
"storage_space" in component_name.lower()
|
|
531
|
+
or "space" in component_name.lower()
|
|
532
|
+
):
|
|
533
|
+
# Apply unit factor for space (e.g., bytes to GB)
|
|
534
|
+
unit_factor = component_config.get("unit_factor", 1)
|
|
535
|
+
storage_usage[component_name] = round(space_used_bytes * unit_factor, 2)
|
|
536
|
+
elif "inode" in component_name.lower() or "file" in component_name.lower():
|
|
537
|
+
# Inodes typically don't need conversion
|
|
538
|
+
unit_factor = component_config.get("unit_factor", 1)
|
|
539
|
+
storage_usage[component_name] = round(inodes_used * unit_factor, 2)
|
|
540
|
+
|
|
541
|
+
usage_report[resource_id] = {"TOTAL_ACCOUNT_USAGE": storage_usage}
|
|
542
|
+
|
|
543
|
+
logger.info(
|
|
544
|
+
"Successfully retrieved storage usage for %d resources",
|
|
545
|
+
len(usage_report),
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
return usage_report
|
|
549
|
+
|
|
550
|
+
except Exception:
|
|
551
|
+
logger.exception("Failed to get storage usage report from CSCS-DWDI")
|
|
552
|
+
raise
|
|
553
|
+
|
|
554
|
+
# Methods not implemented for reporting-only backend
|
|
555
|
+
def get_account(self, account_name: str) -> Optional[dict[str, Any]]:
|
|
556
|
+
"""Not implemented for reporting-only backend."""
|
|
557
|
+
msg = "CSCS-DWDI storage backend is reporting-only and does not support account management"
|
|
558
|
+
raise NotImplementedError(msg)
|
|
559
|
+
|
|
560
|
+
def create_account(self, account_data: dict) -> bool:
|
|
561
|
+
"""Not implemented for reporting-only backend."""
|
|
562
|
+
msg = "CSCS-DWDI storage backend is reporting-only and does not support account creation"
|
|
563
|
+
raise NotImplementedError(msg)
|
|
564
|
+
|
|
565
|
+
def delete_account(self, account_name: str) -> bool:
|
|
566
|
+
"""Not implemented for reporting-only backend."""
|
|
567
|
+
msg = "CSCS-DWDI storage backend is reporting-only and does not support account deletion"
|
|
568
|
+
raise NotImplementedError(msg)
|
|
569
|
+
|
|
570
|
+
def update_account_limit_deposit(
|
|
571
|
+
self,
|
|
572
|
+
account_name: str,
|
|
573
|
+
component_type: str,
|
|
574
|
+
component_amount: float,
|
|
575
|
+
offering_component_data: dict,
|
|
576
|
+
) -> bool:
|
|
577
|
+
"""Not implemented for reporting-only backend."""
|
|
578
|
+
msg = "CSCS-DWDI storage backend is reporting-only and does not support limit updates"
|
|
579
|
+
raise NotImplementedError(msg)
|
|
580
|
+
|
|
581
|
+
def reset_account_limit_deposit(
|
|
582
|
+
self,
|
|
583
|
+
account_name: str,
|
|
584
|
+
component_type: str,
|
|
585
|
+
offering_component_data: dict,
|
|
586
|
+
) -> bool:
|
|
587
|
+
"""Not implemented for reporting-only backend."""
|
|
588
|
+
msg = "CSCS-DWDI storage backend is reporting-only and does not support limit resets"
|
|
589
|
+
raise NotImplementedError(msg)
|
|
590
|
+
|
|
591
|
+
def add_account_users(self, account_name: str, user_backend_ids: list[str]) -> bool:
|
|
592
|
+
"""Not implemented for reporting-only backend."""
|
|
593
|
+
msg = "CSCS-DWDI storage backend is reporting-only and does not support user management"
|
|
594
|
+
raise NotImplementedError(msg)
|
|
595
|
+
|
|
596
|
+
def delete_account_users(self, account_name: str, user_backend_ids: list[str]) -> bool:
|
|
597
|
+
"""Not implemented for reporting-only backend."""
|
|
598
|
+
msg = "CSCS-DWDI storage backend is reporting-only and does not support user management"
|
|
599
|
+
raise NotImplementedError(msg)
|
|
600
|
+
|
|
601
|
+
def list_accounts(self) -> list[dict[str, Any]]:
|
|
602
|
+
"""Not implemented for reporting-only backend."""
|
|
603
|
+
msg = "CSCS-DWDI storage backend is reporting-only and does not support account listing"
|
|
604
|
+
raise NotImplementedError(msg)
|
|
605
|
+
|
|
606
|
+
def set_resource_limits(self, resource_backend_id: str, limits: dict[str, int]) -> None:
|
|
607
|
+
"""Not implemented for reporting-only backend."""
|
|
608
|
+
msg = "CSCS-DWDI storage backend is reporting-only and does not support resource limits"
|
|
609
|
+
raise NotImplementedError(msg)
|
|
610
|
+
|
|
611
|
+
def diagnostics(self) -> bool:
|
|
612
|
+
"""Get diagnostic information for the backend."""
|
|
613
|
+
logger.info(
|
|
614
|
+
"CSCS-DWDI Storage Backend Diagnostics - Type: %s, API: %s, "
|
|
615
|
+
"Filesystem: %s, DataType: %s, Components: %s, Ping: %s",
|
|
616
|
+
self.backend_type,
|
|
617
|
+
self.api_url,
|
|
618
|
+
self.filesystem,
|
|
619
|
+
self.data_type,
|
|
620
|
+
list(self.backend_components.keys()),
|
|
621
|
+
self.ping(),
|
|
622
|
+
)
|
|
623
|
+
return self.ping()
|
|
624
|
+
|
|
625
|
+
def get_resource_metadata(self, resource_backend_id: str) -> dict[str, Any]:
|
|
626
|
+
"""Not implemented for reporting-only backend."""
|
|
627
|
+
msg = "CSCS-DWDI storage backend is reporting-only and does not support resource metadata"
|
|
628
|
+
raise NotImplementedError(msg)
|
|
629
|
+
|
|
630
|
+
def list_components(self) -> list[str]:
|
|
631
|
+
"""List configured components for this backend."""
|
|
632
|
+
return list(self.backend_components.keys())
|
|
633
|
+
|
|
634
|
+
def _collect_resource_limits(
|
|
635
|
+
self, waldur_resource: WaldurResource
|
|
636
|
+
) -> tuple[dict[str, int], dict[str, int]]:
|
|
637
|
+
"""Not implemented for reporting-only backend."""
|
|
638
|
+
msg = "CSCS-DWDI storage backend is reporting-only and does not support resource limits"
|
|
639
|
+
raise NotImplementedError(msg)
|
|
640
|
+
|
|
641
|
+
def _pre_create_resource(
|
|
642
|
+
self, waldur_resource: WaldurResource, user_context: Optional[dict] = None
|
|
643
|
+
) -> None:
|
|
644
|
+
"""Not implemented for reporting-only backend."""
|
|
645
|
+
msg = "CSCS-DWDI storage backend is reporting-only and does not support resource creation"
|
|
646
|
+
raise NotImplementedError(msg)
|
|
647
|
+
|
|
648
|
+
def pause_resource(self, resource_backend_id: str) -> bool:
|
|
649
|
+
"""Not implemented for reporting-only backend."""
|
|
650
|
+
msg = "CSCS-DWDI storage backend is reporting-only and does not support resource pausing"
|
|
651
|
+
raise NotImplementedError(msg)
|
|
652
|
+
|
|
653
|
+
def restore_resource(self, resource_backend_id: str) -> bool:
|
|
654
|
+
"""Not implemented for reporting-only backend."""
|
|
655
|
+
msg = (
|
|
656
|
+
"CSCS-DWDI storage backend is reporting-only and does not support resource restoration"
|
|
657
|
+
)
|
|
658
|
+
raise NotImplementedError(msg)
|
|
659
|
+
|
|
660
|
+
def downscale_resource(self, resource_backend_id: str) -> bool:
|
|
661
|
+
"""Not implemented for reporting-only backend."""
|
|
662
|
+
msg = (
|
|
663
|
+
"CSCS-DWDI storage backend is reporting-only and does not support resource downscaling"
|
|
664
|
+
)
|
|
665
|
+
raise NotImplementedError(msg)
|
|
@@ -21,6 +21,7 @@ class CSCSDWDIClient:
|
|
|
21
21
|
client_secret: str,
|
|
22
22
|
oidc_token_url: Optional[str] = None,
|
|
23
23
|
oidc_scope: Optional[str] = None,
|
|
24
|
+
socks_proxy: Optional[str] = None,
|
|
24
25
|
) -> None:
|
|
25
26
|
"""Initialize CSCS-DWDI client.
|
|
26
27
|
|
|
@@ -30,12 +31,14 @@ class CSCSDWDIClient:
|
|
|
30
31
|
client_secret: OIDC client secret for authentication
|
|
31
32
|
oidc_token_url: OIDC token endpoint URL (required for authentication)
|
|
32
33
|
oidc_scope: OIDC scope to request (optional)
|
|
34
|
+
socks_proxy: SOCKS proxy URL (e.g., "socks5://localhost:12345")
|
|
33
35
|
"""
|
|
34
36
|
self.api_url = api_url.rstrip("/")
|
|
35
37
|
self.client_id = client_id
|
|
36
38
|
self.client_secret = client_secret
|
|
37
39
|
self.oidc_token_url = oidc_token_url
|
|
38
40
|
self.oidc_scope = oidc_scope or "openid"
|
|
41
|
+
self.socks_proxy = socks_proxy
|
|
39
42
|
self._token: Optional[str] = None
|
|
40
43
|
self._token_expires_at: Optional[datetime] = None
|
|
41
44
|
|
|
@@ -91,10 +94,14 @@ class CSCSDWDIClient:
|
|
|
91
94
|
|
|
92
95
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
# Configure httpx client with SOCKS proxy if specified
|
|
98
|
+
client_args: dict[str, Any] = {"timeout": 30.0}
|
|
99
|
+
if self.socks_proxy:
|
|
100
|
+
client_args["proxy"] = self.socks_proxy
|
|
101
|
+
logger.debug("Using SOCKS proxy for token acquisition: %s", self.socks_proxy)
|
|
102
|
+
|
|
103
|
+
with httpx.Client(**client_args) as client:
|
|
104
|
+
response = client.post(self.oidc_token_url, data=token_data, headers=headers)
|
|
98
105
|
response.raise_for_status()
|
|
99
106
|
token_response = response.json()
|
|
100
107
|
|
|
@@ -160,8 +167,14 @@ class CSCSDWDIClient:
|
|
|
160
167
|
to_month,
|
|
161
168
|
)
|
|
162
169
|
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
# Configure httpx client with SOCKS proxy if specified
|
|
171
|
+
client_args: dict[str, Any] = {"timeout": 30.0}
|
|
172
|
+
if self.socks_proxy:
|
|
173
|
+
client_args["proxy"] = self.socks_proxy
|
|
174
|
+
logger.debug("Using SOCKS proxy for API request: %s", self.socks_proxy)
|
|
175
|
+
|
|
176
|
+
with httpx.Client(**client_args) as client:
|
|
177
|
+
response = client.get(url, params=params, headers=headers)
|
|
165
178
|
response.raise_for_status()
|
|
166
179
|
return response.json()
|
|
167
180
|
|
|
@@ -207,13 +220,139 @@ class CSCSDWDIClient:
|
|
|
207
220
|
to_day,
|
|
208
221
|
)
|
|
209
222
|
|
|
210
|
-
|
|
211
|
-
|
|
223
|
+
# Configure httpx client with SOCKS proxy if specified
|
|
224
|
+
client_args: dict[str, Any] = {"timeout": 30.0}
|
|
225
|
+
if self.socks_proxy:
|
|
226
|
+
client_args["proxy"] = self.socks_proxy
|
|
227
|
+
logger.debug("Using SOCKS proxy for API request: %s", self.socks_proxy)
|
|
228
|
+
|
|
229
|
+
with httpx.Client(**client_args) as client:
|
|
230
|
+
response = client.get(url, params=params, headers=headers)
|
|
231
|
+
response.raise_for_status()
|
|
232
|
+
return response.json()
|
|
233
|
+
|
|
234
|
+
def get_storage_usage_for_month(
|
|
235
|
+
self,
|
|
236
|
+
paths: list[str],
|
|
237
|
+
tenant: Optional[str],
|
|
238
|
+
filesystem: str,
|
|
239
|
+
data_type: str,
|
|
240
|
+
exact_month: str,
|
|
241
|
+
) -> dict[str, Any]:
|
|
242
|
+
"""Get storage usage data for specified paths for a month.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
paths: List of storage paths to query
|
|
246
|
+
tenant: Tenant identifier (optional)
|
|
247
|
+
filesystem: Filesystem name (e.g., "lustre")
|
|
248
|
+
data_type: Data type (e.g., "projects")
|
|
249
|
+
exact_month: Month in YYYY-MM format
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
API response with storage usage data
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
httpx.HTTPError: If API request fails
|
|
256
|
+
"""
|
|
257
|
+
token = self._get_auth_token()
|
|
258
|
+
|
|
259
|
+
params: dict[str, Any] = {
|
|
260
|
+
"exact-month": exact_month,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# Add optional parameters
|
|
264
|
+
if paths:
|
|
265
|
+
params["paths"] = paths
|
|
266
|
+
if tenant:
|
|
267
|
+
params["tenant"] = tenant
|
|
268
|
+
if filesystem:
|
|
269
|
+
params["filesystem"] = filesystem
|
|
270
|
+
if data_type:
|
|
271
|
+
params["data_type"] = data_type
|
|
272
|
+
|
|
273
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
274
|
+
|
|
275
|
+
url = f"{self.api_url}/api/v1/storage/usage-month/filesystem_name/data_type"
|
|
276
|
+
|
|
277
|
+
logger.debug(
|
|
278
|
+
"Fetching storage usage for paths %s for month %s",
|
|
279
|
+
paths,
|
|
280
|
+
exact_month,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Configure httpx client with SOCKS proxy if specified
|
|
284
|
+
client_args: dict[str, Any] = {"timeout": 30.0}
|
|
285
|
+
if self.socks_proxy:
|
|
286
|
+
client_args["proxy"] = self.socks_proxy
|
|
287
|
+
logger.debug("Using SOCKS proxy for API request: %s", self.socks_proxy)
|
|
288
|
+
|
|
289
|
+
with httpx.Client(**client_args) as client:
|
|
290
|
+
response = client.get(url, params=params, headers=headers)
|
|
291
|
+
response.raise_for_status()
|
|
292
|
+
return response.json()
|
|
293
|
+
|
|
294
|
+
def get_storage_usage_for_day(
|
|
295
|
+
self,
|
|
296
|
+
paths: list[str],
|
|
297
|
+
tenant: Optional[str],
|
|
298
|
+
filesystem: str,
|
|
299
|
+
data_type: str,
|
|
300
|
+
exact_date: date,
|
|
301
|
+
) -> dict[str, Any]:
|
|
302
|
+
"""Get storage usage data for specified paths for a specific day.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
paths: List of storage paths to query
|
|
306
|
+
tenant: Tenant identifier (optional)
|
|
307
|
+
filesystem: Filesystem name (e.g., "lustre")
|
|
308
|
+
data_type: Data type (e.g., "projects")
|
|
309
|
+
exact_date: Specific date
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
API response with storage usage data
|
|
313
|
+
|
|
314
|
+
Raises:
|
|
315
|
+
httpx.HTTPError: If API request fails
|
|
316
|
+
"""
|
|
317
|
+
token = self._get_auth_token()
|
|
318
|
+
|
|
319
|
+
params: dict[str, Any] = {
|
|
320
|
+
"exact-date": exact_date.strftime("%Y-%m-%d"),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# Add optional parameters
|
|
324
|
+
if paths:
|
|
325
|
+
params["paths"] = paths
|
|
326
|
+
if tenant:
|
|
327
|
+
params["tenant"] = tenant
|
|
328
|
+
if filesystem:
|
|
329
|
+
params["filesystem"] = filesystem
|
|
330
|
+
if data_type:
|
|
331
|
+
params["data_type"] = data_type
|
|
332
|
+
|
|
333
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
334
|
+
|
|
335
|
+
url = f"{self.api_url}/api/v1/storage/usage-day/filesystem_name/data_type"
|
|
336
|
+
|
|
337
|
+
logger.debug(
|
|
338
|
+
"Fetching storage usage for paths %s for date %s",
|
|
339
|
+
paths,
|
|
340
|
+
exact_date,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Configure httpx client with SOCKS proxy if specified
|
|
344
|
+
client_args: dict[str, Any] = {"timeout": 30.0}
|
|
345
|
+
if self.socks_proxy:
|
|
346
|
+
client_args["proxy"] = self.socks_proxy
|
|
347
|
+
logger.debug("Using SOCKS proxy for API request: %s", self.socks_proxy)
|
|
348
|
+
|
|
349
|
+
with httpx.Client(**client_args) as client:
|
|
350
|
+
response = client.get(url, params=params, headers=headers)
|
|
212
351
|
response.raise_for_status()
|
|
213
352
|
return response.json()
|
|
214
353
|
|
|
215
354
|
def ping(self) -> bool:
|
|
216
|
-
"""Check if CSCS-DWDI API is accessible.
|
|
355
|
+
"""Check if CSCS-DWDI compute API is accessible.
|
|
217
356
|
|
|
218
357
|
Returns:
|
|
219
358
|
True if API is accessible, False otherwise
|
|
@@ -231,9 +370,46 @@ class CSCSDWDIClient:
|
|
|
231
370
|
|
|
232
371
|
url = f"{self.api_url}/api/v1/compute/usage-day-multiaccount"
|
|
233
372
|
|
|
234
|
-
|
|
235
|
-
|
|
373
|
+
# Configure httpx client with SOCKS proxy if specified
|
|
374
|
+
client_args: dict[str, Any] = {"timeout": 10.0}
|
|
375
|
+
if self.socks_proxy:
|
|
376
|
+
client_args["proxy"] = self.socks_proxy
|
|
377
|
+
logger.debug("Using SOCKS proxy for ping: %s", self.socks_proxy)
|
|
378
|
+
|
|
379
|
+
with httpx.Client(**client_args) as client:
|
|
380
|
+
response = client.get(url, params=params, headers=headers)
|
|
381
|
+
return response.status_code == HTTP_OK
|
|
382
|
+
except Exception:
|
|
383
|
+
logger.exception("Compute ping failed")
|
|
384
|
+
return False
|
|
385
|
+
|
|
386
|
+
def ping_storage(self) -> bool:
|
|
387
|
+
"""Check if CSCS-DWDI storage API is accessible.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
True if API is accessible, False otherwise
|
|
391
|
+
"""
|
|
392
|
+
try:
|
|
393
|
+
token = self._get_auth_token()
|
|
394
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
395
|
+
|
|
396
|
+
# Use a simple query to test connectivity
|
|
397
|
+
today = datetime.now(tz=timezone.utc).date()
|
|
398
|
+
params = {
|
|
399
|
+
"exact-date": today.strftime("%Y-%m-%d"),
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
url = f"{self.api_url}/api/v1/storage/usage-day/filesystem_name/data_type"
|
|
403
|
+
|
|
404
|
+
# Configure httpx client with SOCKS proxy if specified
|
|
405
|
+
client_args: dict[str, Any] = {"timeout": 10.0}
|
|
406
|
+
if self.socks_proxy:
|
|
407
|
+
client_args["proxy"] = self.socks_proxy
|
|
408
|
+
logger.debug("Using SOCKS proxy for storage ping: %s", self.socks_proxy)
|
|
409
|
+
|
|
410
|
+
with httpx.Client(**client_args) as client:
|
|
411
|
+
response = client.get(url, params=params, headers=headers)
|
|
236
412
|
return response.status_code == HTTP_OK
|
|
237
413
|
except Exception:
|
|
238
|
-
logger.exception("
|
|
414
|
+
logger.exception("Storage ping failed")
|
|
239
415
|
return False
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: waldur-site-agent-cscs-dwdi
|
|
3
|
+
Version: 0.7.3
|
|
4
|
+
Summary: CSCS-DWDI reporting plugin for Waldur Site Agent
|
|
5
|
+
Author-email: OpenNode Team <info@opennodecloud.com>
|
|
6
|
+
Requires-Python: <4,>=3.9
|
|
7
|
+
Requires-Dist: httpx>=0.25.0
|
|
8
|
+
Requires-Dist: waldur-site-agent>=0.7.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# CSCS-DWDI Plugin for Waldur Site Agent
|
|
12
|
+
|
|
13
|
+
This plugin provides integration with the CSCS Data Warehouse Data Intelligence (DWDI) system to report both
|
|
14
|
+
computational and storage usage data to Waldur.
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
The plugin implements two separate backends to handle different types of accounting data:
|
|
19
|
+
|
|
20
|
+
- **Compute Backend** (`cscs-dwdi-compute`): Reports CPU and node hour usage from HPC clusters
|
|
21
|
+
- **Storage Backend** (`cscs-dwdi-storage`): Reports storage space and inode usage from filesystems
|
|
22
|
+
|
|
23
|
+
## Backend Types
|
|
24
|
+
|
|
25
|
+
### Compute Backend
|
|
26
|
+
|
|
27
|
+
The compute backend queries the DWDI API for computational resource usage and reports:
|
|
28
|
+
|
|
29
|
+
- Node hours consumed by accounts and users
|
|
30
|
+
- CPU hours consumed by accounts and users
|
|
31
|
+
- Account-level and user-level usage aggregation
|
|
32
|
+
|
|
33
|
+
**API Endpoints Used:**
|
|
34
|
+
|
|
35
|
+
- `/api/v1/compute/usage-month/account` - Monthly usage data
|
|
36
|
+
- `/api/v1/compute/usage-day/account` - Daily usage data
|
|
37
|
+
|
|
38
|
+
### Storage Backend
|
|
39
|
+
|
|
40
|
+
The storage backend queries the DWDI API for storage resource usage and reports:
|
|
41
|
+
|
|
42
|
+
- Storage space used (converted from bytes to configured units)
|
|
43
|
+
- Inode (file count) usage
|
|
44
|
+
- Path-based resource identification
|
|
45
|
+
|
|
46
|
+
**API Endpoints Used:**
|
|
47
|
+
|
|
48
|
+
- `/api/v1/storage/usage-month/filesystem_name/data_type` - Monthly storage usage
|
|
49
|
+
- `/api/v1/storage/usage-day/filesystem_name/data_type` - Daily storage usage
|
|
50
|
+
|
|
51
|
+
## Configuration
|
|
52
|
+
|
|
53
|
+
### Compute Backend Configuration
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
backend_type: "cscs-dwdi-compute"
|
|
57
|
+
|
|
58
|
+
backend_settings:
|
|
59
|
+
cscs_dwdi_api_url: "https://dwdi.cscs.ch"
|
|
60
|
+
cscs_dwdi_client_id: "your_oidc_client_id"
|
|
61
|
+
cscs_dwdi_client_secret: "your_oidc_client_secret"
|
|
62
|
+
cscs_dwdi_oidc_token_url: "https://auth.cscs.ch/realms/cscs/protocol/openid-connect/token"
|
|
63
|
+
cscs_dwdi_oidc_scope: "openid" # Optional
|
|
64
|
+
|
|
65
|
+
backend_components:
|
|
66
|
+
nodeHours:
|
|
67
|
+
measured_unit: "node-hours"
|
|
68
|
+
unit_factor: 1
|
|
69
|
+
accounting_type: "usage"
|
|
70
|
+
label: "Node Hours"
|
|
71
|
+
|
|
72
|
+
cpuHours:
|
|
73
|
+
measured_unit: "cpu-hours"
|
|
74
|
+
unit_factor: 1
|
|
75
|
+
accounting_type: "usage"
|
|
76
|
+
label: "CPU Hours"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Storage Backend Configuration
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
backend_type: "cscs-dwdi-storage"
|
|
83
|
+
|
|
84
|
+
backend_settings:
|
|
85
|
+
cscs_dwdi_api_url: "https://dwdi.cscs.ch"
|
|
86
|
+
cscs_dwdi_client_id: "your_oidc_client_id"
|
|
87
|
+
cscs_dwdi_client_secret: "your_oidc_client_secret"
|
|
88
|
+
cscs_dwdi_oidc_token_url: "https://auth.cscs.ch/realms/cscs/protocol/openid-connect/token"
|
|
89
|
+
|
|
90
|
+
# Storage-specific settings
|
|
91
|
+
storage_filesystem: "lustre"
|
|
92
|
+
storage_data_type: "projects"
|
|
93
|
+
storage_tenant: "cscs" # Optional
|
|
94
|
+
|
|
95
|
+
# Map Waldur resource IDs to storage paths
|
|
96
|
+
storage_path_mapping:
|
|
97
|
+
"project_123": "/store/projects/proj123"
|
|
98
|
+
"project_456": "/store/projects/proj456"
|
|
99
|
+
|
|
100
|
+
backend_components:
|
|
101
|
+
storage_space:
|
|
102
|
+
measured_unit: "GB"
|
|
103
|
+
unit_factor: 0.000000001 # Convert bytes to GB
|
|
104
|
+
accounting_type: "usage"
|
|
105
|
+
label: "Storage Space (GB)"
|
|
106
|
+
|
|
107
|
+
storage_inodes:
|
|
108
|
+
measured_unit: "count"
|
|
109
|
+
unit_factor: 1
|
|
110
|
+
accounting_type: "usage"
|
|
111
|
+
label: "File Count"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Authentication
|
|
115
|
+
|
|
116
|
+
Both backends use OIDC client credentials flow for authentication with the DWDI API. You need:
|
|
117
|
+
|
|
118
|
+
- `cscs_dwdi_client_id`: OIDC client identifier
|
|
119
|
+
- `cscs_dwdi_client_secret`: OIDC client secret
|
|
120
|
+
- `cscs_dwdi_oidc_token_url`: OIDC token endpoint URL
|
|
121
|
+
- `cscs_dwdi_oidc_scope`: OIDC scope (optional, defaults to "openid")
|
|
122
|
+
|
|
123
|
+
## SOCKS Proxy Support
|
|
124
|
+
|
|
125
|
+
Both backends support SOCKS proxy for network connectivity. This is useful when the DWDI API is only accessible
|
|
126
|
+
through a proxy or jump host.
|
|
127
|
+
|
|
128
|
+
### SOCKS Proxy Configuration
|
|
129
|
+
|
|
130
|
+
Add the SOCKS proxy setting to your backend configuration:
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
backend_settings:
|
|
134
|
+
# ... other settings ...
|
|
135
|
+
socks_proxy: "socks5://localhost:12345" # SOCKS5 proxy URL
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Supported Proxy Types
|
|
139
|
+
|
|
140
|
+
- **SOCKS5**: `socks5://hostname:port`
|
|
141
|
+
- **SOCKS4**: `socks4://hostname:port`
|
|
142
|
+
- **HTTP**: `http://hostname:port`
|
|
143
|
+
|
|
144
|
+
### Usage Examples
|
|
145
|
+
|
|
146
|
+
**SSH Tunnel with SOCKS5:**
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Create SSH tunnel to jump host
|
|
150
|
+
ssh -D 12345 -N user@jumphost.cscs.ch
|
|
151
|
+
|
|
152
|
+
# Configure backend to use tunnel
|
|
153
|
+
backend_settings:
|
|
154
|
+
socks_proxy: "socks5://localhost:12345"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**HTTP Proxy:**
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
backend_settings:
|
|
161
|
+
socks_proxy: "http://proxy.cscs.ch:8080"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Resource Identification
|
|
165
|
+
|
|
166
|
+
### Compute Resources
|
|
167
|
+
|
|
168
|
+
For compute resources, the system uses account names as returned by the DWDI API. The Waldur resource
|
|
169
|
+
`backend_id` should match the account name in the cluster accounting system.
|
|
170
|
+
|
|
171
|
+
### Storage Resources
|
|
172
|
+
|
|
173
|
+
For storage resources, there are two options:
|
|
174
|
+
|
|
175
|
+
1. **Direct Path Usage**: Set the Waldur resource `backend_id` to the actual filesystem path
|
|
176
|
+
2. **Path Mapping**: Use the `storage_path_mapping` setting to map resource IDs to paths
|
|
177
|
+
|
|
178
|
+
## Usage Reporting
|
|
179
|
+
|
|
180
|
+
Both backends are read-only and designed for usage reporting. They implement the `_get_usage_report()` method
|
|
181
|
+
but do not support:
|
|
182
|
+
|
|
183
|
+
- Account creation/deletion
|
|
184
|
+
- Resource management
|
|
185
|
+
- User management
|
|
186
|
+
- Limit setting
|
|
187
|
+
|
|
188
|
+
## Example Configurations
|
|
189
|
+
|
|
190
|
+
See the `examples/` directory for complete configuration examples:
|
|
191
|
+
|
|
192
|
+
- `cscs-dwdi-compute-config.yaml` - Compute backend only
|
|
193
|
+
- `cscs-dwdi-storage-config.yaml` - Storage backend only
|
|
194
|
+
- `cscs-dwdi-combined-config.yaml` - Both backends in one configuration
|
|
195
|
+
|
|
196
|
+
## Installation
|
|
197
|
+
|
|
198
|
+
The plugin is automatically discovered when the waldur-site-agent-cscs-dwdi package is installed alongside waldur-site-agent.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Install all workspace packages including cscs-dwdi plugin
|
|
202
|
+
uv sync --all-packages
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Testing
|
|
206
|
+
|
|
207
|
+
Run the test suite:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
uv run pytest plugins/cscs-dwdi/tests/
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## API Compatibility
|
|
214
|
+
|
|
215
|
+
This plugin is compatible with DWDI API version 1 (`/api/v1/`). It requires the following API endpoints to be available:
|
|
216
|
+
|
|
217
|
+
**Compute API:**
|
|
218
|
+
|
|
219
|
+
- `/api/v1/compute/usage-month/account`
|
|
220
|
+
- `/api/v1/compute/usage-day/account`
|
|
221
|
+
|
|
222
|
+
**Storage API:**
|
|
223
|
+
|
|
224
|
+
- `/api/v1/storage/usage-month/filesystem_name/data_type`
|
|
225
|
+
- `/api/v1/storage/usage-day/filesystem_name/data_type`
|
|
226
|
+
|
|
227
|
+
## Troubleshooting
|
|
228
|
+
|
|
229
|
+
### Authentication Issues
|
|
230
|
+
|
|
231
|
+
- Verify OIDC client credentials are correct
|
|
232
|
+
- Check that the token endpoint URL is accessible
|
|
233
|
+
- Ensure the client has appropriate scopes
|
|
234
|
+
|
|
235
|
+
### Storage Backend Issues
|
|
236
|
+
|
|
237
|
+
- Verify `storage_filesystem` and `storage_data_type` match available values in DWDI
|
|
238
|
+
- Check `storage_path_mapping` if using custom resource IDs
|
|
239
|
+
- Ensure storage paths exist in the DWDI system
|
|
240
|
+
|
|
241
|
+
### Connection Issues
|
|
242
|
+
|
|
243
|
+
- Use the `ping()` method to test API connectivity
|
|
244
|
+
- Check network connectivity to the DWDI API endpoint
|
|
245
|
+
- Verify SSL/TLS configuration
|
|
246
|
+
- If behind a firewall, configure SOCKS proxy (`socks_proxy` setting)
|
|
247
|
+
|
|
248
|
+
### Proxy Issues
|
|
249
|
+
|
|
250
|
+
- Verify proxy server is running and accessible
|
|
251
|
+
- Check proxy authentication if required
|
|
252
|
+
- Test proxy connectivity manually: `curl --proxy socks5://localhost:12345 https://dwdi.cscs.ch`
|
|
253
|
+
- Ensure proxy supports the required protocol (SOCKS4/5, HTTP)
|
|
254
|
+
|
|
255
|
+
## Development
|
|
256
|
+
|
|
257
|
+
### Project Structure
|
|
258
|
+
|
|
259
|
+
```text
|
|
260
|
+
plugins/cscs-dwdi/
|
|
261
|
+
├── pyproject.toml # Plugin configuration
|
|
262
|
+
├── README.md # This documentation
|
|
263
|
+
├── examples/ # Configuration examples
|
|
264
|
+
├── waldur_site_agent_cscs_dwdi/
|
|
265
|
+
│ ├── __init__.py # Package init
|
|
266
|
+
│ ├── backend.py # Backend implementations
|
|
267
|
+
│ └── client.py # CSCS-DWDI API client
|
|
268
|
+
└── tests/
|
|
269
|
+
└── test_cscs_dwdi.py # Plugin tests
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Key Classes
|
|
273
|
+
|
|
274
|
+
- **`CSCSDWDIComputeBackend`**: Compute usage reporting backend
|
|
275
|
+
- **`CSCSDWDIStorageBackend`**: Storage usage reporting backend
|
|
276
|
+
- **`CSCSDWDIClient`**: HTTP client for CSCS-DWDI API communication
|
|
277
|
+
|
|
278
|
+
### Extension Points
|
|
279
|
+
|
|
280
|
+
To extend the plugin:
|
|
281
|
+
|
|
282
|
+
1. **Additional Endpoints**: Modify `CSCSDWDIClient` to support more API endpoints
|
|
283
|
+
2. **Authentication Methods**: Update authentication logic in `client.py`
|
|
284
|
+
3. **Data Processing**: Enhance response processing methods for additional data formats
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
waldur_site_agent_cscs_dwdi/__init__.py,sha256=OHO1yF5NTGt0otI-GollR_ppPXP--aUZRCdaT5-8IWw,56
|
|
2
|
+
waldur_site_agent_cscs_dwdi/backend.py,sha256=2A53fAvjwnCaxsuA42bdqxHEp0DrwN1F4MYoB1ryCuQ,26846
|
|
3
|
+
waldur_site_agent_cscs_dwdi/client.py,sha256=sqCBpioe5d6sJwp-iVGUL3AUIOxkXqTEA6u6mmd0F_Y,13782
|
|
4
|
+
waldur_site_agent_cscs_dwdi-0.7.3.dist-info/METADATA,sha256=_8O14KeIMt7y4UH3-RboadKXx15txwC9R6VwbpGetVU,8131
|
|
5
|
+
waldur_site_agent_cscs_dwdi-0.7.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
waldur_site_agent_cscs_dwdi-0.7.3.dist-info/entry_points.txt,sha256=gbp1thULdYQN4leLZeM8TBoruPSGQEKQxlQ0fg8u3Ug,187
|
|
7
|
+
waldur_site_agent_cscs_dwdi-0.7.3.dist-info/RECORD,,
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: waldur-site-agent-cscs-dwdi
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: CSCS-DWDI reporting plugin for Waldur Site Agent
|
|
5
|
-
Author-email: OpenNode Team <info@opennodecloud.com>
|
|
6
|
-
Requires-Python: <4,>=3.9
|
|
7
|
-
Requires-Dist: httpx>=0.25.0
|
|
8
|
-
Requires-Dist: waldur-site-agent==0.1.0
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
|
|
11
|
-
# CSCS-DWDI Plugin for Waldur Site Agent
|
|
12
|
-
|
|
13
|
-
This plugin provides reporting functionality for Waldur Site Agent by integrating with the CSCS-DWDI
|
|
14
|
-
(Data Warehouse and Data Intelligence) API.
|
|
15
|
-
|
|
16
|
-
## Overview
|
|
17
|
-
|
|
18
|
-
The CSCS-DWDI plugin is a **reporting-only backend** that fetches compute usage data from the CSCS-DWDI
|
|
19
|
-
service and reports it to Waldur. It supports node-hour usage tracking for multiple accounts and users.
|
|
20
|
-
|
|
21
|
-
## Features
|
|
22
|
-
|
|
23
|
-
- **Monthly Usage Reporting**: Fetches usage data for the current month
|
|
24
|
-
- **Multi-Account Support**: Reports usage for multiple accounts in a single API call
|
|
25
|
-
- **Per-User Usage**: Breaks down usage by individual users within each account
|
|
26
|
-
- **OIDC Authentication**: Uses OAuth2/OIDC for secure API access
|
|
27
|
-
- **Automatic Aggregation**: Combines usage across different clusters and time periods
|
|
28
|
-
|
|
29
|
-
## Configuration
|
|
30
|
-
|
|
31
|
-
Add the following configuration to your Waldur Site Agent offering:
|
|
32
|
-
|
|
33
|
-
```yaml
|
|
34
|
-
offerings:
|
|
35
|
-
- name: "CSCS HPC Offering"
|
|
36
|
-
reporting_backend: "cscs-dwdi"
|
|
37
|
-
backend_settings:
|
|
38
|
-
cscs_dwdi_api_url: "https://dwdi-api.cscs.ch"
|
|
39
|
-
cscs_dwdi_client_id: "your-oidc-client-id"
|
|
40
|
-
cscs_dwdi_client_secret: "your-oidc-client-secret"
|
|
41
|
-
# Optional OIDC configuration (for production use)
|
|
42
|
-
cscs_dwdi_oidc_token_url: "https://identity.cscs.ch/realms/cscs/protocol/openid-connect/token"
|
|
43
|
-
cscs_dwdi_oidc_scope: "cscs-dwdi:read"
|
|
44
|
-
|
|
45
|
-
backend_components:
|
|
46
|
-
nodeHours:
|
|
47
|
-
measured_unit: "node-hours"
|
|
48
|
-
unit_factor: 1
|
|
49
|
-
accounting_type: "usage"
|
|
50
|
-
label: "Node Hours"
|
|
51
|
-
storage:
|
|
52
|
-
measured_unit: "TB"
|
|
53
|
-
unit_factor: 1
|
|
54
|
-
accounting_type: "usage"
|
|
55
|
-
label: "Storage Usage"
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Configuration Parameters
|
|
59
|
-
|
|
60
|
-
#### Backend Settings
|
|
61
|
-
|
|
62
|
-
| Parameter | Required | Description |
|
|
63
|
-
|-----------|----------|-------------|
|
|
64
|
-
| `cscs_dwdi_api_url` | Yes | Base URL for the CSCS-DWDI API service |
|
|
65
|
-
| `cscs_dwdi_client_id` | Yes | OIDC client ID for authentication |
|
|
66
|
-
| `cscs_dwdi_client_secret` | Yes | OIDC client secret for authentication |
|
|
67
|
-
| `cscs_dwdi_oidc_token_url` | Yes | OIDC token endpoint URL (required for authentication) |
|
|
68
|
-
| `cscs_dwdi_oidc_scope` | No | OIDC scope to request (defaults to "openid") |
|
|
69
|
-
|
|
70
|
-
#### Backend Components
|
|
71
|
-
|
|
72
|
-
Components must match the field names returned by the CSCS-DWDI API. For example:
|
|
73
|
-
|
|
74
|
-
- `nodeHours` - Maps to the `nodeHours` field in API responses
|
|
75
|
-
- `storage` - Maps to the `storage` field in API responses (if available)
|
|
76
|
-
- `gpuHours` - Maps to the `gpuHours` field in API responses (if available)
|
|
77
|
-
|
|
78
|
-
Each component supports:
|
|
79
|
-
|
|
80
|
-
| Parameter | Description |
|
|
81
|
-
|-----------|-------------|
|
|
82
|
-
| `measured_unit` | Unit for display in Waldur (e.g., "node-hours", "TB") |
|
|
83
|
-
| `unit_factor` | Conversion factor from API units to measured units |
|
|
84
|
-
| `accounting_type` | Either "usage" for actual usage or "limit" for quotas |
|
|
85
|
-
| `label` | Display label in Waldur interface |
|
|
86
|
-
|
|
87
|
-
## Usage Data Format
|
|
88
|
-
|
|
89
|
-
The plugin reports usage for all configured components:
|
|
90
|
-
|
|
91
|
-
- **Component Types**: Configurable (e.g., `nodeHours`, `storage`, `gpuHours`)
|
|
92
|
-
- **Units**: Based on API response and `unit_factor` configuration
|
|
93
|
-
- **Granularity**: Monthly reporting with current month data
|
|
94
|
-
- **User Attribution**: Individual user usage within each account
|
|
95
|
-
- **Aggregation**: Automatically aggregates across clusters and time periods
|
|
96
|
-
|
|
97
|
-
## API Integration
|
|
98
|
-
|
|
99
|
-
The plugin uses the CSCS-DWDI API endpoints:
|
|
100
|
-
|
|
101
|
-
- `GET /api/v1/compute/usage-month-multiaccount` - Primary endpoint for monthly usage data
|
|
102
|
-
- Authentication via OIDC Bearer tokens
|
|
103
|
-
|
|
104
|
-
### Authentication
|
|
105
|
-
|
|
106
|
-
The plugin uses OAuth2/OIDC authentication with the following requirements:
|
|
107
|
-
|
|
108
|
-
- Requires `cscs_dwdi_oidc_token_url` in backend settings
|
|
109
|
-
- Uses OAuth2 `client_credentials` grant flow
|
|
110
|
-
- Automatically handles token caching and renewal
|
|
111
|
-
- Includes 5-minute safety margin for token expiry
|
|
112
|
-
- Fails with proper error logging if OIDC configuration is missing
|
|
113
|
-
|
|
114
|
-
### Data Processing
|
|
115
|
-
|
|
116
|
-
1. **Account Filtering**: Only reports on accounts that match Waldur resource backend IDs
|
|
117
|
-
2. **User Aggregation**: Combines usage for the same user across different dates and clusters
|
|
118
|
-
3. **Time Range**: Automatically queries from the first day of the current month to today
|
|
119
|
-
4. **Precision**: Rounds node-hours to 2 decimal places
|
|
120
|
-
|
|
121
|
-
## Installation
|
|
122
|
-
|
|
123
|
-
This plugin is part of the Waldur Site Agent workspace. To install:
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
# Install all workspace packages including cscs-dwdi plugin
|
|
127
|
-
uv sync --all-packages
|
|
128
|
-
|
|
129
|
-
# Install specific plugin for development
|
|
130
|
-
uv sync --extra cscs-dwdi
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## Testing
|
|
134
|
-
|
|
135
|
-
Run the plugin tests:
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
# Run CSCS-DWDI plugin tests
|
|
139
|
-
uv run pytest plugins/cscs-dwdi/tests/
|
|
140
|
-
|
|
141
|
-
# Run with coverage
|
|
142
|
-
uv run pytest plugins/cscs-dwdi/tests/ --cov=waldur_site_agent_cscs_dwdi
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
## Limitations
|
|
146
|
-
|
|
147
|
-
This is a **reporting-only backend** that does not support:
|
|
148
|
-
|
|
149
|
-
- Account creation or deletion
|
|
150
|
-
- User management
|
|
151
|
-
- Resource limit management
|
|
152
|
-
- Order processing
|
|
153
|
-
- Membership synchronization
|
|
154
|
-
|
|
155
|
-
For these operations, use a different backend (e.g., SLURM) in combination with the CSCS-DWDI reporting backend:
|
|
156
|
-
|
|
157
|
-
```yaml
|
|
158
|
-
offerings:
|
|
159
|
-
- name: "Mixed Backend Offering"
|
|
160
|
-
order_processing_backend: "slurm" # Use SLURM for orders
|
|
161
|
-
reporting_backend: "cscs-dwdi" # Use CSCS-DWDI for reporting
|
|
162
|
-
membership_sync_backend: "slurm" # Use SLURM for membership
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
## Error Handling
|
|
166
|
-
|
|
167
|
-
The plugin includes comprehensive error handling:
|
|
168
|
-
|
|
169
|
-
- **API Connectivity**: Ping checks verify API availability
|
|
170
|
-
- **Authentication**: Token refresh and error handling
|
|
171
|
-
- **Data Validation**: Validates API responses and filters invalid data
|
|
172
|
-
- **Retry Logic**: Uses the framework's built-in retry mechanisms
|
|
173
|
-
|
|
174
|
-
## Development
|
|
175
|
-
|
|
176
|
-
### Project Structure
|
|
177
|
-
|
|
178
|
-
```text
|
|
179
|
-
plugins/cscs-dwdi/
|
|
180
|
-
├── pyproject.toml # Plugin configuration
|
|
181
|
-
├── README.md # This documentation
|
|
182
|
-
├── waldur_site_agent_cscs_dwdi/
|
|
183
|
-
│ ├── __init__.py # Package init
|
|
184
|
-
│ ├── backend.py # Main backend implementation
|
|
185
|
-
│ └── client.py # CSCS-DWDI API client
|
|
186
|
-
└── tests/
|
|
187
|
-
└── test_cscs_dwdi.py # Plugin tests
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### Key Classes
|
|
191
|
-
|
|
192
|
-
- **`CSCSDWDIBackend`**: Main backend class implementing reporting functionality
|
|
193
|
-
- **`CSCSDWDIClient`**: HTTP client for CSCS-DWDI API communication
|
|
194
|
-
|
|
195
|
-
### Extension Points
|
|
196
|
-
|
|
197
|
-
To extend the plugin:
|
|
198
|
-
|
|
199
|
-
1. **Additional Endpoints**: Modify `CSCSDWDIClient` to support more API endpoints
|
|
200
|
-
2. **Authentication Methods**: Update authentication logic in `client.py`
|
|
201
|
-
3. **Data Processing**: Enhance `_process_api_response()` for additional data formats
|
|
202
|
-
|
|
203
|
-
## Troubleshooting
|
|
204
|
-
|
|
205
|
-
### Common Issues
|
|
206
|
-
|
|
207
|
-
#### Authentication Failures
|
|
208
|
-
|
|
209
|
-
- Verify OIDC client credentials
|
|
210
|
-
- Check API URL configuration
|
|
211
|
-
- Ensure proper token scopes
|
|
212
|
-
|
|
213
|
-
#### Missing Usage Data
|
|
214
|
-
|
|
215
|
-
- Verify account names match between Waldur and CSCS-DWDI
|
|
216
|
-
- Check date ranges and API response format
|
|
217
|
-
- Review API rate limits and quotas
|
|
218
|
-
|
|
219
|
-
#### Network Connectivity
|
|
220
|
-
|
|
221
|
-
- Test API connectivity with ping functionality
|
|
222
|
-
- Verify network access from agent deployment environment
|
|
223
|
-
- Check firewall and proxy settings
|
|
224
|
-
|
|
225
|
-
### Debugging
|
|
226
|
-
|
|
227
|
-
Enable debug logging for detailed API interactions:
|
|
228
|
-
|
|
229
|
-
```python
|
|
230
|
-
import logging
|
|
231
|
-
logging.getLogger('waldur_site_agent_cscs_dwdi').setLevel(logging.DEBUG)
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
## Support
|
|
235
|
-
|
|
236
|
-
For issues and questions:
|
|
237
|
-
|
|
238
|
-
- Check the [Waldur Site Agent documentation](../../docs/)
|
|
239
|
-
- Review plugin test cases for usage examples
|
|
240
|
-
- Create issues in the project repository
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
waldur_site_agent_cscs_dwdi/__init__.py,sha256=OHO1yF5NTGt0otI-GollR_ppPXP--aUZRCdaT5-8IWw,56
|
|
2
|
-
waldur_site_agent_cscs_dwdi/backend.py,sha256=V5T9H5b9utxcD73TmACvrc4dZm-GEORLDI0C0tUD_7s,14251
|
|
3
|
-
waldur_site_agent_cscs_dwdi/client.py,sha256=aAWNqrA0oBknWge_TQIU4u8PQi5YdIZtQTZ2XPREznQ,7555
|
|
4
|
-
waldur_site_agent_cscs_dwdi-0.1.0.dist-info/METADATA,sha256=nTtebzVzy0mWYqZejNICgI5hlHFDxbsPAk0woaSABHM,7772
|
|
5
|
-
waldur_site_agent_cscs_dwdi-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
-
waldur_site_agent_cscs_dwdi-0.1.0.dist-info/entry_points.txt,sha256=U3odcX7B4NmT9a98ov4uVxlng3JqiCRb5Ux3h4H_ZFE,93
|
|
7
|
-
waldur_site_agent_cscs_dwdi-0.1.0.dist-info/RECORD,,
|
{waldur_site_agent_cscs_dwdi-0.1.0.dist-info → waldur_site_agent_cscs_dwdi-0.7.3.dist-info}/WHEEL
RENAMED
|
File without changes
|