DIRAC 9.0.0a68__py3-none-any.whl → 9.0.0a70__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.
Files changed (88) hide show
  1. DIRAC/AccountingSystem/Client/Types/Network.py +8 -8
  2. DIRAC/AccountingSystem/Client/Types/PilotSubmission.py +3 -3
  3. DIRAC/ConfigurationSystem/Client/CSAPI.py +11 -1
  4. DIRAC/ConfigurationSystem/Client/Helpers/CSGlobals.py +0 -9
  5. DIRAC/ConfigurationSystem/Client/Helpers/Registry.py +3 -29
  6. DIRAC/ConfigurationSystem/Client/SyncPlugins/CERNLDAPSyncPlugin.py +4 -1
  7. DIRAC/ConfigurationSystem/ConfigTemplate.cfg +3 -0
  8. DIRAC/ConfigurationSystem/private/Modificator.py +11 -3
  9. DIRAC/ConfigurationSystem/private/RefresherBase.py +4 -2
  10. DIRAC/Core/DISET/ServiceReactor.py +11 -3
  11. DIRAC/Core/DISET/private/Transports/M2SSLTransport.py +9 -7
  12. DIRAC/Core/Security/DiracX.py +11 -6
  13. DIRAC/Core/Security/test/test_diracx_token_from_pem.py +161 -0
  14. DIRAC/Core/Tornado/Server/TornadoService.py +1 -1
  15. DIRAC/Core/Utilities/ElasticSearchDB.py +1 -2
  16. DIRAC/Core/Utilities/Subprocess.py +66 -57
  17. DIRAC/Core/Utilities/test/Test_Profiler.py +20 -20
  18. DIRAC/Core/Utilities/test/Test_Subprocess.py +58 -8
  19. DIRAC/Core/scripts/dirac_apptainer_exec.py +8 -8
  20. DIRAC/DataManagementSystem/Agent/FTS3Agent.py +8 -7
  21. DIRAC/DataManagementSystem/Client/DataManager.py +6 -7
  22. DIRAC/DataManagementSystem/Client/FTS3Job.py +125 -34
  23. DIRAC/DataManagementSystem/Client/test/Test_FTS3Objects.py +1 -0
  24. DIRAC/DataManagementSystem/Client/test/Test_scitag.py +69 -0
  25. DIRAC/DataManagementSystem/DB/FileCatalogComponents/DatasetManager/DatasetManager.py +1 -1
  26. DIRAC/DataManagementSystem/scripts/dirac_dms_create_moving_request.py +2 -0
  27. DIRAC/FrameworkSystem/DB/InstalledComponentsDB.py +3 -2
  28. DIRAC/FrameworkSystem/DB/ProxyDB.py +9 -5
  29. DIRAC/FrameworkSystem/Utilities/MonitoringUtilities.py +1 -0
  30. DIRAC/FrameworkSystem/Utilities/TokenManagementUtilities.py +3 -2
  31. DIRAC/FrameworkSystem/Utilities/diracx.py +41 -10
  32. DIRAC/FrameworkSystem/scripts/dirac_login.py +2 -2
  33. DIRAC/FrameworkSystem/scripts/dirac_proxy_init.py +1 -1
  34. DIRAC/FrameworkSystem/scripts/dirac_uninstall_component.py +1 -0
  35. DIRAC/Interfaces/API/Dirac.py +3 -6
  36. DIRAC/Interfaces/Utilities/DConfigCache.py +2 -0
  37. DIRAC/Interfaces/scripts/dirac_wms_job_parameters.py +0 -1
  38. DIRAC/MonitoringSystem/DB/MonitoringDB.py +6 -5
  39. DIRAC/MonitoringSystem/Service/WebAppHandler.py +25 -6
  40. DIRAC/MonitoringSystem/private/MainReporter.py +0 -3
  41. DIRAC/RequestManagementSystem/Agent/RequestExecutingAgent.py +8 -6
  42. DIRAC/RequestManagementSystem/ConfigTemplate.cfg +6 -6
  43. DIRAC/ResourceStatusSystem/Command/FreeDiskSpaceCommand.py +3 -1
  44. DIRAC/Resources/Computing/AREXComputingElement.py +18 -2
  45. DIRAC/Resources/Computing/BatchSystems/Condor.py +0 -3
  46. DIRAC/Resources/Computing/BatchSystems/executeBatch.py +15 -7
  47. DIRAC/Resources/Computing/LocalComputingElement.py +0 -2
  48. DIRAC/Resources/Computing/SSHComputingElement.py +61 -38
  49. DIRAC/Resources/IdProvider/CheckInIdProvider.py +13 -0
  50. DIRAC/Resources/IdProvider/IdProviderFactory.py +13 -3
  51. DIRAC/Resources/IdProvider/tests/Test_IdProviderFactory.py +7 -0
  52. DIRAC/Resources/Storage/FileStorage.py +121 -2
  53. DIRAC/TransformationSystem/Agent/InputDataAgent.py +4 -1
  54. DIRAC/TransformationSystem/Agent/MCExtensionAgent.py +5 -2
  55. DIRAC/TransformationSystem/Agent/TaskManagerAgentBase.py +3 -4
  56. DIRAC/TransformationSystem/Agent/TransformationCleaningAgent.py +44 -9
  57. DIRAC/TransformationSystem/Agent/ValidateOutputDataAgent.py +4 -2
  58. DIRAC/TransformationSystem/Client/TransformationClient.py +9 -1
  59. DIRAC/TransformationSystem/Client/Utilities.py +6 -3
  60. DIRAC/TransformationSystem/DB/TransformationDB.py +105 -43
  61. DIRAC/TransformationSystem/Utilities/ReplicationCLIParameters.py +3 -3
  62. DIRAC/TransformationSystem/scripts/dirac_production_runjoblocal.py +2 -4
  63. DIRAC/TransformationSystem/test/Test_replicationTransformation.py +5 -6
  64. DIRAC/WorkloadManagementSystem/Agent/JobAgent.py +1 -5
  65. DIRAC/WorkloadManagementSystem/Agent/PilotSyncAgent.py +4 -3
  66. DIRAC/WorkloadManagementSystem/Agent/PushJobAgent.py +0 -4
  67. DIRAC/WorkloadManagementSystem/Agent/SiteDirector.py +8 -11
  68. DIRAC/WorkloadManagementSystem/Agent/StalledJobAgent.py +39 -7
  69. DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_SiteDirector.py +8 -2
  70. DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_StalledJobAgent.py +24 -4
  71. DIRAC/WorkloadManagementSystem/Client/DownloadInputData.py +4 -3
  72. DIRAC/WorkloadManagementSystem/ConfigTemplate.cfg +3 -3
  73. DIRAC/WorkloadManagementSystem/DB/JobParametersDB.py +8 -8
  74. DIRAC/WorkloadManagementSystem/DB/SandboxMetadataDB.py +1 -1
  75. DIRAC/WorkloadManagementSystem/DB/StatusUtils.py +48 -21
  76. DIRAC/WorkloadManagementSystem/DB/tests/Test_StatusUtils.py +19 -4
  77. DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py +3 -4
  78. DIRAC/WorkloadManagementSystem/JobWrapper/Watchdog.py +16 -45
  79. DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapper.py +18 -9
  80. DIRAC/WorkloadManagementSystem/Service/JobManagerHandler.py +25 -2
  81. DIRAC/WorkloadManagementSystem/Service/WMSAdministratorHandler.py +18 -31
  82. DIRAC/WorkloadManagementSystem/Utilities/PilotCStoJSONSynchronizer.py +73 -7
  83. {dirac-9.0.0a68.dist-info → dirac-9.0.0a70.dist-info}/METADATA +6 -5
  84. {dirac-9.0.0a68.dist-info → dirac-9.0.0a70.dist-info}/RECORD +88 -86
  85. {dirac-9.0.0a68.dist-info → dirac-9.0.0a70.dist-info}/WHEEL +0 -0
  86. {dirac-9.0.0a68.dist-info → dirac-9.0.0a70.dist-info}/entry_points.txt +0 -0
  87. {dirac-9.0.0a68.dist-info → dirac-9.0.0a70.dist-info}/licenses/LICENSE +0 -0
  88. {dirac-9.0.0a68.dist-info → dirac-9.0.0a70.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- """ Accounting class to stores network metrics gathered by perfSONARs.
1
+ """Accounting class to stores network metrics gathered by perfSONARs.
2
2
 
3
- Filled by "Accounting/Network" agent
3
+ Filled by "Accounting/Network" agent
4
4
  """
5
5
 
6
6
  from DIRAC.AccountingSystem.Client.Types.BaseAccountingType import BaseAccountingType
@@ -16,12 +16,12 @@ class Network(BaseAccountingType):
16
16
 
17
17
  # IPv6 address has up to 45 chars
18
18
  self.definitionKeyFields = [
19
- ("SourceIP", "VARCHAR(50)"),
20
- ("DestinationIP", "VARCHAR(50)"),
21
- ("SourceHostName", "VARCHAR(50)"),
22
- ("DestinationHostName", "VARCHAR(50)"),
23
- ("Source", "VARCHAR(50)"),
24
- ("Destination", "VARCHAR(50)"),
19
+ ("SourceIP", "VARCHAR(64)"),
20
+ ("DestinationIP", "VARCHAR(64)"),
21
+ ("SourceHostName", "VARCHAR(255)"),
22
+ ("DestinationHostName", "VARCHAR(255)"),
23
+ ("Source", "VARCHAR(255)"),
24
+ ("Destination", "VARCHAR(255)"),
25
25
  ]
26
26
 
27
27
  self.definitionAccountingFields = [
@@ -1,6 +1,6 @@
1
- """ Accounting Type for Pilot Submission
1
+ """Accounting Type for Pilot Submission
2
2
 
3
- Filled by the "WorkloadManagement/SiteDirector" agent(s)
3
+ Filled by the "WorkloadManagement/SiteDirector" agent(s)
4
4
  """
5
5
 
6
6
  from DIRAC.AccountingSystem.Client.Types.BaseAccountingType import BaseAccountingType
@@ -13,7 +13,7 @@ class PilotSubmission(BaseAccountingType):
13
13
  super().__init__()
14
14
 
15
15
  self.definitionKeyFields = [
16
- ("HostName", "VARCHAR(100)"),
16
+ ("HostName", "VARCHAR(255)"),
17
17
  ("SiteDirector", "VARCHAR(100)"),
18
18
  ("Site", "VARCHAR(100)"),
19
19
  ("CE", "VARCHAR(100)"),
@@ -456,13 +456,23 @@ class CSAPI:
456
456
  gLogger.error("User is not registered: ", repr(username))
457
457
  return S_OK(False)
458
458
  for prop in properties:
459
- if prop == "Groups":
459
+ if prop in ["Groups", "AffiliationEnds"]:
460
460
  continue
461
461
  prevVal = self.__csMod.getValue(f"{self.__baseSecurity}/Users/{username}/{prop}")
462
462
  if not prevVal or prevVal != properties[prop]:
463
463
  gLogger.info(f"Setting {prop} property for user {username} to {properties[prop]}")
464
464
  self.__csMod.setOptionValue(f"{self.__baseSecurity}/Users/{username}/{prop}", properties[prop])
465
465
  modifiedUser = True
466
+ if properties.get("AffiliationEnds", None):
467
+ user_affiliationends_section = f"{self.__baseSecurity}/Users/{username}/AffiliationEnds"
468
+ # add the section for AffiliationEnds
469
+ res = gConfig.getSections(user_affiliationends_section)
470
+ if not res["OK"]:
471
+ self.__csMod.createSection(user_affiliationends_section)
472
+ # now set value VO = end date
473
+ for vo, end_date in properties["AffiliationEnds"].items():
474
+ self.__csMod.setOptionValue(f"{user_affiliationends_section}/{vo}", end_date)
475
+ modifiedUser = True
466
476
  if "Groups" in properties:
467
477
  result = self.listGroups()
468
478
  if not result["OK"]:
@@ -4,15 +4,6 @@ Some Helper functions to retrieve common location from the CS
4
4
  from DIRAC.Core.Utilities.Extensions import extensionsByPriority
5
5
 
6
6
 
7
- def getSetup() -> str:
8
- """
9
- Return setup name
10
- """
11
- from DIRAC import gConfig
12
-
13
- return gConfig.getValue("/DIRAC/Setup", "")
14
-
15
-
16
7
  def getVO(defaultVO: str = "") -> str:
17
8
  """
18
9
  Return VO from configuration
@@ -3,18 +3,14 @@
3
3
  import errno
4
4
  import inspect
5
5
  import sys
6
-
7
- from threading import Lock
8
6
  from collections.abc import Iterable
7
+ from threading import Lock
8
+ from typing import Optional
9
9
 
10
10
  from cachetools import TTLCache, cached
11
11
  from cachetools.keys import hashkey
12
12
 
13
-
14
- from typing import Optional
15
- from collections.abc import Iterable
16
-
17
- from DIRAC import S_OK, S_ERROR
13
+ from DIRAC import S_ERROR, S_OK
18
14
  from DIRAC.ConfigurationSystem.Client.Config import gConfig
19
15
  from DIRAC.ConfigurationSystem.Client.Helpers.CSGlobals import getVO
20
16
 
@@ -488,28 +484,6 @@ def getVOMSAttributeForGroup(group):
488
484
  return gConfig.getValue(f"{gBaseRegistrySection}/Groups/{group}/VOMSRole", getDefaultVOMSAttribute())
489
485
 
490
486
 
491
- def getDefaultVOMSVO():
492
- """Get default VOMS VO
493
-
494
- :return: str
495
- """
496
- return gConfig.getValue(f"{gBaseRegistrySection}/DefaultVOMSVO", "") or getVO()
497
-
498
-
499
- def getVOMSVOForGroup(group):
500
- """Search VOMS VO for group
501
-
502
- :param str group: group name
503
-
504
- :return: str
505
- """
506
- vomsVO = gConfig.getValue(f"{gBaseRegistrySection}/Groups/{group}/VOMSVO", getDefaultVOMSVO())
507
- if not vomsVO:
508
- vo = getVOForGroup(group)
509
- vomsVO = getVOOption(vo, "VOMSName", "")
510
- return vomsVO
511
-
512
-
513
487
  def getGroupsWithVOMSAttribute(vomsAttr):
514
488
  """Search groups with VOMS attribute
515
489
 
@@ -32,6 +32,9 @@ class CERNLDAPSyncPlugin:
32
32
  else:
33
33
  userDict["PrimaryCERNAccount"] = self._findOwnerAccountName(username, attributes)
34
34
 
35
+ if userDict["CERNAccountType"] in ["Primary", "Secondary"]:
36
+ userDict["CERNPersonId"] = attributes.get("employeeId", [None])[0]
37
+
35
38
  def _findOwnerAccountName(self, username, attributes):
36
39
  """Find the owner account from a CERN LDAP entry.
37
40
 
@@ -64,7 +67,7 @@ class CERNLDAPSyncPlugin:
64
67
  status, result, response, _ = self._connection.search(
65
68
  "OU=Users,OU=Organic Units,DC=cern,DC=ch",
66
69
  f"(CN={commonName})",
67
- attributes=["cernAccountOwner", "cernAccountType"],
70
+ attributes=["cernAccountOwner", "cernAccountType", "employeeId"],
68
71
  )
69
72
  if not status:
70
73
  raise ValueError(f"Bad status from LDAP search: {result}")
@@ -6,6 +6,9 @@ Services
6
6
  {
7
7
  HandlerPath = DIRAC/ConfigurationSystem/Service/TornadoConfigurationHandler.py
8
8
  Port = 9135
9
+ # Set to True to make a DiracX model validation on commits
10
+ # If validation fails, commit will also failed
11
+ VerifyDiracXSyncOnCommit = False
9
12
  # Subsection to configure authorization over the service
10
13
  Authorization
11
14
  {
@@ -1,13 +1,16 @@
1
1
  """ This is the guy that actually modifies the content of the CS
2
2
  """
3
- import zlib
4
- import difflib
5
3
  import datetime
4
+ import difflib
5
+ import zlib
6
6
 
7
7
  from diraccfg import CFG
8
- from DIRAC.Core.Utilities import List
8
+
9
+ from DIRAC import S_ERROR
9
10
  from DIRAC.ConfigurationSystem.Client.ConfigurationData import gConfigurationData
10
11
  from DIRAC.Core.Security.ProxyInfo import getProxyInfo
12
+ from DIRAC.Core.Utilities import List
13
+ from DIRAC.FrameworkSystem.Utilities.diracx import diracxVerifyConfig
11
14
 
12
15
 
13
16
  class Modificator:
@@ -239,6 +242,11 @@ class Modificator:
239
242
  return str(self.cfgData)
240
243
 
241
244
  def commit(self):
245
+ retOpt = self.getValue("/Systems/Configuration/Services/Server/VerifyDiracXSyncOnCommit")
246
+ if retOpt == "True":
247
+ resVerif = diracxVerifyConfig(self.cfgData)
248
+ if not resVerif["OK"]:
249
+ return S_ERROR(resVerif["Message"])
242
250
  compressedData = zlib.compress(str(self.cfgData).encode(), 9)
243
251
  return self.rpcClient.commitNewData(compressedData)
244
252
 
@@ -1,7 +1,7 @@
1
1
  import time
2
2
 
3
3
  from DIRAC.ConfigurationSystem.Client.ConfigurationData import gConfigurationData
4
- from DIRAC.ConfigurationSystem.Client.PathFinder import getGatewayURLs
4
+ from DIRAC.ConfigurationSystem.Client.PathFinder import getGatewayURLs, groupURLsByPriority
5
5
  from DIRAC.Core.Utilities import List
6
6
  from DIRAC.Core.Utilities.EventDispatcher import gEventDispatcher
7
7
  from DIRAC.Core.Utilities.ReturnValues import S_ERROR, S_OK
@@ -138,7 +138,9 @@ class RefresherBase:
138
138
  if not initialServerList:
139
139
  return S_OK()
140
140
 
141
- randomServerList = List.randomize(initialServerList)
141
+ randomServerList = []
142
+ for urlGroup in groupURLsByPriority(initialServerList):
143
+ randomServerList.extend(List.randomize(urlGroup))
142
144
  gLogger.debug(f"Randomized server list is {', '.join(randomServerList)}")
143
145
 
144
146
  for sServer in randomServerList:
@@ -200,6 +200,7 @@ class ServiceReactor:
200
200
  services at the same time
201
201
  """
202
202
  sel = self.__getListeningSelector(svcName)
203
+ throttleExpires = None
203
204
  while self.__alive:
204
205
  clientTransport = None
205
206
  try:
@@ -223,12 +224,19 @@ class ServiceReactor:
223
224
  gLogger.warn(f"Client connected from banned ip {clientIP}")
224
225
  clientTransport.close()
225
226
  continue
227
+ # Handle throttling
228
+ if self.__services[svcName].wantsThrottle and throttleExpires is None:
229
+ throttleExpires = time.time() + THROTTLE_SERVICE_SLEEP_SECONDS
230
+ if throttleExpires:
231
+ if time.time() > throttleExpires:
232
+ throttleExpires = None
233
+ else:
234
+ gLogger.warn("Rejecting client due to throttling", str(clientTransport.getRemoteAddress()))
235
+ clientTransport.close()
236
+ continue
226
237
  # Handle connection
227
238
  self.__stats.connectionStablished()
228
239
  self.__services[svcName].handleConnection(clientTransport)
229
- while self.__services[svcName].wantsThrottle:
230
- gLogger.warn("Sleeping as service requested throttling", svcName)
231
- time.sleep(THROTTLE_SERVICE_SLEEP_SECONDS)
232
240
  # Renew context?
233
241
  now = time.time()
234
242
  renewed = False
@@ -110,19 +110,18 @@ class SSLTransport(BaseTransport):
110
110
  if self.serverMode():
111
111
  raise RuntimeError("SSLTransport is in server mode.")
112
112
 
113
- error = None
113
+ errors = []
114
114
  host, port = self.stServerAddress
115
115
 
116
116
  # The following piece of code was inspired by the python socket documentation
117
117
  # as well as the implementation of M2Crypto.httpslib.HTTPSConnection
118
118
 
119
- # We ignore the returned sockaddr because SSL.Connection.connect needs
120
- # a host name.
119
+ # Get all available addresses (IPv6 and IPv4) and try them in order
121
120
  try:
122
121
  addrInfoList = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
123
122
  except OSError as e:
124
123
  return S_ERROR(f"DNS lookup failed {e!r}")
125
- for family, _socketType, _proto, _canonname, _socketAddress in addrInfoList:
124
+ for family, _socketType, _proto, _canonname, socketAddress in addrInfoList:
126
125
  try:
127
126
  self.oSocket = SSL.Connection(self.__ctx, family=family)
128
127
 
@@ -138,7 +137,10 @@ class SSLTransport(BaseTransport):
138
137
  # set SNI server name since we know it at this point
139
138
  self.oSocket.set_tlsext_host_name(host)
140
139
 
141
- self.oSocket.connect((host, port))
140
+ # tell the connection which host we are connecting to so we can
141
+ # use the address we obtained from DNS
142
+ self.oSocket.set1_host(host)
143
+ self.oSocket.connect(socketAddress)
142
144
 
143
145
  # Once the connection is established, we can use the timeout
144
146
  # asked for RPC
@@ -151,12 +153,12 @@ class SSLTransport(BaseTransport):
151
153
  # They should be propagated upwards and caught by the BaseClient
152
154
  # not to enter the retry loop
153
155
  except OSError as e:
154
- error = f"{e}:{repr(e)}"
156
+ errors.append(f"{socketAddress} {e}:{repr(e)}")
155
157
 
156
158
  if self.oSocket is not None:
157
159
  self.close()
158
160
 
159
- return S_ERROR(error)
161
+ return S_ERROR("; ".join(errors))
160
162
 
161
163
  def initAsServer(self):
162
164
  """Prepare this server socket for use."""
@@ -40,7 +40,7 @@ from DIRAC.Core.Utilities.ReturnValues import convertToReturnValue, returnValueO
40
40
 
41
41
  PEM_BEGIN = "-----BEGIN DIRACX-----"
42
42
  PEM_END = "-----END DIRACX-----"
43
- RE_DIRACX_PEM = re.compile(rf"{PEM_BEGIN}\n(.*)\n{PEM_END}", re.MULTILINE | re.DOTALL)
43
+ RE_DIRACX_PEM = re.compile(rf"{PEM_BEGIN}\n(.*?)\n{PEM_END}", re.DOTALL)
44
44
 
45
45
 
46
46
  @convertToReturnValue
@@ -62,21 +62,26 @@ def addTokenToPEM(pemPath, group):
62
62
  token_type=token_content.get("token_type"),
63
63
  refresh_token=token_content.get("refresh_token"),
64
64
  )
65
-
66
65
  token_pem = f"{PEM_BEGIN}\n"
67
66
  data = base64.b64encode(serialize_credentials(token).encode("utf-8")).decode()
68
67
  token_pem += textwrap.fill(data, width=64)
69
68
  token_pem += f"\n{PEM_END}\n"
70
69
 
71
- with open(pemPath, "a") as f:
72
- f.write(token_pem)
70
+ pem = Path(pemPath).read_text()
71
+ # Remove any existing DiracX token there would be
72
+ new_pem = re.sub(RE_DIRACX_PEM, "", pem)
73
+ new_pem += token_pem
74
+
75
+ Path(pemPath).write_text(new_pem)
73
76
 
74
77
 
75
78
  def diracxTokenFromPEM(pemPath) -> dict[str, Any] | None:
76
79
  """Extract the DiracX token from the proxy PEM file"""
77
80
  pem = Path(pemPath).read_text()
78
- if match := RE_DIRACX_PEM.search(pem):
79
- match = match.group(1)
81
+ if match := RE_DIRACX_PEM.findall(pem):
82
+ if len(match) > 1:
83
+ raise ValueError("Found multiple DiracX tokens, this should never happen")
84
+ match = match[0]
80
85
  return json.loads(base64.b64decode(match).decode("utf-8"))
81
86
 
82
87
 
@@ -0,0 +1,161 @@
1
+ import base64
2
+ import json
3
+ import pytest
4
+ import tempfile
5
+ from pathlib import Path
6
+ from unittest.mock import patch, mock_open
7
+
8
+ from DIRAC.Core.Security.DiracX import diracxTokenFromPEM, PEM_BEGIN, PEM_END, RE_DIRACX_PEM
9
+
10
+
11
+ class TestDiracxTokenFromPEM:
12
+ """Test cases for diracxTokenFromPEM function"""
13
+
14
+ def create_valid_token_data(self):
15
+ """Create valid token data for testing"""
16
+ return {
17
+ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.test",
18
+ "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.refresh",
19
+ "expires_in": 3600,
20
+ "token_type": "Bearer",
21
+ }
22
+
23
+ def create_pem_content(self, token_data=None, include_other_content=True):
24
+ """Create PEM content with embedded DiracX token"""
25
+ if token_data is None:
26
+ token_data = self.create_valid_token_data()
27
+
28
+ # Encode token data
29
+ token_json = json.dumps(token_data)
30
+ encoded_token = base64.b64encode(token_json.encode("utf-8")).decode()
31
+
32
+ # Create PEM content
33
+ pem_content = ""
34
+ if include_other_content:
35
+ pem_content += "-----BEGIN CERTIFICATE-----\n"
36
+ pem_content += "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n"
37
+ pem_content += "-----END CERTIFICATE-----\n"
38
+
39
+ pem_content += f"{PEM_BEGIN}\n"
40
+ pem_content += encoded_token + "\n"
41
+ pem_content += f"{PEM_END}\n"
42
+
43
+ return pem_content
44
+
45
+ def test_valid_token_extraction(self):
46
+ """Test successful extraction of valid token from PEM file"""
47
+ token_data = self.create_valid_token_data()
48
+ pem_content = self.create_pem_content(token_data)
49
+
50
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".pem") as f:
51
+ f.write(pem_content)
52
+ temp_path = f.name
53
+
54
+ try:
55
+ result = diracxTokenFromPEM(temp_path)
56
+ assert result == token_data
57
+ finally:
58
+ Path(temp_path).unlink()
59
+
60
+ def test_no_token_in_pem(self):
61
+ """Test behavior when no DiracX token is present in PEM file"""
62
+ pem_content = """-----BEGIN CERTIFICATE-----
63
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
64
+ -----END CERTIFICATE-----"""
65
+
66
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".pem") as f:
67
+ f.write(pem_content)
68
+ temp_path = f.name
69
+
70
+ try:
71
+ result = diracxTokenFromPEM(temp_path)
72
+ assert result is None
73
+ finally:
74
+ Path(temp_path).unlink()
75
+
76
+ def test_multiple_tokens_error(self):
77
+ """Test that multiple tokens raise ValueError"""
78
+ token_data = self.create_valid_token_data()
79
+
80
+ # Create PEM with two tokens
81
+ pem_content = self.create_pem_content(token_data)
82
+ pem_content += "\n" + self.create_pem_content(token_data, include_other_content=False)
83
+
84
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".pem") as f:
85
+ f.write(pem_content)
86
+ temp_path = f.name
87
+
88
+ try:
89
+ with pytest.raises(ValueError, match="Found multiple DiracX tokens"):
90
+ diracxTokenFromPEM(temp_path)
91
+ finally:
92
+ Path(temp_path).unlink()
93
+
94
+ def test_malformed_base64(self):
95
+ """Test behavior with malformed base64 data"""
96
+ pem_content = f"""{PEM_BEGIN}
97
+ invalid_base64_data_that_will_cause_error!
98
+ {PEM_END}"""
99
+
100
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".pem") as f:
101
+ f.write(pem_content)
102
+ temp_path = f.name
103
+
104
+ try:
105
+ with pytest.raises(Exception): # base64.b64decode will raise an exception
106
+ diracxTokenFromPEM(temp_path)
107
+ finally:
108
+ Path(temp_path).unlink()
109
+
110
+ def test_invalid_json_in_token(self):
111
+ """Test behavior with invalid JSON in token data"""
112
+ invalid_json = "this is not valid json"
113
+ encoded_invalid = base64.b64encode(invalid_json.encode("utf-8")).decode()
114
+
115
+ pem_content = f"""{PEM_BEGIN}
116
+ {encoded_invalid}
117
+ {PEM_END}"""
118
+
119
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".pem") as f:
120
+ f.write(pem_content)
121
+ temp_path = f.name
122
+
123
+ try:
124
+ with pytest.raises(json.JSONDecodeError):
125
+ diracxTokenFromPEM(temp_path)
126
+ finally:
127
+ Path(temp_path).unlink()
128
+
129
+ def test_token_with_unicode_characters(self):
130
+ """Test token with unicode characters"""
131
+ unicode_token = {
132
+ "access_token": "token_with_unicode_ñ_é_ü",
133
+ "refresh_token": "refresh_with_emoji_🚀_🎉",
134
+ "expires_in": 3600,
135
+ "token_type": "Bearer",
136
+ }
137
+
138
+ pem_content = self.create_pem_content(unicode_token)
139
+
140
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".pem") as f:
141
+ f.write(pem_content)
142
+ temp_path = f.name
143
+
144
+ try:
145
+ result = diracxTokenFromPEM(temp_path)
146
+ assert result == unicode_token
147
+ finally:
148
+ Path(temp_path).unlink()
149
+
150
+ def test_regex_pattern_validation(self):
151
+ """Test that the regex pattern correctly identifies DiracX tokens"""
152
+ # Test that the regex matches the expected pattern
153
+ token_data = self.create_valid_token_data()
154
+ token_json = json.dumps(token_data)
155
+ encoded_token = base64.b64encode(token_json.encode("utf-8")).decode()
156
+
157
+ test_content = f"{PEM_BEGIN}\n{encoded_token}\n{PEM_END}"
158
+ matches = RE_DIRACX_PEM.findall(test_content)
159
+
160
+ assert len(matches) == 1
161
+ assert matches[0] == encoded_token
@@ -73,7 +73,7 @@ class TornadoService(BaseRequestHandler): # pylint: disable=abstract-method
73
73
  :py:class:`BaseRequestHandler <DIRAC.Core.Tornado.Server.private.BaseRequestHandler.BaseRequestHandler>` for more details.
74
74
 
75
75
  In order to pass information around and keep some states, we use instance attributes.
76
- These are initialized in the :py:meth:`.initialize` method.
76
+ These are initialized in the ``initialize`` methods.
77
77
 
78
78
  The handler only define the ``post`` verb. Please refer to :py:meth:`.post` for the details.
79
79
 
@@ -82,7 +82,6 @@ def generateDocs(data, withTimeStamp=True):
82
82
 
83
83
 
84
84
  class ElasticSearchDB:
85
-
86
85
  """
87
86
  .. class:: ElasticSearchDB
88
87
 
@@ -506,7 +505,7 @@ class ElasticSearchDB:
506
505
  indexName = self.generateFullIndexName(indexPrefix, period)
507
506
  else:
508
507
  indexName = indexPrefix
509
- sLog.debug(f"Bulk indexing into {indexName} of {data}")
508
+ sLog.debug(f"Bulk indexing into {indexName} of {len(data)}")
510
509
 
511
510
  res = self.existingIndex(indexName)
512
511
  if not res["OK"]: