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.
- DIRAC/AccountingSystem/Client/Types/Network.py +8 -8
- DIRAC/AccountingSystem/Client/Types/PilotSubmission.py +3 -3
- DIRAC/ConfigurationSystem/Client/CSAPI.py +11 -1
- DIRAC/ConfigurationSystem/Client/Helpers/CSGlobals.py +0 -9
- DIRAC/ConfigurationSystem/Client/Helpers/Registry.py +3 -29
- DIRAC/ConfigurationSystem/Client/SyncPlugins/CERNLDAPSyncPlugin.py +4 -1
- DIRAC/ConfigurationSystem/ConfigTemplate.cfg +3 -0
- DIRAC/ConfigurationSystem/private/Modificator.py +11 -3
- DIRAC/ConfigurationSystem/private/RefresherBase.py +4 -2
- DIRAC/Core/DISET/ServiceReactor.py +11 -3
- DIRAC/Core/DISET/private/Transports/M2SSLTransport.py +9 -7
- DIRAC/Core/Security/DiracX.py +11 -6
- DIRAC/Core/Security/test/test_diracx_token_from_pem.py +161 -0
- DIRAC/Core/Tornado/Server/TornadoService.py +1 -1
- DIRAC/Core/Utilities/ElasticSearchDB.py +1 -2
- DIRAC/Core/Utilities/Subprocess.py +66 -57
- DIRAC/Core/Utilities/test/Test_Profiler.py +20 -20
- DIRAC/Core/Utilities/test/Test_Subprocess.py +58 -8
- DIRAC/Core/scripts/dirac_apptainer_exec.py +8 -8
- DIRAC/DataManagementSystem/Agent/FTS3Agent.py +8 -7
- DIRAC/DataManagementSystem/Client/DataManager.py +6 -7
- DIRAC/DataManagementSystem/Client/FTS3Job.py +125 -34
- DIRAC/DataManagementSystem/Client/test/Test_FTS3Objects.py +1 -0
- DIRAC/DataManagementSystem/Client/test/Test_scitag.py +69 -0
- DIRAC/DataManagementSystem/DB/FileCatalogComponents/DatasetManager/DatasetManager.py +1 -1
- DIRAC/DataManagementSystem/scripts/dirac_dms_create_moving_request.py +2 -0
- DIRAC/FrameworkSystem/DB/InstalledComponentsDB.py +3 -2
- DIRAC/FrameworkSystem/DB/ProxyDB.py +9 -5
- DIRAC/FrameworkSystem/Utilities/MonitoringUtilities.py +1 -0
- DIRAC/FrameworkSystem/Utilities/TokenManagementUtilities.py +3 -2
- DIRAC/FrameworkSystem/Utilities/diracx.py +41 -10
- DIRAC/FrameworkSystem/scripts/dirac_login.py +2 -2
- DIRAC/FrameworkSystem/scripts/dirac_proxy_init.py +1 -1
- DIRAC/FrameworkSystem/scripts/dirac_uninstall_component.py +1 -0
- DIRAC/Interfaces/API/Dirac.py +3 -6
- DIRAC/Interfaces/Utilities/DConfigCache.py +2 -0
- DIRAC/Interfaces/scripts/dirac_wms_job_parameters.py +0 -1
- DIRAC/MonitoringSystem/DB/MonitoringDB.py +6 -5
- DIRAC/MonitoringSystem/Service/WebAppHandler.py +25 -6
- DIRAC/MonitoringSystem/private/MainReporter.py +0 -3
- DIRAC/RequestManagementSystem/Agent/RequestExecutingAgent.py +8 -6
- DIRAC/RequestManagementSystem/ConfigTemplate.cfg +6 -6
- DIRAC/ResourceStatusSystem/Command/FreeDiskSpaceCommand.py +3 -1
- DIRAC/Resources/Computing/AREXComputingElement.py +18 -2
- DIRAC/Resources/Computing/BatchSystems/Condor.py +0 -3
- DIRAC/Resources/Computing/BatchSystems/executeBatch.py +15 -7
- DIRAC/Resources/Computing/LocalComputingElement.py +0 -2
- DIRAC/Resources/Computing/SSHComputingElement.py +61 -38
- DIRAC/Resources/IdProvider/CheckInIdProvider.py +13 -0
- DIRAC/Resources/IdProvider/IdProviderFactory.py +13 -3
- DIRAC/Resources/IdProvider/tests/Test_IdProviderFactory.py +7 -0
- DIRAC/Resources/Storage/FileStorage.py +121 -2
- DIRAC/TransformationSystem/Agent/InputDataAgent.py +4 -1
- DIRAC/TransformationSystem/Agent/MCExtensionAgent.py +5 -2
- DIRAC/TransformationSystem/Agent/TaskManagerAgentBase.py +3 -4
- DIRAC/TransformationSystem/Agent/TransformationCleaningAgent.py +44 -9
- DIRAC/TransformationSystem/Agent/ValidateOutputDataAgent.py +4 -2
- DIRAC/TransformationSystem/Client/TransformationClient.py +9 -1
- DIRAC/TransformationSystem/Client/Utilities.py +6 -3
- DIRAC/TransformationSystem/DB/TransformationDB.py +105 -43
- DIRAC/TransformationSystem/Utilities/ReplicationCLIParameters.py +3 -3
- DIRAC/TransformationSystem/scripts/dirac_production_runjoblocal.py +2 -4
- DIRAC/TransformationSystem/test/Test_replicationTransformation.py +5 -6
- DIRAC/WorkloadManagementSystem/Agent/JobAgent.py +1 -5
- DIRAC/WorkloadManagementSystem/Agent/PilotSyncAgent.py +4 -3
- DIRAC/WorkloadManagementSystem/Agent/PushJobAgent.py +0 -4
- DIRAC/WorkloadManagementSystem/Agent/SiteDirector.py +8 -11
- DIRAC/WorkloadManagementSystem/Agent/StalledJobAgent.py +39 -7
- DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_SiteDirector.py +8 -2
- DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_StalledJobAgent.py +24 -4
- DIRAC/WorkloadManagementSystem/Client/DownloadInputData.py +4 -3
- DIRAC/WorkloadManagementSystem/ConfigTemplate.cfg +3 -3
- DIRAC/WorkloadManagementSystem/DB/JobParametersDB.py +8 -8
- DIRAC/WorkloadManagementSystem/DB/SandboxMetadataDB.py +1 -1
- DIRAC/WorkloadManagementSystem/DB/StatusUtils.py +48 -21
- DIRAC/WorkloadManagementSystem/DB/tests/Test_StatusUtils.py +19 -4
- DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py +3 -4
- DIRAC/WorkloadManagementSystem/JobWrapper/Watchdog.py +16 -45
- DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapper.py +18 -9
- DIRAC/WorkloadManagementSystem/Service/JobManagerHandler.py +25 -2
- DIRAC/WorkloadManagementSystem/Service/WMSAdministratorHandler.py +18 -31
- DIRAC/WorkloadManagementSystem/Utilities/PilotCStoJSONSynchronizer.py +73 -7
- {dirac-9.0.0a68.dist-info → dirac-9.0.0a70.dist-info}/METADATA +6 -5
- {dirac-9.0.0a68.dist-info → dirac-9.0.0a70.dist-info}/RECORD +88 -86
- {dirac-9.0.0a68.dist-info → dirac-9.0.0a70.dist-info}/WHEEL +0 -0
- {dirac-9.0.0a68.dist-info → dirac-9.0.0a70.dist-info}/entry_points.txt +0 -0
- {dirac-9.0.0a68.dist-info → dirac-9.0.0a70.dist-info}/licenses/LICENSE +0 -0
- {dirac-9.0.0a68.dist-info → dirac-9.0.0a70.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Accounting class to stores network metrics gathered by perfSONARs.
|
|
2
2
|
|
|
3
|
-
|
|
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(
|
|
20
|
-
("DestinationIP", "VARCHAR(
|
|
21
|
-
("SourceHostName", "VARCHAR(
|
|
22
|
-
("DestinationHostName", "VARCHAR(
|
|
23
|
-
("Source", "VARCHAR(
|
|
24
|
-
("Destination", "VARCHAR(
|
|
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
|
-
"""
|
|
1
|
+
"""Accounting Type for Pilot Submission
|
|
2
2
|
|
|
3
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
#
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
161
|
+
return S_ERROR("; ".join(errors))
|
|
160
162
|
|
|
161
163
|
def initAsServer(self):
|
|
162
164
|
"""Prepare this server socket for use."""
|
DIRAC/Core/Security/DiracX.py
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
72
|
-
|
|
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.
|
|
79
|
-
match
|
|
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
|
|
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"]:
|