DIRAC 9.0.14__py3-none-any.whl → 9.0.15__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/ConfigurationSystem/Client/CSAPI.py +11 -0
- DIRAC/Core/Utilities/CGroups2.py +1 -0
- DIRAC/Core/Utilities/ElasticSearchDB.py +1 -1
- DIRAC/Core/Utilities/MySQL.py +51 -25
- DIRAC/DataManagementSystem/Client/DataManager.py +7 -10
- DIRAC/DataManagementSystem/Client/FTS3Job.py +12 -3
- DIRAC/FrameworkSystem/Service/SystemAdministratorHandler.py +41 -11
- DIRAC/Interfaces/API/Dirac.py +12 -4
- DIRAC/Interfaces/API/Job.py +62 -17
- DIRAC/RequestManagementSystem/private/RequestTask.py +2 -1
- DIRAC/Resources/Catalog/FileCatalogClient.py +18 -7
- DIRAC/Resources/Catalog/Utilities.py +3 -3
- DIRAC/Resources/Computing/BatchSystems/SLURM.py +1 -1
- DIRAC/Resources/Computing/BatchSystems/TimeLeft/TimeLeft.py +3 -1
- DIRAC/Resources/Computing/ComputingElement.py +39 -34
- DIRAC/Resources/Computing/InProcessComputingElement.py +20 -7
- DIRAC/Resources/Computing/PoolComputingElement.py +76 -37
- DIRAC/Resources/Computing/SingularityComputingElement.py +19 -9
- DIRAC/Resources/Computing/test/Test_InProcessComputingElement.py +69 -8
- DIRAC/Resources/Computing/test/Test_PoolComputingElement.py +102 -35
- DIRAC/Resources/Storage/GFAL2_StorageBase.py +9 -0
- DIRAC/TransformationSystem/Agent/TransformationAgent.py +12 -13
- DIRAC/WorkloadManagementSystem/Client/JobReport.py +10 -6
- DIRAC/WorkloadManagementSystem/Client/JobState/JobState.py +12 -3
- DIRAC/WorkloadManagementSystem/Client/Matcher.py +18 -24
- DIRAC/WorkloadManagementSystem/DB/TaskQueueDB.py +137 -7
- DIRAC/WorkloadManagementSystem/Executor/JobScheduling.py +8 -14
- DIRAC/WorkloadManagementSystem/Executor/test/Test_Executor.py +3 -5
- DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py +2 -2
- DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapper.py +1 -1
- DIRAC/WorkloadManagementSystem/Utilities/JobParameters.py +81 -2
- DIRAC/WorkloadManagementSystem/Utilities/QueueUtilities.py +5 -5
- DIRAC/WorkloadManagementSystem/Utilities/RemoteRunner.py +2 -1
- DIRAC/WorkloadManagementSystem/Utilities/test/Test_RemoteRunner.py +7 -3
- DIRAC/WorkloadManagementSystem/scripts/dirac_wms_get_wn_parameters.py +3 -3
- DIRAC/__init__.py +1 -1
- DIRAC/tests/Utilities/testJobDefinitions.py +57 -20
- {dirac-9.0.14.dist-info → dirac-9.0.15.dist-info}/METADATA +2 -2
- {dirac-9.0.14.dist-info → dirac-9.0.15.dist-info}/RECORD +43 -43
- {dirac-9.0.14.dist-info → dirac-9.0.15.dist-info}/WHEEL +0 -0
- {dirac-9.0.14.dist-info → dirac-9.0.15.dist-info}/entry_points.txt +0 -0
- {dirac-9.0.14.dist-info → dirac-9.0.15.dist-info}/licenses/LICENSE +0 -0
- {dirac-9.0.14.dist-info → dirac-9.0.15.dist-info}/top_level.txt +0 -0
|
@@ -299,8 +299,19 @@ class CSAPI:
|
|
|
299
299
|
if "Users" in groupsDict[group] and entity in groupsDict[group]["Users"]:
|
|
300
300
|
entitiesDict[entity]["Groups"].append(group)
|
|
301
301
|
entitiesDict[entity]["Groups"].sort()
|
|
302
|
+
entitiesDict[entity]["AffiliationEnds"] = self.getUserAffiliationEnds(entity)
|
|
302
303
|
return S_OK(entitiesDict)
|
|
303
304
|
|
|
305
|
+
def getUserAffiliationEnds(self, nick):
|
|
306
|
+
affiliation_ends_current = {}
|
|
307
|
+
csSection = f"{self.__baseSecurity}/Users/{nick}/"
|
|
308
|
+
user_sections = self.__csMod.getSections(csSection)
|
|
309
|
+
if "AffiliationEnds" in user_sections:
|
|
310
|
+
affiliation_ends_opts = self.__csMod.getOptions(f"{csSection}/AffiliationEnds")
|
|
311
|
+
for vo_ in affiliation_ends_opts:
|
|
312
|
+
affiliation_ends_current[vo_] = self.__csMod.getValue(f"{csSection}/AffiliationEnds/{vo_}")
|
|
313
|
+
return affiliation_ends_current
|
|
314
|
+
|
|
304
315
|
def listGroups(self):
|
|
305
316
|
"""
|
|
306
317
|
List all groups
|
DIRAC/Core/Utilities/CGroups2.py
CHANGED
|
@@ -300,6 +300,7 @@ class CG2Manager(metaclass=DIRACSingleton):
|
|
|
300
300
|
if "ceParameters" in kwargs:
|
|
301
301
|
if cpuLimit := kwargs["ceParameters"].get("CPULimit", None):
|
|
302
302
|
cores = float(cpuLimit)
|
|
303
|
+
# MemoryLimitMB should be the job upper limit
|
|
303
304
|
if memoryMB := int(kwargs["ceParameters"].get("MemoryLimitMB", 0)):
|
|
304
305
|
memory = memoryMB * 1024 * 1024
|
|
305
306
|
if kwargs["ceParameters"].get("MemoryNoSwap", "no").lower() in ("yes", "true"):
|
|
@@ -621,7 +621,7 @@ class ElasticSearchDB:
|
|
|
621
621
|
if period.lower() == "day":
|
|
622
622
|
suffix = todayUTC.strftime("%Y-%m-%d")
|
|
623
623
|
elif period.lower() == "week":
|
|
624
|
-
suffix = todayUTC.isocalendar()[1]
|
|
624
|
+
suffix = f"{todayUTC.strftime('%Y')}-{todayUTC.isocalendar()[1]}"
|
|
625
625
|
elif period.lower() == "month":
|
|
626
626
|
suffix = todayUTC.strftime("%Y-%m")
|
|
627
627
|
elif period.lower() == "year":
|
DIRAC/Core/Utilities/MySQL.py
CHANGED
|
@@ -903,6 +903,21 @@ class MySQL:
|
|
|
903
903
|
return createView
|
|
904
904
|
return S_OK()
|
|
905
905
|
|
|
906
|
+
def _parseForeignKeyReference(self, auxTable, defaultKey):
|
|
907
|
+
"""
|
|
908
|
+
Parse foreign key reference in format 'Table' or 'Table.key'
|
|
909
|
+
|
|
910
|
+
:param str auxTable: Foreign key reference (e.g., 'MyTable' or 'MyTable.id')
|
|
911
|
+
:param str defaultKey: Default key name if not specified in auxTable
|
|
912
|
+
:return: tuple (table_name, key_name)
|
|
913
|
+
"""
|
|
914
|
+
if "." in auxTable:
|
|
915
|
+
parts = auxTable.split(".", 1)
|
|
916
|
+
if len(parts) != 2:
|
|
917
|
+
raise ValueError(f"Invalid foreign key reference format: {auxTable}")
|
|
918
|
+
return parts[0], parts[1]
|
|
919
|
+
return auxTable, defaultKey
|
|
920
|
+
|
|
906
921
|
def _createTables(self, tableDict, force=False):
|
|
907
922
|
"""
|
|
908
923
|
tableDict:
|
|
@@ -957,30 +972,37 @@ class MySQL:
|
|
|
957
972
|
if "Fields" not in thisTable:
|
|
958
973
|
return S_ERROR(DErrno.EMYSQL, f"Missing `Fields` key in `{table}` table dictionary")
|
|
959
974
|
|
|
960
|
-
|
|
961
|
-
|
|
975
|
+
# Build dependency-ordered list of tables to create
|
|
976
|
+
# Tables with foreign keys must be created after their referenced tables
|
|
977
|
+
tableCreationList = []
|
|
962
978
|
auxiliaryTableList = []
|
|
963
979
|
|
|
964
|
-
|
|
980
|
+
# Get list of existing tables in the database to handle migrations
|
|
981
|
+
existingTablesResult = self._query("SHOW TABLES")
|
|
982
|
+
if not existingTablesResult["OK"]:
|
|
983
|
+
return existingTablesResult
|
|
984
|
+
existingTables = [t[0] for t in existingTablesResult["Value"]]
|
|
985
|
+
|
|
965
986
|
extracted = True
|
|
966
987
|
while tableList and extracted:
|
|
967
988
|
# iterate extracting tables from list if they only depend on
|
|
968
989
|
# already extracted tables.
|
|
969
990
|
extracted = False
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
tableCreationList.append([])
|
|
991
|
+
currentLevelTables = []
|
|
992
|
+
|
|
973
993
|
for table in list(tableList):
|
|
974
994
|
toBeExtracted = True
|
|
975
995
|
thisTable = tableDict[table]
|
|
976
996
|
if "ForeignKeys" in thisTable:
|
|
977
997
|
thisKeys = thisTable["ForeignKeys"]
|
|
978
998
|
for key, auxTable in thisKeys.items():
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
999
|
+
try:
|
|
1000
|
+
forTable, forKey = self._parseForeignKeyReference(auxTable, key)
|
|
1001
|
+
except ValueError as e:
|
|
1002
|
+
return S_ERROR(DErrno.EMYSQL, str(e))
|
|
1003
|
+
|
|
1004
|
+
# Check if the referenced table is either being created or already exists
|
|
1005
|
+
if forTable not in auxiliaryTableList and forTable not in existingTables:
|
|
984
1006
|
toBeExtracted = False
|
|
985
1007
|
break
|
|
986
1008
|
if key not in thisTable["Fields"]:
|
|
@@ -988,24 +1010,29 @@ class MySQL:
|
|
|
988
1010
|
DErrno.EMYSQL,
|
|
989
1011
|
f"ForeignKey `{key}` -> `{forKey}` not defined in Primary table `{table}`.",
|
|
990
1012
|
)
|
|
991
|
-
if
|
|
1013
|
+
# Only validate field existence if the referenced table is in tableDict
|
|
1014
|
+
if forTable in tableDict and forKey not in tableDict[forTable]["Fields"]:
|
|
992
1015
|
return S_ERROR(
|
|
993
1016
|
DErrno.EMYSQL,
|
|
994
|
-
"ForeignKey
|
|
995
|
-
% (key, forKey, forTable),
|
|
1017
|
+
f"ForeignKey `{key}` -> `{forKey}` not defined in Auxiliary table `{forTable}`.",
|
|
996
1018
|
)
|
|
997
1019
|
|
|
998
1020
|
if toBeExtracted:
|
|
999
1021
|
# self.log.debug('Table %s ready to be created' % table)
|
|
1000
1022
|
extracted = True
|
|
1001
1023
|
tableList.remove(table)
|
|
1002
|
-
|
|
1024
|
+
currentLevelTables.append(table)
|
|
1025
|
+
|
|
1026
|
+
if currentLevelTables:
|
|
1027
|
+
tableCreationList.append(currentLevelTables)
|
|
1028
|
+
auxiliaryTableList.extend(currentLevelTables)
|
|
1003
1029
|
|
|
1004
1030
|
if tableList:
|
|
1005
1031
|
return S_ERROR(DErrno.EMYSQL, f"Recursive Foreign Keys in {', '.join(tableList)}")
|
|
1006
1032
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1033
|
+
# Create tables level by level
|
|
1034
|
+
for levelTables in tableCreationList:
|
|
1035
|
+
for table in levelTables:
|
|
1009
1036
|
# Check if Table exist
|
|
1010
1037
|
retDict = self.__checkTable(table, force=force)
|
|
1011
1038
|
if not retDict["OK"]:
|
|
@@ -1035,18 +1062,17 @@ class MySQL:
|
|
|
1035
1062
|
for index in indexDict:
|
|
1036
1063
|
indexedFields = "`, `".join(indexDict[index])
|
|
1037
1064
|
cmdList.append(f"UNIQUE INDEX `{index}` ( `{indexedFields}` )")
|
|
1065
|
+
|
|
1038
1066
|
if "ForeignKeys" in thisTable:
|
|
1039
1067
|
thisKeys = thisTable["ForeignKeys"]
|
|
1040
1068
|
for key, auxTable in thisKeys.items():
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1069
|
+
try:
|
|
1070
|
+
forTable, forKey = self._parseForeignKeyReference(auxTable, key)
|
|
1071
|
+
except ValueError as e:
|
|
1072
|
+
return S_ERROR(DErrno.EMYSQL, str(e))
|
|
1045
1073
|
|
|
1046
|
-
# cmdList.append( '`%s` %s' % ( forTable, tableDict[forTable]['Fields'][forKey] )
|
|
1047
1074
|
cmdList.append(
|
|
1048
|
-
"FOREIGN KEY (
|
|
1049
|
-
" ON DELETE RESTRICT" % (key, forTable, forKey)
|
|
1075
|
+
f"FOREIGN KEY ( `{key}` ) REFERENCES `{forTable}` ( `{forKey}` ) ON DELETE RESTRICT"
|
|
1050
1076
|
)
|
|
1051
1077
|
|
|
1052
1078
|
engine = thisTable.get("Engine", "InnoDB")
|
|
@@ -1058,7 +1084,7 @@ class MySQL:
|
|
|
1058
1084
|
engine,
|
|
1059
1085
|
charset,
|
|
1060
1086
|
)
|
|
1061
|
-
retDict = self.
|
|
1087
|
+
retDict = self._update(cmd)
|
|
1062
1088
|
if not retDict["OK"]:
|
|
1063
1089
|
return retDict
|
|
1064
1090
|
# self.log.debug('Table %s created' % table)
|
|
@@ -22,7 +22,7 @@ from DIRAC import S_OK, S_ERROR, gLogger, gConfig
|
|
|
22
22
|
from DIRAC.Core.Utilities import DErrno
|
|
23
23
|
from DIRAC.Core.Utilities.Adler import fileAdler, compareAdler
|
|
24
24
|
from DIRAC.Core.Utilities.File import makeGuid, getSize
|
|
25
|
-
from DIRAC.Core.Utilities.List import randomize
|
|
25
|
+
from DIRAC.Core.Utilities.List import randomize
|
|
26
26
|
from DIRAC.Core.Utilities.ReturnValues import returnSingleResult
|
|
27
27
|
from DIRAC.Core.Security.ProxyInfo import getProxyInfo
|
|
28
28
|
from DIRAC.Core.Security.ProxyInfo import getVOfromProxyGroup
|
|
@@ -1677,18 +1677,15 @@ class DataManager:
|
|
|
1677
1677
|
"""get replicas from catalogue and filter if requested
|
|
1678
1678
|
Warning: all filters are independent, hence active and preferDisk should be set if using forJobs
|
|
1679
1679
|
"""
|
|
1680
|
-
|
|
1681
|
-
failed = {}
|
|
1680
|
+
|
|
1682
1681
|
if not protocol:
|
|
1683
1682
|
protocol = self.registrationProtocol
|
|
1684
1683
|
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
else:
|
|
1691
|
-
return res
|
|
1684
|
+
res = self.fileCatalog.getReplicas(lfns, allStatus=allStatus)
|
|
1685
|
+
if not res["OK"]:
|
|
1686
|
+
return res
|
|
1687
|
+
catalogReplicas = res["Value"]["Successful"]
|
|
1688
|
+
failed = res["Value"]["Failed"]
|
|
1692
1689
|
if not getUrl:
|
|
1693
1690
|
for lfn in catalogReplicas:
|
|
1694
1691
|
catalogReplicas[lfn] = dict.fromkeys(catalogReplicas[lfn], True)
|
|
@@ -272,7 +272,7 @@ class FTS3Job(JSerializable):
|
|
|
272
272
|
|
|
273
273
|
# If the file is failed, check if it is recoverable
|
|
274
274
|
if file_state in FTS3File.FTS_FAILED_STATES:
|
|
275
|
-
if not fileDict.get("
|
|
275
|
+
if not fileDict.get("recoverable", True):
|
|
276
276
|
filesStatus[file_id]["status"] = "Defunct"
|
|
277
277
|
|
|
278
278
|
# If the file is not in a final state, but the job is, we return an error
|
|
@@ -400,7 +400,7 @@ class FTS3Job(JSerializable):
|
|
|
400
400
|
:return: S_OK( (job object, list of ftsFileIDs in the job))
|
|
401
401
|
"""
|
|
402
402
|
|
|
403
|
-
log = gLogger.
|
|
403
|
+
log = gLogger.getLocalSubLogger(f"constructTransferJob/{self.operationID}/{self.sourceSE}_{self.targetSE}")
|
|
404
404
|
|
|
405
405
|
isMultiHop = False
|
|
406
406
|
useTokens = False
|
|
@@ -411,6 +411,15 @@ class FTS3Job(JSerializable):
|
|
|
411
411
|
log.debug(f"Multihop job has {len(allLFNs)} files while only 1 allowed")
|
|
412
412
|
return S_ERROR(errno.E2BIG, "Trying multihop job with more than one file !")
|
|
413
413
|
allHops = [(self.sourceSE, self.multiHopSE), (self.multiHopSE, self.targetSE)]
|
|
414
|
+
if tokensEnabled:
|
|
415
|
+
tokensEnabled = all(
|
|
416
|
+
[
|
|
417
|
+
self.__seTokenSupport(StorageElement(seName))
|
|
418
|
+
for seName in (self.sourceSE, self.multiHopSE, self.targetSE)
|
|
419
|
+
]
|
|
420
|
+
)
|
|
421
|
+
if not tokensEnabled:
|
|
422
|
+
log.warn("Not using token because not all hop supports it")
|
|
414
423
|
isMultiHop = True
|
|
415
424
|
else:
|
|
416
425
|
allHops = [(self.sourceSE, self.targetSE)]
|
|
@@ -736,7 +745,7 @@ class FTS3Job(JSerializable):
|
|
|
736
745
|
:return: S_OK( (job object, list of ftsFileIDs in the job))
|
|
737
746
|
"""
|
|
738
747
|
|
|
739
|
-
log = gLogger.
|
|
748
|
+
log = gLogger.getLocalSubLogger(f"constructStagingJob/{self.operationID}/{self.targetSE}")
|
|
740
749
|
|
|
741
750
|
transfers = []
|
|
742
751
|
fileIDsInTheJob = set()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
1
|
+
"""SystemAdministrator service is a tool to control and monitor the DIRAC services and agents"""
|
|
2
|
+
|
|
3
3
|
import socket
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
@@ -254,17 +254,38 @@ class SystemAdministratorHandler(RequestHandler):
|
|
|
254
254
|
types_updateSoftware = [str]
|
|
255
255
|
|
|
256
256
|
def export_updateSoftware(self, version):
|
|
257
|
+
"""Update DIRAC, or its extension, to a version. Use extension_name==version for the DIRAC extension.
|
|
258
|
+
|
|
259
|
+
A version can be:
|
|
260
|
+
- a PEP440 valid version of DIRAC.
|
|
261
|
+
- a PEP440 valid version of a DIRAC extension.
|
|
262
|
+
- "integration" or "devel" or "master" or "main" would all be interpreted as git+https://github.com/DIRACGrid/DIRAC.git@integration#egg=DIRAC[server]
|
|
263
|
+
- a git tag/branch like git+https://github.com/fstagni/DIRAC.git@test_branch#egg=DIRAC[server]
|
|
264
|
+
"""
|
|
257
265
|
# Validate and normalise the requested version
|
|
258
266
|
primaryExtension = None
|
|
259
267
|
if "==" in version:
|
|
260
268
|
primaryExtension, version = version.split("==")
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
269
|
+
|
|
270
|
+
released_version = True
|
|
271
|
+
isPrerelease = False
|
|
272
|
+
|
|
273
|
+
# Special cases (e.g. installing the integration/main branch)
|
|
274
|
+
if version.lower() in ["integration", "devel", "master", "main"]:
|
|
275
|
+
released_version = False
|
|
276
|
+
version = "git+https://github.com/DIRACGrid/DIRAC.git@integration#egg=DIRAC[server]"
|
|
277
|
+
|
|
278
|
+
if released_version:
|
|
279
|
+
try:
|
|
280
|
+
version = Version(version)
|
|
281
|
+
isPrerelease = version.is_prerelease
|
|
282
|
+
version = f"v{version}"
|
|
283
|
+
except InvalidVersion:
|
|
284
|
+
if "https://" in version:
|
|
285
|
+
released_version = False
|
|
286
|
+
else:
|
|
287
|
+
self.log.exception("Invalid version passed", version)
|
|
288
|
+
return S_ERROR(f"Invalid version passed {version!r}")
|
|
268
289
|
|
|
269
290
|
# Find what to install
|
|
270
291
|
otherExtensions = []
|
|
@@ -290,10 +311,11 @@ class SystemAdministratorHandler(RequestHandler):
|
|
|
290
311
|
installer.flush()
|
|
291
312
|
self.log.info("Downloaded DIRACOS installer to", installer.name)
|
|
292
313
|
|
|
314
|
+
directory = version if released_version else version.split("@")[1].split("#")[0]
|
|
293
315
|
newProPrefix = os.path.join(
|
|
294
316
|
rootPath,
|
|
295
317
|
"versions",
|
|
296
|
-
f"{
|
|
318
|
+
f"{directory}-{datetime.utcnow().strftime('%s')}",
|
|
297
319
|
)
|
|
298
320
|
installPrefix = os.path.join(newProPrefix, f"{platform.system()}-{platform.machine()}")
|
|
299
321
|
self.log.info("Running DIRACOS installer for prefix", installPrefix)
|
|
@@ -313,7 +335,15 @@ class SystemAdministratorHandler(RequestHandler):
|
|
|
313
335
|
cmd = [f"{installPrefix}/bin/pip", "install", "--no-color", "-v"]
|
|
314
336
|
if isPrerelease:
|
|
315
337
|
cmd += ["--pre"]
|
|
316
|
-
|
|
338
|
+
if released_version:
|
|
339
|
+
cmd += [f"{primaryExtension}[server]=={version}"]
|
|
340
|
+
else:
|
|
341
|
+
# from here on we assume a version like git+https://github.com/DIRACGrid/DIRAC.git@integration#egg=DIRAC[server]
|
|
342
|
+
# is specified, for the primaryExtension
|
|
343
|
+
if not version.startswith("git+"):
|
|
344
|
+
version = f"git+{version}"
|
|
345
|
+
cmd += [version]
|
|
346
|
+
|
|
317
347
|
cmd += [f"{e}[server]" for e in otherExtensions]
|
|
318
348
|
r = subprocess.run(
|
|
319
349
|
cmd,
|
DIRAC/Interfaces/API/Dirac.py
CHANGED
|
@@ -208,7 +208,9 @@ class Dirac(API):
|
|
|
208
208
|
return S_OK("Nothing to do")
|
|
209
209
|
|
|
210
210
|
#############################################################################
|
|
211
|
-
def getInputDataCatalog(
|
|
211
|
+
def getInputDataCatalog(
|
|
212
|
+
self, lfns, siteName="", fileName="pool_xml_catalog.xml", ignoreMissing=False, inputDataPolicy=None
|
|
213
|
+
):
|
|
212
214
|
"""This utility will create a pool xml catalogue slice for the specified LFNs using
|
|
213
215
|
the full input data resolution policy plugins for the VO.
|
|
214
216
|
|
|
@@ -228,6 +230,10 @@ class Dirac(API):
|
|
|
228
230
|
:type siteName: string
|
|
229
231
|
:param fileName: Catalogue name (can include path)
|
|
230
232
|
:type fileName: string
|
|
233
|
+
:param ignoreMissing: Flag to ignore missing files
|
|
234
|
+
:type ignoreMissing: bool
|
|
235
|
+
:param inputDataPolicy: Input data policy module to use
|
|
236
|
+
:type inputDataPolicy: string or None
|
|
231
237
|
:returns: S_OK,S_ERROR
|
|
232
238
|
|
|
233
239
|
"""
|
|
@@ -248,8 +254,8 @@ class Dirac(API):
|
|
|
248
254
|
|
|
249
255
|
self.log.verbose(localSEList)
|
|
250
256
|
|
|
251
|
-
|
|
252
|
-
if not
|
|
257
|
+
inputDataModule = Operations().getValue("InputDataPolicy/InputDataModule")
|
|
258
|
+
if not inputDataModule:
|
|
253
259
|
return self._errorReport("Could not retrieve /DIRAC/Operations/InputDataPolicy/InputDataModule for VO")
|
|
254
260
|
|
|
255
261
|
self.log.info(f"Attempting to resolve data for {siteName}")
|
|
@@ -279,11 +285,13 @@ class Dirac(API):
|
|
|
279
285
|
|
|
280
286
|
self.log.verbose(configDict)
|
|
281
287
|
argumentsDict = {"FileCatalog": resolvedData, "Configuration": configDict, "InputData": lfns}
|
|
288
|
+
if inputDataPolicy:
|
|
289
|
+
argumentsDict["Job"] = {"InputDataPolicy": inputDataPolicy}
|
|
282
290
|
if ignoreMissing:
|
|
283
291
|
argumentsDict["IgnoreMissing"] = True
|
|
284
292
|
self.log.verbose(argumentsDict)
|
|
285
293
|
|
|
286
|
-
result = self.objectLoader.loadObject(
|
|
294
|
+
result = self.objectLoader.loadObject(inputDataModule)
|
|
287
295
|
if not result["OK"]:
|
|
288
296
|
return result
|
|
289
297
|
module = result["Value"](argumentsDict)
|
DIRAC/Interfaces/API/Job.py
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Job Base Class
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
This class provides generic job definition functionality suitable for any VO.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
Helper functions are documented with example usage for the DIRAC API. An example
|
|
7
|
+
script (for a simple executable) would be::
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
from DIRAC.Interfaces.API.Dirac import Dirac
|
|
10
|
+
from DIRAC.Interfaces.API.Job import Job
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
j = Job()
|
|
13
|
+
j.setCPUTime(500)
|
|
14
|
+
j.setExecutable('/bin/echo hello')
|
|
15
|
+
j.setExecutable('yourPythonScript.py')
|
|
16
|
+
j.setExecutable('/bin/echo hello again')
|
|
17
|
+
j.setName('MyJobName')
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
dirac = Dirac()
|
|
20
|
+
jobID = dirac.submitJob(j)
|
|
21
|
+
print 'Submission Result: ',jobID
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Note that several executables can be provided and wil be executed sequentially.
|
|
24
24
|
"""
|
|
25
|
+
|
|
25
26
|
import os
|
|
26
27
|
import re
|
|
27
28
|
import shlex
|
|
@@ -517,6 +518,50 @@ class Job(API):
|
|
|
517
518
|
return S_OK()
|
|
518
519
|
|
|
519
520
|
#############################################################################
|
|
521
|
+
def setRAMRequirements(self, ramRequired: int = 0, maxRAM: int = 0):
|
|
522
|
+
"""Helper function.
|
|
523
|
+
Specify the RAM requirements for the job, in MB. 0 (default) means no specific requirements.
|
|
524
|
+
|
|
525
|
+
Example usage:
|
|
526
|
+
|
|
527
|
+
>>> job = Job()
|
|
528
|
+
>>> job.setRAMRequirements(ramRequired=2000)
|
|
529
|
+
means that the job needs at least 2 GBs of RAM to work. This is taken into consideration at job's matching time.
|
|
530
|
+
The job definition does not specify an upper limit.
|
|
531
|
+
From a user's point of view this is fine (normally, not for admins).
|
|
532
|
+
|
|
533
|
+
>>> job.setRAMRequirements(ramRequired=500, maxRAM=3800)
|
|
534
|
+
means that the job needs 500 MBs of RAM to work. 3.8 GBs will then be the upper limit for CG2 limits.
|
|
535
|
+
|
|
536
|
+
>>> job.setRAMRequirements(ramRequired=3200, maxRAM=3200)
|
|
537
|
+
means that we should match this job if there is at least 3.2 available GBs of run. At the same time, CG2 will not allow to use more than that.
|
|
538
|
+
|
|
539
|
+
>>> job.setRAMRequirements(maxRAM=4000)
|
|
540
|
+
means that the job does not set a min amount of RAM (so can match--run "everywhere"), but the 4 GBs will then be the upper limit for CG2 limits.
|
|
541
|
+
|
|
542
|
+
>>> job.setRAMRequirements(ramRequired=8000, maxRAM=4000)
|
|
543
|
+
Makes no sense, an error will be raised
|
|
544
|
+
"""
|
|
545
|
+
if ramRequired and maxRAM and ramRequired > maxRAM:
|
|
546
|
+
return self._reportError("Invalid settings, ramRequired is higher than maxRAM")
|
|
547
|
+
|
|
548
|
+
if ramRequired:
|
|
549
|
+
self._addParameter(
|
|
550
|
+
self.workflow,
|
|
551
|
+
"MinRAM",
|
|
552
|
+
"JDL",
|
|
553
|
+
ramRequired,
|
|
554
|
+
"MBs of RAM requested",
|
|
555
|
+
)
|
|
556
|
+
if maxRAM:
|
|
557
|
+
self._addParameter(
|
|
558
|
+
self.workflow,
|
|
559
|
+
"MaxRAM",
|
|
560
|
+
"JDL",
|
|
561
|
+
maxRAM,
|
|
562
|
+
"Max MBs of RAM to be used",
|
|
563
|
+
)
|
|
564
|
+
|
|
520
565
|
def setNumberOfProcessors(self, numberOfProcessors=None, minNumberOfProcessors=None, maxNumberOfProcessors=None):
|
|
521
566
|
"""Helper function to set the number of processors required by the job.
|
|
522
567
|
|
|
@@ -709,7 +754,7 @@ class Job(API):
|
|
|
709
754
|
Example usage:
|
|
710
755
|
|
|
711
756
|
>>> job = Job()
|
|
712
|
-
>>> job.setTag( ['WholeNode'
|
|
757
|
+
>>> job.setTag( ['WholeNode'] )
|
|
713
758
|
|
|
714
759
|
:param tags: single tag string or a list of tags
|
|
715
760
|
:type tags: str or python:list
|
|
@@ -106,7 +106,8 @@ class RequestTask:
|
|
|
106
106
|
userDN, userGroup, requiredTimeLeft=1200, cacheTime=4 * 43200
|
|
107
107
|
)
|
|
108
108
|
if not getProxy["OK"]:
|
|
109
|
-
|
|
109
|
+
self.log.error("unable to setup shifter proxy", f"{shifter}: {getProxy['Message']}")
|
|
110
|
+
continue
|
|
110
111
|
chain = getProxy["chain"]
|
|
111
112
|
fileName = getProxy["Value"]
|
|
112
113
|
self.log.debug(f"got {shifter}: {userName} {userGroup}")
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
1
|
+
"""The FileCatalogClient is a class representing the client of the DIRAC File Catalog"""
|
|
2
|
+
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
|
|
6
6
|
from DIRAC import S_OK, S_ERROR
|
|
7
|
+
from DIRAC.Core.Utilities.List import breakListIntoChunks
|
|
7
8
|
from DIRAC.Core.Tornado.Client.ClientSelector import TransferClientSelector as TransferClient
|
|
8
9
|
|
|
9
10
|
from DIRAC.ConfigurationSystem.Client.Helpers.Registry import getVOMSAttributeForGroup, getDNForUsername
|
|
10
11
|
from DIRAC.Resources.Catalog.Utilities import checkCatalogArguments
|
|
11
12
|
from DIRAC.Resources.Catalog.FileCatalogClientBase import FileCatalogClientBase
|
|
12
13
|
|
|
14
|
+
GET_REPLICAS_CHUNK_SIZE = 10_000
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
class FileCatalogClient(FileCatalogClientBase):
|
|
15
18
|
"""Client code to the DIRAC File Catalogue"""
|
|
@@ -135,14 +138,22 @@ class FileCatalogClient(FileCatalogClientBase):
|
|
|
135
138
|
@checkCatalogArguments
|
|
136
139
|
def getReplicas(self, lfns, allStatus=False, timeout=120):
|
|
137
140
|
"""Get the replicas of the given files"""
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
successful = {}
|
|
142
|
+
failed = {}
|
|
140
143
|
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
# We want to sort the lfns because of the way the server groups the db queries by
|
|
145
|
+
# directory. So if we sort them, the grouping is more efficient.
|
|
146
|
+
for chunk in breakListIntoChunks(sorted(lfns), GET_REPLICAS_CHUNK_SIZE):
|
|
147
|
+
rpcClient = self._getRPC(timeout=timeout)
|
|
148
|
+
result = rpcClient.getReplicas(chunk, allStatus)
|
|
149
|
+
|
|
150
|
+
if not result["OK"]:
|
|
151
|
+
return result
|
|
152
|
+
successful.update(result["Value"]["Successful"])
|
|
153
|
+
failed.update(result["Value"]["Failed"])
|
|
143
154
|
|
|
144
155
|
# If there is no PFN returned, just set the LFN instead
|
|
145
|
-
lfnDict =
|
|
156
|
+
lfnDict = {"Successful": successful, "Failed": failed}
|
|
146
157
|
for lfn in lfnDict["Successful"]:
|
|
147
158
|
for se in lfnDict["Successful"][lfn]:
|
|
148
159
|
if not lfnDict["Successful"][lfn][se]:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
1
|
+
"""DIRAC FileCatalog client utilities"""
|
|
2
|
+
|
|
3
3
|
import os
|
|
4
4
|
import errno
|
|
5
5
|
import functools
|
|
@@ -16,7 +16,7 @@ def checkArgumentFormat(path, generateMap=False):
|
|
|
16
16
|
"""Check and process format of the arguments to FileCatalog methods"""
|
|
17
17
|
if isinstance(path, str):
|
|
18
18
|
urls = {path: True}
|
|
19
|
-
elif isinstance(path, list):
|
|
19
|
+
elif isinstance(path, (list, set)):
|
|
20
20
|
urls = {}
|
|
21
21
|
for url in path:
|
|
22
22
|
urls[url] = True
|
|
@@ -300,7 +300,7 @@ srun -l -k %(wrapper)s
|
|
|
300
300
|
|
|
301
301
|
waitingJobs = 0
|
|
302
302
|
runningJobs = 0
|
|
303
|
-
lines = output.split("\n")
|
|
303
|
+
lines = output.strip().split("\n")
|
|
304
304
|
for line in lines[1:]:
|
|
305
305
|
_jid, status = line.split()
|
|
306
306
|
if status in ["PENDING", "SUSPENDED", "CONFIGURING"]:
|
|
@@ -75,7 +75,9 @@ class TimeLeft:
|
|
|
75
75
|
|
|
76
76
|
resourceDict = self.batchPlugin.getResourceUsage()
|
|
77
77
|
if not resourceDict["OK"]:
|
|
78
|
-
self.log.warn(
|
|
78
|
+
self.log.warn(
|
|
79
|
+
f"Could not determine timeleft for batch system at site {DIRAC.siteName()}: {resourceDict['Message']}"
|
|
80
|
+
)
|
|
79
81
|
return resourceDict
|
|
80
82
|
|
|
81
83
|
resources = resourceDict["Value"]
|