DIRAC 9.0.0a54__py3-none-any.whl → 9.0.7__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/AccountingCLI.py +0 -140
- DIRAC/AccountingSystem/Client/DataStoreClient.py +0 -13
- DIRAC/AccountingSystem/Client/Types/BaseAccountingType.py +0 -7
- DIRAC/AccountingSystem/ConfigTemplate.cfg +0 -5
- DIRAC/AccountingSystem/Service/DataStoreHandler.py +0 -72
- DIRAC/ConfigurationSystem/Client/Helpers/CSGlobals.py +0 -9
- DIRAC/ConfigurationSystem/Client/Helpers/Registry.py +34 -32
- DIRAC/ConfigurationSystem/Client/Helpers/Resources.py +11 -43
- DIRAC/ConfigurationSystem/Client/Helpers/test/Test_Helpers.py +0 -16
- DIRAC/ConfigurationSystem/Client/LocalConfiguration.py +14 -8
- DIRAC/ConfigurationSystem/Client/PathFinder.py +47 -8
- DIRAC/ConfigurationSystem/Client/SyncPlugins/CERNLDAPSyncPlugin.py +4 -1
- DIRAC/ConfigurationSystem/Client/VOMS2CSSynchronizer.py +9 -2
- DIRAC/ConfigurationSystem/Client/test/Test_PathFinder.py +41 -1
- DIRAC/ConfigurationSystem/private/RefresherBase.py +4 -2
- DIRAC/Core/DISET/ServiceReactor.py +11 -3
- DIRAC/Core/DISET/private/BaseClient.py +1 -2
- DIRAC/Core/DISET/private/Transports/M2SSLTransport.py +9 -7
- DIRAC/Core/Security/DiracX.py +12 -7
- DIRAC/Core/Security/IAMService.py +4 -3
- DIRAC/Core/Security/ProxyInfo.py +9 -5
- DIRAC/Core/Security/test/test_diracx_token_from_pem.py +161 -0
- DIRAC/Core/Tornado/Client/ClientSelector.py +4 -1
- DIRAC/Core/Tornado/Server/TornadoService.py +1 -1
- DIRAC/Core/Utilities/ClassAd/ClassAdLight.py +4 -290
- DIRAC/Core/Utilities/DErrno.py +5 -309
- DIRAC/Core/Utilities/Extensions.py +10 -1
- DIRAC/Core/Utilities/Graphs/GraphData.py +1 -1
- DIRAC/Core/Utilities/JDL.py +1 -195
- DIRAC/Core/Utilities/List.py +1 -124
- DIRAC/Core/Utilities/MySQL.py +101 -97
- DIRAC/Core/Utilities/Os.py +32 -1
- DIRAC/Core/Utilities/Platform.py +2 -107
- DIRAC/Core/Utilities/ReturnValues.py +7 -252
- DIRAC/Core/Utilities/StateMachine.py +12 -178
- DIRAC/Core/Utilities/TimeUtilities.py +10 -253
- DIRAC/Core/Utilities/test/Test_JDL.py +0 -3
- DIRAC/Core/Utilities/test/Test_Profiler.py +20 -20
- DIRAC/Core/scripts/dirac_agent.py +1 -1
- DIRAC/Core/scripts/dirac_apptainer_exec.py +16 -7
- DIRAC/Core/scripts/dirac_platform.py +1 -92
- DIRAC/DataManagementSystem/Agent/FTS3Agent.py +8 -7
- DIRAC/DataManagementSystem/Agent/RequestOperations/RemoveFile.py +7 -6
- DIRAC/DataManagementSystem/Client/FTS3Job.py +71 -34
- DIRAC/DataManagementSystem/DB/FTS3DB.py +3 -0
- DIRAC/DataManagementSystem/DB/FileCatalogComponents/DatasetManager/DatasetManager.py +1 -1
- DIRAC/DataManagementSystem/Utilities/DMSHelpers.py +6 -2
- DIRAC/DataManagementSystem/scripts/dirac_dms_create_moving_request.py +2 -0
- DIRAC/DataManagementSystem/scripts/dirac_dms_protocol_matrix.py +0 -1
- DIRAC/FrameworkSystem/Client/ComponentInstaller.py +4 -2
- DIRAC/FrameworkSystem/DB/ProxyDB.py +9 -5
- DIRAC/FrameworkSystem/Utilities/TokenManagementUtilities.py +3 -2
- DIRAC/FrameworkSystem/Utilities/diracx.py +2 -74
- DIRAC/FrameworkSystem/private/authorization/AuthServer.py +2 -2
- DIRAC/FrameworkSystem/scripts/dirac_login.py +2 -2
- DIRAC/FrameworkSystem/scripts/dirac_proxy_init.py +1 -1
- DIRAC/Interfaces/API/Dirac.py +27 -13
- DIRAC/Interfaces/API/DiracAdmin.py +42 -7
- DIRAC/Interfaces/API/Job.py +1 -0
- DIRAC/Interfaces/scripts/dirac_admin_allow_site.py +7 -1
- DIRAC/Interfaces/scripts/dirac_admin_ban_site.py +7 -1
- DIRAC/Interfaces/scripts/dirac_wms_job_parameters.py +0 -1
- DIRAC/MonitoringSystem/Client/Types/WMSHistory.py +4 -0
- DIRAC/MonitoringSystem/Client/WebAppClient.py +26 -0
- DIRAC/MonitoringSystem/ConfigTemplate.cfg +9 -0
- DIRAC/MonitoringSystem/DB/MonitoringDB.py +6 -25
- DIRAC/MonitoringSystem/Service/MonitoringHandler.py +0 -33
- DIRAC/MonitoringSystem/Service/WebAppHandler.py +599 -0
- DIRAC/MonitoringSystem/private/MainReporter.py +0 -3
- DIRAC/ProductionSystem/scripts/dirac_prod_get_trans.py +2 -3
- DIRAC/RequestManagementSystem/Agent/RequestExecutingAgent.py +8 -6
- DIRAC/RequestManagementSystem/ConfigTemplate.cfg +6 -6
- DIRAC/RequestManagementSystem/DB/test/RMSTestScenari.py +2 -0
- DIRAC/ResourceStatusSystem/Client/SiteStatus.py +4 -2
- DIRAC/ResourceStatusSystem/Command/FreeDiskSpaceCommand.py +3 -1
- DIRAC/ResourceStatusSystem/Utilities/CSHelpers.py +2 -31
- DIRAC/ResourceStatusSystem/scripts/dirac_rss_set_status.py +18 -4
- DIRAC/Resources/Catalog/RucioFileCatalogClient.py +1 -1
- DIRAC/Resources/Computing/AREXComputingElement.py +19 -3
- DIRAC/Resources/Computing/BatchSystems/Condor.py +126 -108
- DIRAC/Resources/Computing/BatchSystems/SLURM.py +5 -1
- DIRAC/Resources/Computing/BatchSystems/test/Test_SLURM.py +46 -0
- DIRAC/Resources/Computing/HTCondorCEComputingElement.py +37 -43
- DIRAC/Resources/Computing/SingularityComputingElement.py +6 -1
- DIRAC/Resources/Computing/test/Test_HTCondorCEComputingElement.py +67 -49
- DIRAC/Resources/Computing/test/Test_PoolComputingElement.py +2 -1
- DIRAC/Resources/IdProvider/CheckInIdProvider.py +13 -0
- DIRAC/Resources/IdProvider/IdProviderFactory.py +11 -3
- DIRAC/Resources/Storage/StorageBase.py +4 -2
- DIRAC/Resources/Storage/StorageElement.py +4 -4
- DIRAC/TransformationSystem/Agent/TaskManagerAgentBase.py +10 -16
- DIRAC/TransformationSystem/Agent/TransformationAgent.py +22 -1
- DIRAC/TransformationSystem/Agent/TransformationCleaningAgent.py +15 -15
- DIRAC/TransformationSystem/Client/Transformation.py +2 -1
- DIRAC/TransformationSystem/Client/TransformationClient.py +0 -7
- DIRAC/TransformationSystem/Client/Utilities.py +9 -0
- DIRAC/TransformationSystem/Service/TransformationManagerHandler.py +0 -336
- 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/Workflow/Modules/test/Test_Modules.py +5 -0
- DIRAC/WorkloadManagementSystem/Agent/JobAgent.py +1 -5
- DIRAC/WorkloadManagementSystem/Agent/JobCleaningAgent.py +11 -7
- DIRAC/WorkloadManagementSystem/Agent/PilotSyncAgent.py +4 -3
- DIRAC/WorkloadManagementSystem/Agent/PushJobAgent.py +13 -13
- DIRAC/WorkloadManagementSystem/Agent/SiteDirector.py +10 -13
- DIRAC/WorkloadManagementSystem/Agent/StalledJobAgent.py +18 -51
- DIRAC/WorkloadManagementSystem/Agent/StatesAccountingAgent.py +41 -1
- DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_JobAgent.py +2 -0
- DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_JobCleaningAgent.py +7 -9
- DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_PushJobAgent.py +1 -0
- DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_SiteDirector.py +8 -2
- DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_StalledJobAgent.py +4 -5
- DIRAC/WorkloadManagementSystem/Client/DownloadInputData.py +7 -5
- DIRAC/WorkloadManagementSystem/Client/JobMonitoringClient.py +10 -11
- DIRAC/WorkloadManagementSystem/Client/JobState/JobManifest.py +32 -261
- DIRAC/WorkloadManagementSystem/Client/JobStateUpdateClient.py +3 -0
- DIRAC/WorkloadManagementSystem/Client/JobStatus.py +8 -152
- DIRAC/WorkloadManagementSystem/Client/SandboxStoreClient.py +25 -38
- DIRAC/WorkloadManagementSystem/Client/WMSClient.py +2 -3
- DIRAC/WorkloadManagementSystem/Client/test/Test_Client_DownloadInputData.py +29 -0
- DIRAC/WorkloadManagementSystem/ConfigTemplate.cfg +4 -8
- DIRAC/WorkloadManagementSystem/DB/JobDB.py +40 -69
- DIRAC/WorkloadManagementSystem/DB/JobDBUtils.py +18 -147
- DIRAC/WorkloadManagementSystem/DB/JobParametersDB.py +9 -9
- DIRAC/WorkloadManagementSystem/DB/PilotAgentsDB.py +3 -2
- DIRAC/WorkloadManagementSystem/DB/SandboxMetadataDB.py +28 -39
- DIRAC/WorkloadManagementSystem/DB/StatusUtils.py +125 -0
- DIRAC/WorkloadManagementSystem/DB/tests/Test_JobDB.py +1 -1
- DIRAC/WorkloadManagementSystem/DB/tests/Test_StatusUtils.py +28 -0
- DIRAC/WorkloadManagementSystem/Executor/JobSanity.py +3 -3
- DIRAC/WorkloadManagementSystem/FutureClient/JobStateUpdateClient.py +2 -14
- DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py +14 -9
- DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapper.py +36 -10
- DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapperTemplate.py +4 -0
- DIRAC/WorkloadManagementSystem/Service/JobManagerHandler.py +33 -154
- DIRAC/WorkloadManagementSystem/Service/JobMonitoringHandler.py +5 -323
- DIRAC/WorkloadManagementSystem/Service/JobStateUpdateHandler.py +0 -16
- DIRAC/WorkloadManagementSystem/Service/PilotManagerHandler.py +6 -102
- DIRAC/WorkloadManagementSystem/Service/SandboxStoreHandler.py +5 -51
- DIRAC/WorkloadManagementSystem/Service/WMSAdministratorHandler.py +16 -79
- DIRAC/WorkloadManagementSystem/Utilities/JobModel.py +28 -199
- DIRAC/WorkloadManagementSystem/Utilities/JobParameters.py +65 -3
- DIRAC/WorkloadManagementSystem/Utilities/JobStatusUtility.py +2 -64
- DIRAC/WorkloadManagementSystem/Utilities/ParametricJob.py +7 -171
- DIRAC/WorkloadManagementSystem/Utilities/PilotCStoJSONSynchronizer.py +73 -7
- DIRAC/WorkloadManagementSystem/Utilities/PilotWrapper.py +2 -0
- DIRAC/WorkloadManagementSystem/Utilities/RemoteRunner.py +16 -0
- DIRAC/WorkloadManagementSystem/Utilities/Utils.py +36 -1
- DIRAC/WorkloadManagementSystem/Utilities/jobAdministration.py +15 -0
- DIRAC/WorkloadManagementSystem/Utilities/test/Test_JobModel.py +1 -5
- DIRAC/WorkloadManagementSystem/Utilities/test/Test_ParametricJob.py +45 -128
- DIRAC/WorkloadManagementSystem/Utilities/test/Test_PilotWrapper.py +16 -0
- DIRAC/__init__.py +55 -54
- {dirac-9.0.0a54.dist-info → dirac-9.0.7.dist-info}/METADATA +6 -4
- {dirac-9.0.0a54.dist-info → dirac-9.0.7.dist-info}/RECORD +160 -160
- {dirac-9.0.0a54.dist-info → dirac-9.0.7.dist-info}/WHEEL +1 -1
- {dirac-9.0.0a54.dist-info → dirac-9.0.7.dist-info}/entry_points.txt +0 -3
- DIRAC/Core/Utilities/test/Test_List.py +0 -150
- DIRAC/Core/Utilities/test/Test_Time.py +0 -88
- DIRAC/TransformationSystem/scripts/dirac_transformation_archive.py +0 -30
- DIRAC/TransformationSystem/scripts/dirac_transformation_clean.py +0 -30
- DIRAC/TransformationSystem/scripts/dirac_transformation_remove_output.py +0 -30
- DIRAC/WorkloadManagementSystem/Utilities/test/Test_JobManager.py +0 -58
- {dirac-9.0.0a54.dist-info → dirac-9.0.7.dist-info}/licenses/LICENSE +0 -0
- {dirac-9.0.0a54.dist-info → dirac-9.0.7.dist-info}/top_level.txt +0 -0
|
@@ -22,7 +22,6 @@ from DIRAC.Core.DISET.RequestHandler import RequestHandler
|
|
|
22
22
|
from DIRAC.Core.Security import Properties
|
|
23
23
|
from DIRAC.Core.Utilities.File import getGlobbedTotalSize, mkDir
|
|
24
24
|
from DIRAC.Core.Utilities.ObjectLoader import ObjectLoader
|
|
25
|
-
from DIRAC.DataManagementSystem.Service.StorageElementHandler import getDiskSpace
|
|
26
25
|
from DIRAC.FrameworkSystem.Utilities.diracx import TheImpersonator
|
|
27
26
|
|
|
28
27
|
|
|
@@ -139,7 +138,7 @@ class SandboxStoreHandlerMixin:
|
|
|
139
138
|
gLogger.debug("Sandbox already exists in storage backend", res.pfn)
|
|
140
139
|
|
|
141
140
|
assignTo = {key: [(res.pfn, assignTo[key])] for key in assignTo}
|
|
142
|
-
result = self.
|
|
141
|
+
result = self.sandboxDB.assignSandboxesToEntities(assignTo, credDict["username"], credDict["group"])
|
|
143
142
|
if not result["OK"]:
|
|
144
143
|
return result
|
|
145
144
|
return S_OK(res.pfn)
|
|
@@ -153,7 +152,7 @@ class SandboxStoreHandlerMixin:
|
|
|
153
152
|
fileHelper.markAsTransferred()
|
|
154
153
|
sbURL = f"SB:{self.__localSEName}|{sbPath}"
|
|
155
154
|
assignTo = {key: [(sbURL, assignTo[key])] for key in assignTo}
|
|
156
|
-
result = self.
|
|
155
|
+
result = self.sandboxDB.assignSandboxesToEntities(assignTo, credDict["username"], credDict["group"])
|
|
157
156
|
if not result["OK"]:
|
|
158
157
|
return result
|
|
159
158
|
return S_OK(sbURL)
|
|
@@ -210,7 +209,9 @@ class SandboxStoreHandlerMixin:
|
|
|
210
209
|
|
|
211
210
|
sbURL = f"SB:{self.__localSEName}|{sbPath}"
|
|
212
211
|
assignTo = {key: [(sbURL, assignTo[key])] for key in assignTo}
|
|
213
|
-
if not (result := self.
|
|
212
|
+
if not (result := self.sandboxDB.assignSandboxesToEntities(assignTo, credDict["username"], credDict["group"]))[
|
|
213
|
+
"OK"
|
|
214
|
+
]:
|
|
214
215
|
return result
|
|
215
216
|
return S_OK(sbURL)
|
|
216
217
|
|
|
@@ -328,33 +329,6 @@ class SandboxStoreHandlerMixin:
|
|
|
328
329
|
|
|
329
330
|
return result
|
|
330
331
|
|
|
331
|
-
##################
|
|
332
|
-
# Assigning sbs to jobs
|
|
333
|
-
|
|
334
|
-
types_assignSandboxesToEntities = [dict]
|
|
335
|
-
|
|
336
|
-
def export_assignSandboxesToEntities(self, enDict, ownerName="", ownerGroup=""):
|
|
337
|
-
"""
|
|
338
|
-
Assign sandboxes to jobs.
|
|
339
|
-
Expects a dict of { entityId : [ ( SB, SBType ), ... ] }
|
|
340
|
-
"""
|
|
341
|
-
credDict = self.getRemoteCredentials()
|
|
342
|
-
return self.sandboxDB.assignSandboxesToEntities(
|
|
343
|
-
enDict, credDict["username"], credDict["group"], ownerName, ownerGroup
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
##################
|
|
347
|
-
# Unassign sbs to jobs
|
|
348
|
-
|
|
349
|
-
types_unassignEntities = [(list, tuple)]
|
|
350
|
-
|
|
351
|
-
def export_unassignEntities(self, entitiesList):
|
|
352
|
-
"""
|
|
353
|
-
Unassign a list of jobs
|
|
354
|
-
"""
|
|
355
|
-
credDict = self.getRemoteCredentials()
|
|
356
|
-
return self.sandboxDB.unassignEntities(entitiesList, credDict["username"], credDict["group"])
|
|
357
|
-
|
|
358
332
|
##################
|
|
359
333
|
# Getting assigned sandboxes
|
|
360
334
|
|
|
@@ -376,26 +350,6 @@ class SandboxStoreHandlerMixin:
|
|
|
376
350
|
sbDict[SBType].append(f"SB:{SEName}|{SEPFN}")
|
|
377
351
|
return S_OK(sbDict)
|
|
378
352
|
|
|
379
|
-
##################
|
|
380
|
-
# Disk space left management
|
|
381
|
-
|
|
382
|
-
types_getFreeDiskSpace = []
|
|
383
|
-
|
|
384
|
-
def export_getFreeDiskSpace(self):
|
|
385
|
-
"""Get the free disk space of the storage element
|
|
386
|
-
If no size is specified, terabytes will be used by default.
|
|
387
|
-
"""
|
|
388
|
-
|
|
389
|
-
return getDiskSpace(self.getCSOption("BasePath", "/opt/dirac/storage/sandboxes"))
|
|
390
|
-
|
|
391
|
-
types_getTotalDiskSpace = []
|
|
392
|
-
|
|
393
|
-
def export_getTotalDiskSpace(self):
|
|
394
|
-
"""Get the total disk space of the storage element
|
|
395
|
-
If no size is specified, terabytes will be used by default.
|
|
396
|
-
"""
|
|
397
|
-
return getDiskSpace(self.getCSOption("BasePath", "/opt/dirac/storage/sandboxes"), total=True)
|
|
398
|
-
|
|
399
353
|
##################
|
|
400
354
|
# Download sandboxes
|
|
401
355
|
|
|
@@ -3,7 +3,6 @@ This is a DIRAC WMS administrator interface.
|
|
|
3
3
|
"""
|
|
4
4
|
from DIRAC import S_ERROR, S_OK
|
|
5
5
|
from DIRAC.ConfigurationSystem.Client.Helpers import Registry
|
|
6
|
-
from DIRAC.ConfigurationSystem.Client.Helpers.Resources import getSites
|
|
7
6
|
from DIRAC.Core.DISET.RequestHandler import RequestHandler
|
|
8
7
|
from DIRAC.Core.Utilities.Decorators import deprecated
|
|
9
8
|
from DIRAC.Core.Utilities.ObjectLoader import ObjectLoader
|
|
@@ -151,86 +150,24 @@ class WMSAdministratorHandlerMixin:
|
|
|
151
150
|
:param str jobID: job ID
|
|
152
151
|
:return: S_OK(dict)/S_ERROR()
|
|
153
152
|
"""
|
|
154
|
-
|
|
155
|
-
# Get the pilot grid reference first from the job parameters
|
|
156
|
-
|
|
157
|
-
credDict = self.getRemoteCredentials()
|
|
158
|
-
vo = credDict.get("VO", Registry.getVOForGroup(credDict["group"]))
|
|
159
|
-
res = self.elasticJobParametersDB.getJobParameters(int(jobID), vo=vo, paramList=["Pilot_Reference"])
|
|
160
|
-
if not res["OK"]:
|
|
161
|
-
return res
|
|
162
|
-
if res["Value"].get(int(jobID)):
|
|
163
|
-
pilotReference = res["Value"][int(jobID)]["Pilot_Reference"]
|
|
164
|
-
|
|
165
|
-
if not pilotReference:
|
|
166
|
-
res = self.jobDB.getJobParameter(int(jobID), "Pilot_Reference")
|
|
167
|
-
if not res["OK"]:
|
|
168
|
-
return res
|
|
169
|
-
pilotReference = res["Value"]
|
|
170
|
-
|
|
171
|
-
if not pilotReference:
|
|
172
|
-
# Failed to get the pilot reference, try to look in the attic parameters
|
|
173
|
-
res = self.jobDB.getAtticJobParameters(int(jobID), ["Pilot_Reference"])
|
|
174
|
-
if res["OK"]:
|
|
175
|
-
c = -1
|
|
176
|
-
# Get the pilot reference for the last rescheduling cycle
|
|
177
|
-
for cycle in res["Value"]:
|
|
178
|
-
if cycle > c:
|
|
179
|
-
pilotReference = res["Value"][cycle]["Pilot_Reference"]
|
|
180
|
-
c = cycle
|
|
181
|
-
|
|
182
|
-
if pilotReference:
|
|
183
|
-
return self.pilotManager.getPilotOutput(pilotReference)
|
|
184
|
-
return S_ERROR("No pilot job reference found")
|
|
153
|
+
result = self.pilotManager.getPilots(jobID)
|
|
185
154
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
@classmethod
|
|
190
|
-
def export_getSiteSummaryWeb(cls, selectDict, sortList, startItem, maxItems):
|
|
191
|
-
"""Get the summary of the jobs running on sites in a generic format
|
|
192
|
-
|
|
193
|
-
:param dict selectDict: selectors
|
|
194
|
-
:param list sortList: sorting list
|
|
195
|
-
:param int startItem: start item number
|
|
196
|
-
:param int maxItems: maximum of items
|
|
197
|
-
|
|
198
|
-
:return: S_OK(dict)/S_ERROR()
|
|
199
|
-
"""
|
|
200
|
-
return cls.jobDB.getSiteSummaryWeb(selectDict, sortList, startItem, maxItems)
|
|
201
|
-
|
|
202
|
-
##############################################################################
|
|
203
|
-
types_getSiteSummarySelectors = []
|
|
204
|
-
|
|
205
|
-
@classmethod
|
|
206
|
-
def export_getSiteSummarySelectors(cls):
|
|
207
|
-
"""Get all the distinct selector values for the site summary web portal page
|
|
155
|
+
if not result["OK"]:
|
|
156
|
+
return result
|
|
157
|
+
pilotJobReferences = result["Value"].keys()
|
|
208
158
|
|
|
209
|
-
:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
countryList = []
|
|
223
|
-
for site in siteList:
|
|
224
|
-
if site.find(".") != -1:
|
|
225
|
-
country = site.split(".")[2].lower()
|
|
226
|
-
if country not in countryList:
|
|
227
|
-
countryList.append(country)
|
|
228
|
-
countryList.sort()
|
|
229
|
-
resultDict["Country"] = countryList
|
|
230
|
-
siteList.sort()
|
|
231
|
-
resultDict["Site"] = siteList
|
|
232
|
-
|
|
233
|
-
return S_OK(resultDict)
|
|
159
|
+
outputs = {"StdOut": "", "StdErr": ""}
|
|
160
|
+
for pilotRef in pilotJobReferences:
|
|
161
|
+
result = self.pilotManager.getPilotOutput(pilotRef)
|
|
162
|
+
if not result["OK"]:
|
|
163
|
+
stdout = f"Could not retrieve output: {result['Message']}"
|
|
164
|
+
error = f"Could not retrieve error: {result['Message']}"
|
|
165
|
+
else:
|
|
166
|
+
stdout, error = result["Value"]["StdOut"], result["Value"]["StdErr"]
|
|
167
|
+
outputs["StdOut"] += f"# PilotJobReference: {pilotRef}\n\n{stdout}\n"
|
|
168
|
+
outputs["StdErr"] += f"# PilotJobReference: {pilotRef}\n\n{error}\n"
|
|
169
|
+
|
|
170
|
+
return S_OK(outputs)
|
|
234
171
|
|
|
235
172
|
|
|
236
173
|
class WMSAdministratorHandler(WMSAdministratorHandlerMixin, RequestHandler):
|
|
@@ -1,209 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from typing import Any, Annotated, TypeAlias, Self
|
|
7
|
-
|
|
8
|
-
from pydantic import BaseModel, BeforeValidator, model_validator, field_validator, ConfigDict
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
from pydantic import PrivateAttr
|
|
5
|
+
from DIRACCommon.WorkloadManagementSystem.Utilities.JobModel import * # noqa: F401, F403
|
|
9
6
|
|
|
10
7
|
from DIRAC import gLogger
|
|
11
|
-
from DIRAC.ConfigurationSystem.Client.Helpers.
|
|
12
|
-
from DIRAC.ConfigurationSystem.Client.Helpers.Resources import getDIRACPlatforms, getSites
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# HACK: Convert appropriate iterables into sets
|
|
16
|
-
def default_set_validator(value):
|
|
17
|
-
if value is None:
|
|
18
|
-
return set()
|
|
19
|
-
elif not isinstance(value, Iterable):
|
|
20
|
-
return value
|
|
21
|
-
elif isinstance(value, (str, bytes, bytearray)):
|
|
22
|
-
return value
|
|
23
|
-
else:
|
|
24
|
-
return set(value)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
CoercibleSetStr: TypeAlias = Annotated[set[str], BeforeValidator(default_set_validator)]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class BaseJobDescriptionModel(BaseModel):
|
|
31
|
-
"""Base model for the job description (not parametric)"""
|
|
32
|
-
|
|
33
|
-
model_config = ConfigDict(validate_assignment=True)
|
|
34
|
-
|
|
35
|
-
arguments: str = ""
|
|
36
|
-
bannedSites: CoercibleSetStr = set()
|
|
37
|
-
# TODO: This should use a field factory
|
|
38
|
-
cpuTime: int = Operations().getValue("JobDescription/DefaultCPUTime", 86400)
|
|
39
|
-
executable: str
|
|
40
|
-
executionEnvironment: dict = None
|
|
41
|
-
gridCE: str = ""
|
|
42
|
-
inputSandbox: CoercibleSetStr = set()
|
|
43
|
-
inputData: CoercibleSetStr = set()
|
|
44
|
-
inputDataPolicy: str = ""
|
|
45
|
-
jobConfigArgs: str = ""
|
|
46
|
-
jobGroup: str = ""
|
|
47
|
-
jobType: str = "User"
|
|
48
|
-
jobName: str = "Name"
|
|
49
|
-
# TODO: This should be an StrEnum
|
|
50
|
-
logLevel: str = "INFO"
|
|
51
|
-
# TODO: This can't be None with this type hint
|
|
52
|
-
maxNumberOfProcessors: int = None
|
|
53
|
-
minNumberOfProcessors: int = 1
|
|
54
|
-
outputData: CoercibleSetStr = set()
|
|
55
|
-
outputPath: str = ""
|
|
56
|
-
outputSandbox: CoercibleSetStr = set()
|
|
57
|
-
outputSE: str = ""
|
|
58
|
-
platform: str = ""
|
|
59
|
-
# TODO: This should use a field factory
|
|
60
|
-
priority: int = Operations().getValue("JobDescription/DefaultPriority", 1)
|
|
61
|
-
sites: CoercibleSetStr = set()
|
|
62
|
-
stderr: str = "std.err"
|
|
63
|
-
stdout: str = "std.out"
|
|
64
|
-
tags: CoercibleSetStr = set()
|
|
65
|
-
extraFields: dict[str, Any] = {}
|
|
66
|
-
|
|
67
|
-
@field_validator("cpuTime")
|
|
68
|
-
def checkCPUTimeBounds(cls, v):
|
|
69
|
-
minCPUTime = Operations().getValue("JobDescription/MinCPUTime", 100)
|
|
70
|
-
maxCPUTime = Operations().getValue("JobDescription/MaxCPUTime", 500000)
|
|
71
|
-
if not minCPUTime <= v <= maxCPUTime:
|
|
72
|
-
raise ValueError(f"cpuTime out of bounds (must be between {minCPUTime} and {maxCPUTime})")
|
|
73
|
-
return v
|
|
74
|
-
|
|
75
|
-
@field_validator("executable")
|
|
76
|
-
def checkExecutableIsNotAnEmptyString(cls, v: str):
|
|
77
|
-
if not v:
|
|
78
|
-
raise ValueError("executable must not be an empty string")
|
|
79
|
-
return v
|
|
80
|
-
|
|
81
|
-
@field_validator("jobType")
|
|
82
|
-
def checkJobTypeIsAllowed(cls, v: str):
|
|
83
|
-
jobTypes = Operations().getValue("JobDescription/AllowedJobTypes", ["User", "Test", "Hospital"])
|
|
84
|
-
transformationTypes = Operations().getValue("Transformations/DataProcessing", [])
|
|
85
|
-
allowedTypes = jobTypes + transformationTypes
|
|
86
|
-
if v not in allowedTypes:
|
|
87
|
-
raise ValueError(f"jobType '{v}' is not allowed for this kind of user (must be in {allowedTypes})")
|
|
88
|
-
return v
|
|
89
|
-
|
|
90
|
-
@field_validator("inputData")
|
|
91
|
-
def checkInputDataDoesntContainDoubleSlashes(cls, v):
|
|
92
|
-
if v:
|
|
93
|
-
for lfn in v:
|
|
94
|
-
if lfn.find("//") > -1:
|
|
95
|
-
raise ValueError("Input data contains //")
|
|
96
|
-
return v
|
|
97
|
-
|
|
98
|
-
@field_validator("inputData")
|
|
99
|
-
def addLFNPrefixIfStringStartsWithASlash(cls, v: set[str]):
|
|
100
|
-
if v:
|
|
101
|
-
v = {lfn.strip() for lfn in v if lfn.strip()}
|
|
102
|
-
v = {f"LFN:{lfn}" if lfn.startswith("/") else lfn for lfn in v}
|
|
103
|
-
|
|
104
|
-
for lfn in v:
|
|
105
|
-
if not lfn.startswith("LFN:/"):
|
|
106
|
-
raise ValueError("Input data files must start with LFN:/")
|
|
107
|
-
return v
|
|
108
|
-
|
|
109
|
-
@model_validator(mode="after")
|
|
110
|
-
def checkNumberOfInputDataFiles(self) -> Self:
|
|
111
|
-
if self.inputData:
|
|
112
|
-
maxInputDataFiles = Operations().getValue("JobDescription/MaxInputData", 500)
|
|
113
|
-
if self.jobType == "User" and len(self.inputData) >= maxInputDataFiles:
|
|
114
|
-
raise ValueError(f"inputData contains too many files (must contain at most {maxInputDataFiles})")
|
|
115
|
-
return self
|
|
116
|
-
|
|
117
|
-
@field_validator("inputSandbox")
|
|
118
|
-
def checkLFNSandboxesAreWellFormated(cls, v: set[str]):
|
|
119
|
-
for inputSandbox in v:
|
|
120
|
-
if inputSandbox.startswith("LFN:") and not inputSandbox.startswith("LFN:/"):
|
|
121
|
-
raise ValueError("LFN files must start by LFN:/")
|
|
122
|
-
return v
|
|
123
|
-
|
|
124
|
-
@field_validator("logLevel")
|
|
125
|
-
def checkLogLevelIsValid(cls, v: str):
|
|
126
|
-
v = v.upper()
|
|
127
|
-
possibleLogLevels = gLogger.getAllPossibleLevels()
|
|
128
|
-
if v not in possibleLogLevels:
|
|
129
|
-
raise ValueError(f"Log level {v} not in {possibleLogLevels}")
|
|
130
|
-
return v
|
|
131
|
-
|
|
132
|
-
@field_validator("minNumberOfProcessors")
|
|
133
|
-
def checkMinNumberOfProcessorsBounds(cls, v):
|
|
134
|
-
minNumberOfProcessors = Operations().getValue("JobDescription/MinNumberOfProcessors", 1)
|
|
135
|
-
maxNumberOfProcessors = Operations().getValue("JobDescription/MaxNumberOfProcessors", 1024)
|
|
136
|
-
if not minNumberOfProcessors <= v <= maxNumberOfProcessors:
|
|
137
|
-
raise ValueError(
|
|
138
|
-
f"minNumberOfProcessors out of bounds (must be between {minNumberOfProcessors} and {maxNumberOfProcessors})"
|
|
139
|
-
)
|
|
140
|
-
return v
|
|
141
|
-
|
|
142
|
-
@field_validator("maxNumberOfProcessors")
|
|
143
|
-
def checkMaxNumberOfProcessorsBounds(cls, v):
|
|
144
|
-
minNumberOfProcessors = Operations().getValue("JobDescription/MinNumberOfProcessors", 1)
|
|
145
|
-
maxNumberOfProcessors = Operations().getValue("JobDescription/MaxNumberOfProcessors", 1024)
|
|
146
|
-
if not minNumberOfProcessors <= v <= maxNumberOfProcessors:
|
|
147
|
-
raise ValueError(
|
|
148
|
-
f"minNumberOfProcessors out of bounds (must be between {minNumberOfProcessors} and {maxNumberOfProcessors})"
|
|
149
|
-
)
|
|
150
|
-
return v
|
|
151
|
-
|
|
152
|
-
@model_validator(mode="after")
|
|
153
|
-
def checkThatMaxNumberOfProcessorsIsGreaterThanMinNumberOfProcessors(self) -> Self:
|
|
154
|
-
if self.maxNumberOfProcessors:
|
|
155
|
-
if self.maxNumberOfProcessors < self.minNumberOfProcessors:
|
|
156
|
-
raise ValueError("maxNumberOfProcessors must be greater than minNumberOfProcessors")
|
|
157
|
-
return self
|
|
158
|
-
|
|
159
|
-
@model_validator(mode="after")
|
|
160
|
-
def addTagsDependingOnNumberOfProcessors(self) -> Self:
|
|
161
|
-
if self.minNumberOfProcessors == self.maxNumberOfProcessors:
|
|
162
|
-
self.tags.add(f"{self.minNumberOfProcessors}Processors")
|
|
163
|
-
if self.minNumberOfProcessors > 1:
|
|
164
|
-
self.tags.add("MultiProcessor")
|
|
165
|
-
return self
|
|
8
|
+
from DIRAC.ConfigurationSystem.Client.Helpers.Resources import getSites
|
|
166
9
|
|
|
167
|
-
@field_validator("sites")
|
|
168
|
-
def checkSites(cls, v: set[str]):
|
|
169
|
-
if v:
|
|
170
|
-
res = getSites()
|
|
171
|
-
if not res["OK"]:
|
|
172
|
-
raise ValueError(res["Message"])
|
|
173
|
-
invalidSites = v - set(res["Value"]).union({"ANY"})
|
|
174
|
-
if invalidSites:
|
|
175
|
-
raise ValueError(f"Invalid sites: {' '.join(invalidSites)}")
|
|
176
|
-
return v
|
|
177
10
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if self.sites and self.bannedSites:
|
|
181
|
-
while self.bannedSites:
|
|
182
|
-
self.sites.discard(self.bannedSites.pop())
|
|
183
|
-
if not self.sites:
|
|
184
|
-
raise ValueError("sites and bannedSites are mutually exclusive")
|
|
185
|
-
return self
|
|
11
|
+
def _make_model_config(cls=None) -> BaseJobDescriptionModelConfg:
|
|
12
|
+
from DIRAC.ConfigurationSystem.Client.Helpers.Operations import Operations
|
|
186
13
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
14
|
+
ops = Operations()
|
|
15
|
+
allowedJobTypes = ops.getValue("JobDescription/AllowedJobTypes", ["User", "Test", "Hospital"])
|
|
16
|
+
allowedJobTypes += ops.getValue("Transformations/DataProcessing", [])
|
|
17
|
+
return {
|
|
18
|
+
"cpuTime": ops.getValue("JobDescription/DefaultCPUTime", 86400),
|
|
19
|
+
"priority": ops.getValue("JobDescription/DefaultPriority", 1),
|
|
20
|
+
"minCPUTime": ops.getValue("JobDescription/MinCPUTime", 100),
|
|
21
|
+
"maxCPUTime": ops.getValue("JobDescription/MaxCPUTime", 500000),
|
|
22
|
+
"allowedJobTypes": allowedJobTypes,
|
|
23
|
+
"maxInputDataFiles": ops.getValue("JobDescription/MaxInputData", 500),
|
|
24
|
+
"minNumberOfProcessors": ops.getValue("JobDescription/MinNumberOfProcessors", 1),
|
|
25
|
+
"maxNumberOfProcessors": ops.getValue("JobDescription/MaxNumberOfProcessors", 1024),
|
|
26
|
+
"minPriority": ops.getValue("JobDescription/MinPriority", 0),
|
|
27
|
+
"maxPriority": ops.getValue("JobDescription/MaxPriority", 10),
|
|
28
|
+
"possibleLogLevels": gLogger.getAllPossibleLevels(),
|
|
29
|
+
"sites": getSites(),
|
|
30
|
+
}
|
|
194
31
|
|
|
195
32
|
|
|
196
|
-
class
|
|
197
|
-
|
|
33
|
+
class BaseJobDescriptionModel(BaseJobDescriptionModel): # noqa: F405 pylint: disable=function-redefined
|
|
34
|
+
_config_builder: ClassVar = _make_model_config
|
|
198
35
|
|
|
199
|
-
owner: str
|
|
200
|
-
ownerGroup: str
|
|
201
|
-
vo: str
|
|
202
36
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if self.inputData:
|
|
206
|
-
for lfn in self.inputData:
|
|
207
|
-
if not lfn.startswith(f"LFN:/{self.vo}/"):
|
|
208
|
-
raise ValueError(f"Input data not correctly specified (must start with LFN:/{self.vo}/)")
|
|
209
|
-
return self
|
|
37
|
+
class JobDescriptionModel(JobDescriptionModel): # noqa: F405 pylint: disable=function-redefined
|
|
38
|
+
_config_builder: ClassVar = _make_model_config
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
1
|
+
"""DIRAC Workload Management System utility module to get available memory and processors"""
|
|
2
|
+
|
|
3
3
|
import multiprocessing
|
|
4
4
|
|
|
5
|
-
from DIRAC import gConfig, gLogger
|
|
5
|
+
from DIRAC import S_OK, gConfig, gLogger
|
|
6
6
|
from DIRAC.Core.Utilities.List import fromChar
|
|
7
7
|
|
|
8
8
|
|
|
@@ -139,3 +139,65 @@ def getNumberOfGPUs(siteName=None, gridCE=None, queue=None):
|
|
|
139
139
|
# 3) return 0
|
|
140
140
|
gLogger.info("NumberOfGPUs could not be found in CS")
|
|
141
141
|
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def getJobParameters(jobIDs: list[int], parName: str | None, vo: str = "") -> dict:
|
|
145
|
+
"""Utility to get a job parameter for a list of jobIDs pertaining to a VO.
|
|
146
|
+
If the jobID is not in the JobParametersDB, it will be looked up in the JobDB.
|
|
147
|
+
|
|
148
|
+
Requires direct access to the JobParametersDB and JobDB.
|
|
149
|
+
|
|
150
|
+
:param jobIDs: list of jobIDs
|
|
151
|
+
:param parName: name of the parameter to be retrieved
|
|
152
|
+
:param vo: VO of the jobIDs
|
|
153
|
+
:return: dictionary with jobID as key and the parameter as value
|
|
154
|
+
:rtype: dict
|
|
155
|
+
"""
|
|
156
|
+
from DIRAC.WorkloadManagementSystem.DB.JobParametersDB import JobParametersDB
|
|
157
|
+
from DIRAC.WorkloadManagementSystem.DB.JobDB import JobDB
|
|
158
|
+
|
|
159
|
+
elasticJobParametersDB = JobParametersDB()
|
|
160
|
+
jobDB = JobDB()
|
|
161
|
+
|
|
162
|
+
if vo: # a user is connecting, with a proxy
|
|
163
|
+
res = elasticJobParametersDB.getJobParameters(jobIDs, vo, parName)
|
|
164
|
+
if not res["OK"]:
|
|
165
|
+
return res
|
|
166
|
+
parameters = res["Value"]
|
|
167
|
+
else: # a service is connecting, no proxy, e.g. StalledJobAgent
|
|
168
|
+
q = f"SELECT JobID, VO FROM Jobs WHERE JobID IN ({','.join([str(jobID) for jobID in jobIDs])})"
|
|
169
|
+
res = jobDB._query(q)
|
|
170
|
+
if not res["OK"]:
|
|
171
|
+
return res
|
|
172
|
+
if not res["Value"]:
|
|
173
|
+
return S_OK({})
|
|
174
|
+
# get the VO for each jobID
|
|
175
|
+
voDict = {}
|
|
176
|
+
for jobID, vo in res["Value"]:
|
|
177
|
+
if vo not in voDict:
|
|
178
|
+
voDict[vo] = []
|
|
179
|
+
voDict[vo].append(jobID)
|
|
180
|
+
# get the parameters for each VO
|
|
181
|
+
parameters = {}
|
|
182
|
+
for vo, jobIDs in voDict.items():
|
|
183
|
+
res = elasticJobParametersDB.getJobParameters(jobIDs, vo, parName)
|
|
184
|
+
if not res["OK"]:
|
|
185
|
+
return res
|
|
186
|
+
parameters.update(res["Value"])
|
|
187
|
+
|
|
188
|
+
# Need anyway to get also from JobDB, for those jobs with parameters registered in MySQL or in both backends
|
|
189
|
+
res = jobDB.getJobParameters(jobIDs, parName)
|
|
190
|
+
if not res["OK"]:
|
|
191
|
+
return res
|
|
192
|
+
parametersM = res["Value"]
|
|
193
|
+
|
|
194
|
+
# and now combine
|
|
195
|
+
final = dict(parametersM)
|
|
196
|
+
# if job in JobDB, update with parameters from ES if any
|
|
197
|
+
for jobID in final:
|
|
198
|
+
final[jobID].update(parameters.get(jobID, {}))
|
|
199
|
+
# if job in ES and not in JobDB, take ES
|
|
200
|
+
for jobID in parameters:
|
|
201
|
+
if jobID not in final:
|
|
202
|
+
final[jobID] = parameters[jobID]
|
|
203
|
+
return S_OK(final)
|
|
@@ -9,10 +9,11 @@ from DIRAC import S_ERROR, S_OK, gLogger
|
|
|
9
9
|
from DIRAC.Core.Utilities import TimeUtilities
|
|
10
10
|
from DIRAC.Core.Utilities.ObjectLoader import ObjectLoader
|
|
11
11
|
from DIRAC.WorkloadManagementSystem.Client import JobStatus
|
|
12
|
+
from DIRACCommon.WorkloadManagementSystem.Utilities.JobStatusUtility import getStartAndEndTime, getNewStatus
|
|
12
13
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
14
|
-
from DIRAC.WorkloadManagementSystem.DB.JobDB import JobDB
|
|
15
15
|
from DIRAC.WorkloadManagementSystem.DB.JobLoggingDB import JobLoggingDB
|
|
16
|
+
from DIRAC.WorkloadManagementSystem.DB.JobDB import JobDB
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class JobStatusUtility:
|
|
@@ -180,66 +181,3 @@ class JobStatusUtility:
|
|
|
180
181
|
return result
|
|
181
182
|
|
|
182
183
|
return S_OK((attrNames, attrValues))
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def getStartAndEndTime(startTime, endTime, updateTimes, timeStamps, statusDict):
|
|
186
|
-
newStat = ""
|
|
187
|
-
firstUpdate = TimeUtilities.toEpoch(TimeUtilities.fromString(updateTimes[0]))
|
|
188
|
-
for ts, st in timeStamps:
|
|
189
|
-
if firstUpdate >= ts:
|
|
190
|
-
newStat = st
|
|
191
|
-
# Pick up start and end times from all updates
|
|
192
|
-
for updTime in updateTimes:
|
|
193
|
-
sDict = statusDict[updTime]
|
|
194
|
-
newStat = sDict.get("Status", newStat)
|
|
195
|
-
|
|
196
|
-
if not startTime and newStat == JobStatus.RUNNING:
|
|
197
|
-
# Pick up the start date when the job starts running if not existing
|
|
198
|
-
startTime = updTime
|
|
199
|
-
elif not endTime and newStat in JobStatus.JOB_FINAL_STATES:
|
|
200
|
-
# Pick up the end time when the job is in a final status
|
|
201
|
-
endTime = updTime
|
|
202
|
-
|
|
203
|
-
return startTime, endTime
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def getNewStatus(
|
|
207
|
-
jobID: int,
|
|
208
|
-
updateTimes: list[datetime],
|
|
209
|
-
lastTime: datetime,
|
|
210
|
-
statusDict: dict[datetime, Any],
|
|
211
|
-
currentStatus,
|
|
212
|
-
force: bool,
|
|
213
|
-
log,
|
|
214
|
-
):
|
|
215
|
-
status = ""
|
|
216
|
-
minor = ""
|
|
217
|
-
application = ""
|
|
218
|
-
# Get the last status values looping on the most recent upupdateTimes in chronological order
|
|
219
|
-
for updTime in [dt for dt in updateTimes if dt >= lastTime]:
|
|
220
|
-
sDict = statusDict[updTime]
|
|
221
|
-
log.debug(f"\tTime {updTime} - Statuses {str(sDict)}")
|
|
222
|
-
status = sDict.get("Status", currentStatus)
|
|
223
|
-
# evaluate the state machine if the status is changing
|
|
224
|
-
if not force and status != currentStatus:
|
|
225
|
-
res = JobStatus.JobsStateMachine(currentStatus).getNextState(status)
|
|
226
|
-
if not res["OK"]:
|
|
227
|
-
return res
|
|
228
|
-
newStat = res["Value"]
|
|
229
|
-
# If the JobsStateMachine does not accept the candidate, don't update
|
|
230
|
-
if newStat != status:
|
|
231
|
-
# keeping the same status
|
|
232
|
-
log.error(
|
|
233
|
-
f"Job Status Error: {jobID} can't move from {currentStatus} to {status}: using {newStat}",
|
|
234
|
-
)
|
|
235
|
-
status = newStat
|
|
236
|
-
sDict["Status"] = newStat
|
|
237
|
-
# Change the source to indicate this is not what was requested
|
|
238
|
-
source = sDict.get("Source", "")
|
|
239
|
-
sDict["Source"] = source + "(SM)"
|
|
240
|
-
# at this stage status == newStat. Set currentStatus to this new status
|
|
241
|
-
currentStatus = newStat
|
|
242
|
-
|
|
243
|
-
minor = sDict.get("MinorStatus", minor)
|
|
244
|
-
application = sDict.get("ApplicationStatus", application)
|
|
245
|
-
return S_OK((status, minor, application))
|