waldur-site-agent-cscs-dwdi 0.7.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.

@@ -1,4 +1,4 @@
1
- """CSCS-DWDI backend implementation for usage reporting."""
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 CSCSDWDIBackend(BaseBackend):
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
- with httpx.Client() as client:
95
- response = client.post(
96
- self.oidc_token_url, data=token_data, headers=headers, timeout=30.0
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
- with httpx.Client() as client:
164
- response = client.get(url, params=params, headers=headers, timeout=30.0)
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
- with httpx.Client() as client:
211
- response = client.get(url, params=params, headers=headers, timeout=30.0)
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
- with httpx.Client() as client:
235
- response = client.get(url, params=params, headers=headers, timeout=10.0)
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("Ping failed")
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,,
@@ -0,0 +1,3 @@
1
+ [waldur_site_agent.backends]
2
+ cscs-dwdi-compute = waldur_site_agent_cscs_dwdi.backend:CSCSDWDIComputeBackend
3
+ cscs-dwdi-storage = waldur_site_agent_cscs_dwdi.backend:CSCSDWDIStorageBackend
@@ -1,240 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: waldur-site-agent-cscs-dwdi
3
- Version: 0.7.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.7.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.7.0.dist-info/METADATA,sha256=PwteVYnSaM4f4_Uew3r0utcZtNlyYfamtlZnJHufJak,7772
5
- waldur_site_agent_cscs_dwdi-0.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- waldur_site_agent_cscs_dwdi-0.7.0.dist-info/entry_points.txt,sha256=U3odcX7B4NmT9a98ov4uVxlng3JqiCRb5Ux3h4H_ZFE,93
7
- waldur_site_agent_cscs_dwdi-0.7.0.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [waldur_site_agent.backends]
2
- cscs-dwdi = waldur_site_agent_cscs_dwdi.backend:CSCSDWDIBackend