DIRAC 9.0.0a42__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.
Files changed (236) hide show
  1. DIRAC/AccountingSystem/Client/AccountingCLI.py +0 -140
  2. DIRAC/AccountingSystem/Client/DataStoreClient.py +0 -13
  3. DIRAC/AccountingSystem/Client/Types/BaseAccountingType.py +0 -7
  4. DIRAC/AccountingSystem/ConfigTemplate.cfg +0 -5
  5. DIRAC/AccountingSystem/Service/DataStoreHandler.py +0 -72
  6. DIRAC/ConfigurationSystem/Client/Helpers/CSGlobals.py +0 -9
  7. DIRAC/ConfigurationSystem/Client/Helpers/Registry.py +38 -26
  8. DIRAC/ConfigurationSystem/Client/Helpers/Resources.py +11 -43
  9. DIRAC/ConfigurationSystem/Client/Helpers/test/Test_Helpers.py +0 -16
  10. DIRAC/ConfigurationSystem/Client/LocalConfiguration.py +14 -8
  11. DIRAC/ConfigurationSystem/Client/PathFinder.py +47 -8
  12. DIRAC/ConfigurationSystem/Client/SyncPlugins/CERNLDAPSyncPlugin.py +4 -1
  13. DIRAC/ConfigurationSystem/Client/VOMS2CSSynchronizer.py +32 -19
  14. DIRAC/ConfigurationSystem/Client/test/Test_PathFinder.py +41 -1
  15. DIRAC/ConfigurationSystem/private/RefresherBase.py +4 -2
  16. DIRAC/Core/Base/API.py +4 -7
  17. DIRAC/Core/Base/SQLAlchemyDB.py +1 -0
  18. DIRAC/Core/DISET/ServiceReactor.py +11 -3
  19. DIRAC/Core/DISET/private/BaseClient.py +1 -2
  20. DIRAC/Core/DISET/private/Transports/M2SSLTransport.py +9 -7
  21. DIRAC/Core/DISET/private/Transports/SSL/M2Utils.py +3 -1
  22. DIRAC/Core/LCG/GOCDBClient.py +5 -7
  23. DIRAC/Core/Security/DiracX.py +31 -17
  24. DIRAC/Core/Security/IAMService.py +5 -10
  25. DIRAC/Core/Security/Locations.py +27 -18
  26. DIRAC/Core/Security/ProxyInfo.py +9 -5
  27. DIRAC/Core/Security/VOMSService.py +2 -4
  28. DIRAC/Core/Security/m2crypto/X509Certificate.py +4 -6
  29. DIRAC/Core/Security/m2crypto/asn1_utils.py +17 -5
  30. DIRAC/Core/Security/test/test_diracx_token_from_pem.py +161 -0
  31. DIRAC/Core/Tornado/Client/ClientSelector.py +4 -1
  32. DIRAC/Core/Tornado/Server/TornadoService.py +1 -1
  33. DIRAC/Core/Utilities/CGroups2.py +328 -0
  34. DIRAC/Core/Utilities/ClassAd/ClassAdLight.py +4 -290
  35. DIRAC/Core/Utilities/DErrno.py +5 -309
  36. DIRAC/Core/Utilities/Extensions.py +10 -1
  37. DIRAC/Core/Utilities/File.py +1 -1
  38. DIRAC/Core/Utilities/Graphs/GraphData.py +1 -1
  39. DIRAC/Core/Utilities/Graphs/GraphUtilities.py +6 -1
  40. DIRAC/Core/Utilities/JDL.py +1 -195
  41. DIRAC/Core/Utilities/List.py +1 -124
  42. DIRAC/Core/Utilities/MySQL.py +103 -99
  43. DIRAC/Core/Utilities/Os.py +32 -1
  44. DIRAC/Core/Utilities/Platform.py +2 -107
  45. DIRAC/Core/Utilities/Proxy.py +0 -4
  46. DIRAC/Core/Utilities/ReturnValues.py +7 -252
  47. DIRAC/Core/Utilities/StateMachine.py +12 -178
  48. DIRAC/Core/Utilities/Subprocess.py +35 -14
  49. DIRAC/Core/Utilities/TimeUtilities.py +10 -253
  50. DIRAC/Core/Utilities/test/Test_JDL.py +0 -3
  51. DIRAC/Core/Utilities/test/Test_Profiler.py +20 -20
  52. DIRAC/Core/scripts/dirac_agent.py +1 -1
  53. DIRAC/Core/scripts/dirac_apptainer_exec.py +72 -46
  54. DIRAC/Core/scripts/dirac_configure.py +1 -3
  55. DIRAC/Core/scripts/dirac_install_db.py +24 -6
  56. DIRAC/Core/scripts/dirac_platform.py +1 -92
  57. DIRAC/DataManagementSystem/Agent/FTS3Agent.py +8 -7
  58. DIRAC/DataManagementSystem/Agent/RequestOperations/RemoveFile.py +7 -6
  59. DIRAC/DataManagementSystem/Client/FTS3Job.py +71 -34
  60. DIRAC/DataManagementSystem/DB/FTS3DB.py +7 -3
  61. DIRAC/DataManagementSystem/DB/FileCatalogComponents/DatasetManager/DatasetManager.py +1 -1
  62. DIRAC/DataManagementSystem/DB/FileCatalogDB.sql +9 -9
  63. DIRAC/DataManagementSystem/DB/FileCatalogWithFkAndPsDB.sql +9 -9
  64. DIRAC/DataManagementSystem/Utilities/DMSHelpers.py +6 -2
  65. DIRAC/DataManagementSystem/scripts/dirac_admin_allow_se.py +13 -8
  66. DIRAC/DataManagementSystem/scripts/dirac_admin_ban_se.py +13 -8
  67. DIRAC/DataManagementSystem/scripts/dirac_dms_create_moving_request.py +2 -0
  68. DIRAC/DataManagementSystem/scripts/dirac_dms_protocol_matrix.py +0 -1
  69. DIRAC/FrameworkSystem/Client/BundleDeliveryClient.py +2 -7
  70. DIRAC/FrameworkSystem/Client/ComponentInstaller.py +9 -4
  71. DIRAC/FrameworkSystem/Client/ProxyManagerClient.py +5 -2
  72. DIRAC/FrameworkSystem/Client/SystemAdministratorClientCLI.py +11 -6
  73. DIRAC/FrameworkSystem/ConfigTemplate.cfg +2 -0
  74. DIRAC/FrameworkSystem/DB/AuthDB.py +3 -3
  75. DIRAC/FrameworkSystem/DB/InstalledComponentsDB.py +4 -4
  76. DIRAC/FrameworkSystem/DB/ProxyDB.py +11 -3
  77. DIRAC/FrameworkSystem/DB/TokenDB.py +1 -1
  78. DIRAC/FrameworkSystem/Service/ProxyManagerHandler.py +8 -6
  79. DIRAC/FrameworkSystem/Utilities/MonitoringUtilities.py +2 -19
  80. DIRAC/FrameworkSystem/Utilities/TokenManagementUtilities.py +3 -2
  81. DIRAC/FrameworkSystem/Utilities/diracx.py +36 -14
  82. DIRAC/FrameworkSystem/private/authorization/AuthServer.py +2 -2
  83. DIRAC/FrameworkSystem/scripts/dirac_admin_update_pilot.py +18 -11
  84. DIRAC/FrameworkSystem/scripts/dirac_login.py +2 -2
  85. DIRAC/FrameworkSystem/scripts/dirac_proxy_init.py +7 -8
  86. DIRAC/Interfaces/API/Dirac.py +27 -15
  87. DIRAC/Interfaces/API/DiracAdmin.py +45 -17
  88. DIRAC/Interfaces/API/Job.py +9 -13
  89. DIRAC/Interfaces/scripts/dirac_admin_allow_site.py +12 -18
  90. DIRAC/Interfaces/scripts/dirac_admin_ban_site.py +12 -10
  91. DIRAC/Interfaces/scripts/dirac_admin_get_site_mask.py +4 -13
  92. DIRAC/Interfaces/scripts/dirac_admin_reset_job.py +3 -6
  93. DIRAC/Interfaces/scripts/dirac_wms_job_parameters.py +0 -1
  94. DIRAC/MonitoringSystem/Client/Types/WMSHistory.py +4 -0
  95. DIRAC/MonitoringSystem/Client/WebAppClient.py +26 -0
  96. DIRAC/MonitoringSystem/ConfigTemplate.cfg +9 -0
  97. DIRAC/MonitoringSystem/DB/MonitoringDB.py +6 -25
  98. DIRAC/MonitoringSystem/Service/MonitoringHandler.py +0 -33
  99. DIRAC/MonitoringSystem/Service/WebAppHandler.py +599 -0
  100. DIRAC/MonitoringSystem/private/MainReporter.py +0 -3
  101. DIRAC/ProductionSystem/DB/ProductionDB.sql +4 -4
  102. DIRAC/ProductionSystem/scripts/dirac_prod_get.py +2 -2
  103. DIRAC/ProductionSystem/scripts/dirac_prod_get_all.py +2 -2
  104. DIRAC/ProductionSystem/scripts/dirac_prod_get_trans.py +2 -3
  105. DIRAC/RequestManagementSystem/Agent/RequestExecutingAgent.py +8 -6
  106. DIRAC/RequestManagementSystem/Agent/RequestOperations/ForwardDISET.py +2 -14
  107. DIRAC/RequestManagementSystem/Client/ReqClient.py +66 -13
  108. DIRAC/RequestManagementSystem/ConfigTemplate.cfg +6 -6
  109. DIRAC/RequestManagementSystem/DB/RequestDB.py +10 -5
  110. DIRAC/RequestManagementSystem/DB/test/RMSTestScenari.py +2 -0
  111. DIRAC/RequestManagementSystem/private/RequestValidator.py +40 -46
  112. DIRAC/ResourceStatusSystem/Client/SiteStatus.py +4 -2
  113. DIRAC/ResourceStatusSystem/Command/FreeDiskSpaceCommand.py +3 -1
  114. DIRAC/ResourceStatusSystem/DB/ResourceManagementDB.py +8 -8
  115. DIRAC/ResourceStatusSystem/DB/ResourceStatusDB.py +2 -2
  116. DIRAC/ResourceStatusSystem/Utilities/CSHelpers.py +2 -31
  117. DIRAC/ResourceStatusSystem/scripts/dirac_rss_set_status.py +30 -12
  118. DIRAC/Resources/Catalog/RucioFileCatalogClient.py +195 -1
  119. DIRAC/Resources/Catalog/test/Test_RucioFileCatalogClient.py +181 -0
  120. DIRAC/Resources/Computing/AREXComputingElement.py +25 -8
  121. DIRAC/Resources/Computing/BatchSystems/Condor.py +126 -108
  122. DIRAC/Resources/Computing/BatchSystems/SLURM.py +5 -1
  123. DIRAC/Resources/Computing/BatchSystems/test/Test_SLURM.py +46 -0
  124. DIRAC/Resources/Computing/ComputingElement.py +1 -1
  125. DIRAC/Resources/Computing/HTCondorCEComputingElement.py +44 -44
  126. DIRAC/Resources/Computing/InProcessComputingElement.py +4 -2
  127. DIRAC/Resources/Computing/LocalComputingElement.py +1 -18
  128. DIRAC/Resources/Computing/SSHBatchComputingElement.py +1 -17
  129. DIRAC/Resources/Computing/SSHComputingElement.py +1 -18
  130. DIRAC/Resources/Computing/SingularityComputingElement.py +19 -5
  131. DIRAC/Resources/Computing/test/Test_HTCondorCEComputingElement.py +67 -49
  132. DIRAC/Resources/Computing/test/Test_PoolComputingElement.py +2 -1
  133. DIRAC/Resources/IdProvider/CheckInIdProvider.py +13 -0
  134. DIRAC/Resources/IdProvider/IdProviderFactory.py +11 -3
  135. DIRAC/Resources/MessageQueue/StompMQConnector.py +1 -1
  136. DIRAC/Resources/Storage/GFAL2_StorageBase.py +24 -15
  137. DIRAC/Resources/Storage/OccupancyPlugins/WLCGAccountingHTTPJson.py +1 -3
  138. DIRAC/Resources/Storage/StorageBase.py +4 -2
  139. DIRAC/Resources/Storage/StorageElement.py +6 -7
  140. DIRAC/StorageManagementSystem/DB/StorageManagementDB.sql +2 -2
  141. DIRAC/TransformationSystem/Agent/TaskManagerAgentBase.py +10 -16
  142. DIRAC/TransformationSystem/Agent/TransformationAgent.py +22 -1
  143. DIRAC/TransformationSystem/Agent/TransformationCleaningAgent.py +16 -16
  144. DIRAC/TransformationSystem/Client/TaskManager.py +2 -4
  145. DIRAC/TransformationSystem/Client/Transformation.py +6 -7
  146. DIRAC/TransformationSystem/Client/TransformationClient.py +21 -11
  147. DIRAC/TransformationSystem/Client/Utilities.py +9 -0
  148. DIRAC/TransformationSystem/DB/TransformationDB.py +11 -14
  149. DIRAC/TransformationSystem/DB/TransformationDB.sql +9 -9
  150. DIRAC/TransformationSystem/Service/TransformationManagerHandler.py +0 -333
  151. DIRAC/TransformationSystem/Utilities/ReplicationCLIParameters.py +3 -3
  152. DIRAC/TransformationSystem/Utilities/TransformationInfo.py +7 -5
  153. DIRAC/TransformationSystem/scripts/dirac_production_runjoblocal.py +2 -4
  154. DIRAC/TransformationSystem/test/Test_TransformationInfo.py +22 -15
  155. DIRAC/TransformationSystem/test/Test_replicationTransformation.py +5 -6
  156. DIRAC/Workflow/Modules/test/Test_Modules.py +5 -0
  157. DIRAC/WorkloadManagementSystem/Agent/JobAgent.py +38 -26
  158. DIRAC/WorkloadManagementSystem/Agent/JobCleaningAgent.py +12 -8
  159. DIRAC/WorkloadManagementSystem/Agent/PilotSyncAgent.py +4 -3
  160. DIRAC/WorkloadManagementSystem/Agent/PushJobAgent.py +13 -13
  161. DIRAC/WorkloadManagementSystem/Agent/SiteDirector.py +18 -14
  162. DIRAC/WorkloadManagementSystem/Agent/StalledJobAgent.py +18 -51
  163. DIRAC/WorkloadManagementSystem/Agent/StatesAccountingAgent.py +41 -1
  164. DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_JobAgent.py +45 -4
  165. DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_JobCleaningAgent.py +7 -9
  166. DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_PushJobAgent.py +1 -0
  167. DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_SiteDirector.py +9 -2
  168. DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_StalledJobAgent.py +4 -5
  169. DIRAC/WorkloadManagementSystem/Client/DownloadInputData.py +9 -9
  170. DIRAC/WorkloadManagementSystem/Client/InputDataResolution.py +6 -6
  171. DIRAC/WorkloadManagementSystem/Client/JobMonitoringClient.py +10 -11
  172. DIRAC/WorkloadManagementSystem/Client/JobReport.py +1 -1
  173. DIRAC/WorkloadManagementSystem/Client/JobState/CachedJobState.py +3 -0
  174. DIRAC/WorkloadManagementSystem/Client/JobState/JobManifest.py +32 -261
  175. DIRAC/WorkloadManagementSystem/Client/JobState/JobState.py +6 -0
  176. DIRAC/WorkloadManagementSystem/Client/JobStateUpdateClient.py +3 -0
  177. DIRAC/WorkloadManagementSystem/Client/JobStatus.py +8 -152
  178. DIRAC/WorkloadManagementSystem/Client/PoolXMLSlice.py +12 -19
  179. DIRAC/WorkloadManagementSystem/Client/SandboxStoreClient.py +25 -38
  180. DIRAC/WorkloadManagementSystem/Client/WMSClient.py +2 -3
  181. DIRAC/WorkloadManagementSystem/Client/test/Test_Client_DownloadInputData.py +29 -0
  182. DIRAC/WorkloadManagementSystem/ConfigTemplate.cfg +4 -8
  183. DIRAC/WorkloadManagementSystem/DB/JobDB.py +89 -132
  184. DIRAC/WorkloadManagementSystem/DB/JobDB.sql +8 -8
  185. DIRAC/WorkloadManagementSystem/DB/JobDBUtils.py +18 -147
  186. DIRAC/WorkloadManagementSystem/DB/JobLoggingDB.py +19 -6
  187. DIRAC/WorkloadManagementSystem/DB/JobParametersDB.py +9 -9
  188. DIRAC/WorkloadManagementSystem/DB/PilotAgentsDB.py +16 -5
  189. DIRAC/WorkloadManagementSystem/DB/PilotAgentsDB.sql +3 -3
  190. DIRAC/WorkloadManagementSystem/DB/SandboxMetadataDB.py +44 -82
  191. DIRAC/WorkloadManagementSystem/DB/StatusUtils.py +125 -0
  192. DIRAC/WorkloadManagementSystem/DB/tests/Test_JobDB.py +1 -1
  193. DIRAC/WorkloadManagementSystem/DB/tests/Test_StatusUtils.py +28 -0
  194. DIRAC/WorkloadManagementSystem/Executor/JobSanity.py +5 -4
  195. DIRAC/WorkloadManagementSystem/Executor/JobScheduling.py +4 -0
  196. DIRAC/WorkloadManagementSystem/FutureClient/JobStateUpdateClient.py +75 -33
  197. DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py +22 -11
  198. DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapperTemplate.py +9 -10
  199. DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapper.py +60 -10
  200. DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapperTemplate.py +4 -0
  201. DIRAC/WorkloadManagementSystem/Service/JobManagerHandler.py +33 -154
  202. DIRAC/WorkloadManagementSystem/Service/JobMonitoringHandler.py +5 -323
  203. DIRAC/WorkloadManagementSystem/Service/JobStateUpdateHandler.py +0 -16
  204. DIRAC/WorkloadManagementSystem/Service/PilotManagerHandler.py +6 -103
  205. DIRAC/WorkloadManagementSystem/Service/SandboxStoreHandler.py +7 -53
  206. DIRAC/WorkloadManagementSystem/Service/WMSAdministratorHandler.py +16 -79
  207. DIRAC/WorkloadManagementSystem/Service/WMSUtilities.py +4 -18
  208. DIRAC/WorkloadManagementSystem/Utilities/JobModel.py +28 -209
  209. DIRAC/WorkloadManagementSystem/Utilities/JobParameters.py +65 -3
  210. DIRAC/WorkloadManagementSystem/Utilities/JobStatusUtility.py +2 -64
  211. DIRAC/WorkloadManagementSystem/Utilities/ParametricJob.py +7 -171
  212. DIRAC/WorkloadManagementSystem/Utilities/PilotCStoJSONSynchronizer.py +73 -7
  213. DIRAC/WorkloadManagementSystem/Utilities/PilotWrapper.py +41 -11
  214. DIRAC/WorkloadManagementSystem/Utilities/RemoteRunner.py +16 -0
  215. DIRAC/WorkloadManagementSystem/Utilities/Utils.py +36 -1
  216. DIRAC/WorkloadManagementSystem/Utilities/jobAdministration.py +15 -0
  217. DIRAC/WorkloadManagementSystem/Utilities/test/Test_JobModel.py +1 -15
  218. DIRAC/WorkloadManagementSystem/Utilities/test/Test_ParametricJob.py +45 -128
  219. DIRAC/WorkloadManagementSystem/Utilities/test/Test_PilotWrapper.py +16 -0
  220. DIRAC/WorkloadManagementSystem/scripts/dirac_jobexec.py +7 -2
  221. DIRAC/WorkloadManagementSystem/scripts/dirac_wms_pilot_job_info.py +1 -1
  222. DIRAC/__init__.py +62 -60
  223. DIRAC/tests/Utilities/testJobDefinitions.py +22 -28
  224. {DIRAC-9.0.0a42.dist-info → dirac-9.0.7.dist-info}/METADATA +8 -5
  225. {DIRAC-9.0.0a42.dist-info → dirac-9.0.7.dist-info}/RECORD +229 -228
  226. {DIRAC-9.0.0a42.dist-info → dirac-9.0.7.dist-info}/WHEEL +1 -1
  227. {DIRAC-9.0.0a42.dist-info → dirac-9.0.7.dist-info}/entry_points.txt +0 -3
  228. DIRAC/Core/Utilities/test/Test_List.py +0 -150
  229. DIRAC/Core/Utilities/test/Test_Time.py +0 -88
  230. DIRAC/Resources/Computing/PilotBundle.py +0 -70
  231. DIRAC/TransformationSystem/scripts/dirac_transformation_archive.py +0 -30
  232. DIRAC/TransformationSystem/scripts/dirac_transformation_clean.py +0 -30
  233. DIRAC/TransformationSystem/scripts/dirac_transformation_remove_output.py +0 -30
  234. DIRAC/WorkloadManagementSystem/Utilities/test/Test_JobManager.py +0 -58
  235. {DIRAC-9.0.0a42.dist-info → dirac-9.0.7.dist-info/licenses}/LICENSE +0 -0
  236. {DIRAC-9.0.0a42.dist-info → dirac-9.0.7.dist-info}/top_level.txt +0 -0
@@ -28,6 +28,7 @@ from DIRAC.ConfigurationSystem.Client.Helpers.Resources import getCESiteMapping
28
28
  from DIRAC.Core.Base.DB import DB
29
29
  from DIRAC.Core.Utilities import DErrno
30
30
  from DIRAC.Core.Utilities.MySQL import _quotedList
31
+ from DIRAC.Core.Utilities.ReturnValues import returnValueOrRaise
31
32
  from DIRAC.FrameworkSystem.Client.Logger import contextLogger
32
33
  from DIRAC.ResourceStatusSystem.Client.SiteStatus import SiteStatus
33
34
  from DIRAC.WorkloadManagementSystem.Client import PilotStatus
@@ -401,9 +402,18 @@ AND SubmissionTime < DATE_SUB(UTC_TIMESTAMP(),INTERVAL %d DAY)"
401
402
  if result["Value"]:
402
403
  return int(result["Value"][0][0])
403
404
  return 0
404
- refString = ",".join(["'" + ref + "'" for ref in pilotRef])
405
- req = f"SELECT PilotID from PilotAgents WHERE PilotJobReference in ( {refString} )"
406
- result = self._query(req)
405
+
406
+ sqlCmd = "CREATE TEMPORARY TABLE to_select_PilotAgents (PilotID VARCHAR(255) NOT NULL, PRIMARY KEY (PilotID)) ENGINE=MEMORY;"
407
+ returnValueOrRaise(self._update(sqlCmd))
408
+ try:
409
+ sqlCmd = "INSERT INTO to_select_PilotAgents (PilotID) VALUES ( %s )"
410
+ returnValueOrRaise(self._updatemany(sqlCmd, [(p,) for p in pilotRef]))
411
+ sqlCmd = "SELECT PilotID FROM PilotAgents JOIN to_select_PilotAgents USING (PilotID)"
412
+ result = self._query(sqlCmd)
413
+ finally:
414
+ sqlCmd = "DROP TEMPORARY TABLE to_select_PilotAgents"
415
+ returnValueOrRaise(self._update(sqlCmd))
416
+
407
417
  if not result["OK"]:
408
418
  return []
409
419
  if result["Value"]:
@@ -437,9 +447,10 @@ AND SubmissionTime < DATE_SUB(UTC_TIMESTAMP(),INTERVAL %d DAY)"
437
447
  """Get IDs of Jobs that were executed by a pilot"""
438
448
  cmd = "SELECT pilotID,JobID FROM JobToPilotMapping "
439
449
  if isinstance(pilotID, list):
440
- cmd = cmd + " WHERE pilotID IN (%s)" % ",".join(["%s" % x for x in pilotID])
450
+ pilotIDs_string = ",".join(str(int(x)) for x in pilotID)
451
+ cmd = f"{cmd} WHERE pilotID IN ({pilotIDs_string})"
441
452
  else:
442
- cmd = cmd + f" WHERE pilotID = {pilotID}"
453
+ cmd = f"{cmd} WHERE pilotID = {pilotID}"
443
454
 
444
455
  result = self._query(cmd)
445
456
  if not result["OK"]:
@@ -46,7 +46,7 @@ CREATE TABLE `PilotAgents` (
46
46
  KEY `PilotJobReference` (`PilotJobReference`),
47
47
  KEY `Status` (`Status`),
48
48
  KEY `Statuskey` (`GridSite`,`DestinationSite`,`Status`)
49
- ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
49
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
50
50
 
51
51
 
52
52
  DROP TABLE IF EXISTS `JobToPilotMapping`;
@@ -56,7 +56,7 @@ CREATE TABLE `JobToPilotMapping` (
56
56
  `StartTime` DATETIME NOT NULL,
57
57
  KEY `JobID` (`JobID`),
58
58
  KEY `PilotID` (`PilotID`)
59
- ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
59
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
60
60
 
61
61
  DROP TABLE IF EXISTS `PilotOutput`;
62
62
  CREATE TABLE `PilotOutput` (
@@ -64,4 +64,4 @@ CREATE TABLE `PilotOutput` (
64
64
  `StdOutput` MEDIUMTEXT,
65
65
  `StdError` MEDIUMTEXT,
66
66
  PRIMARY KEY (`PilotID`)
67
- ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
67
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -1,10 +1,11 @@
1
1
  """ SandboxMetadataDB class is a front-end to the metadata for sandboxes
2
2
  """
3
- from DIRAC import S_ERROR, S_OK, gLogger
3
+ from DIRAC import S_ERROR, S_OK
4
4
  from DIRAC.ConfigurationSystem.Client.Helpers import Registry
5
5
  from DIRAC.Core.Base.DB import DB
6
6
  from DIRAC.Core.Security import Properties
7
7
  from DIRAC.Core.Utilities import List
8
+ from DIRAC.Core.Utilities.ReturnValues import convertToReturnValue, returnValueOrRaise
8
9
 
9
10
 
10
11
  class SandboxMetadataDB(DB):
@@ -63,7 +64,7 @@ class SandboxMetadataDB(DB):
63
64
  "Type": "VARCHAR(64) NOT NULL",
64
65
  },
65
66
  "Indexes": {"Entity": ["EntityId"], "SBIndex": ["SBId"]},
66
- "UniqueIndexes": {"Mapping": ["SBId", "EntityId", "Type"]},
67
+ "PrimaryKey": ["SBId", "EntityId", "Type"],
67
68
  }
68
69
 
69
70
  for tableName in self.__tablesDesc:
@@ -219,60 +220,27 @@ class SandboxMetadataDB(DB):
219
220
  return result
220
221
  return S_OK(assigned)
221
222
 
222
- def __filterEntitiesByRequester(self, entitiesList, requesterName, requesterGroup):
223
+ @convertToReturnValue
224
+ def unassignEntities(self, entities: list):
223
225
  """
224
- Given a list of entities and a requester, return the ones that the requester is allowed to modify
225
- """
226
- sqlCond = ["s.OwnerId=o.OwnerId", "s.SBId=e.SBId"]
227
- requesterProps = Registry.getPropertiesForEntity(requesterGroup, name=requesterName)
228
- if Properties.JOB_ADMINISTRATOR in requesterProps:
229
- # Do nothing, just ensure it doesn't fit in the other cases
230
- pass
231
- elif Properties.JOB_SHARING in requesterProps:
232
- sqlCond.append(f"o.OwnerGroup='{requesterGroup}'")
233
- elif Properties.NORMAL_USER in requesterProps:
234
- sqlCond.append(f"o.OwnerGroup='{requesterGroup}'")
235
- sqlCond.append(f"o.Owner='{requesterName}'")
236
- else:
237
- return S_ERROR("Not authorized to access sandbox")
238
- for i in range(len(entitiesList)):
239
- entitiesList[i] = self._escapeString(entitiesList[i])["Value"]
240
- if len(entitiesList) == 1:
241
- sqlCond.append(f"e.EntityId = {entitiesList[0]}")
242
- else:
243
- sqlCond.append(f"e.EntityId in ( {', '.join(entitiesList)} )")
244
- sqlCmd = "SELECT DISTINCT e.EntityId FROM `sb_EntityMapping` e, `sb_SandBoxes` s, `sb_Owners` o WHERE"
245
- sqlCmd = f"{sqlCmd} {' AND '.join(sqlCond)}"
246
- result = self._query(sqlCmd)
247
- if not result["OK"]:
248
- return result
249
- return S_OK([row[0] for row in result["Value"]])
250
-
251
- def unassignEntities(self, entities, requesterName, requesterGroup):
252
- """
253
- Unassign jobs to sandboxes
226
+ Unassign entities to sandboxes. Entities are a list of strings, e.g. ['job:1234', 'job:5678'].
254
227
 
255
228
  :param list entities: list of entities to unassign
256
229
  """
257
- updated = 0
258
230
  if not entities:
259
- return S_OK()
260
- result = self.__filterEntitiesByRequester(entities, requesterName, requesterGroup)
261
- if not result["OK"]:
262
- gLogger.error("Cannot filter entities", result["Message"])
263
- return result
264
- ids = result["Value"]
265
- if not ids:
266
- return S_OK(0)
267
- sqlCmd = "DELETE FROM `sb_EntityMapping` WHERE EntityId in ( %s )" % ", ".join(
268
- ["'%s'" % str(eid) for eid in ids]
269
- )
270
- result = self._update(sqlCmd)
271
- if not result["OK"]:
272
- gLogger.error("Cannot unassign entities", result["Message"])
273
- else:
274
- updated += 1
275
- return S_OK(updated)
231
+ return None
232
+
233
+ sqlCmd = "CREATE TEMPORARY TABLE to_delete_EntityId (EntityId VARCHAR(128) NOT NULL, PRIMARY KEY (EntityId)) ENGINE=MEMORY;"
234
+ returnValueOrRaise(self._update(sqlCmd))
235
+ try:
236
+ sqlCmd = "INSERT INTO to_delete_EntityId (EntityId) VALUES ( %s )"
237
+ returnValueOrRaise(self._updatemany(sqlCmd, [(e,) for e in entities]))
238
+ sqlCmd = "DELETE m from `sb_EntityMapping` m JOIN to_delete_EntityId t USING (EntityId)"
239
+ returnValueOrRaise(self._update(sqlCmd))
240
+ finally:
241
+ sqlCmd = "DROP TEMPORARY TABLE to_delete_EntityId"
242
+ returnValueOrRaise(self._update(sqlCmd))
243
+ return 1
276
244
 
277
245
  def getSandboxesAssignedToEntity(self, entityId, requesterName, requesterGroup, requestedVO):
278
246
  """
@@ -315,22 +283,37 @@ class SandboxMetadataDB(DB):
315
283
  "TIMESTAMPDIFF( DAY, LastAccessTime, UTC_TIMESTAMP() ) >= %d" % self.__assignedSBGraceDays,
316
284
  f"! Assigned AND TIMESTAMPDIFF( DAY, LastAccessTime, UTC_TIMESTAMP() ) >= {self.__unassignedSBGraceDays}",
317
285
  ]
318
- sqlCmd = f"SELECT SBId, SEName, SEPFN FROM `sb_SandBoxes` WHERE ( {' ) OR ( '.join(sqlCond)} )"
286
+ # Exclude sandboxes that are in S3 as those are handled by DiracX
287
+ sqlCmd = f"SELECT SBId, SEName, SEPFN FROM `sb_SandBoxes` WHERE SEPFN not like '/S3/%' AND (( {' ) OR ( '.join(sqlCond)} ))"
319
288
  return self._query(sqlCmd)
320
289
 
290
+ @convertToReturnValue
321
291
  def deleteSandboxes(self, SBIdList):
322
292
  """
323
- Delete sandboxes
293
+ Delete sandboxes using a temporary table for efficiency and consistency.
324
294
  """
325
- sqlSBList = ", ".join([str(sbid) for sbid in SBIdList])
326
- for table in ("sb_SandBoxes", "sb_EntityMapping"):
327
- sqlCmd = f"DELETE FROM `{table}` WHERE SBId IN ( {sqlSBList} )"
328
- result = self._update(sqlCmd)
329
- if not result["OK"]:
330
- return result
295
+ if not SBIdList:
296
+ return S_OK()
297
+ # Create temporary table
298
+ sqlCmd = "CREATE TEMPORARY TABLE to_delete_SBId (SBId INTEGER(10) UNSIGNED NOT NULL, PRIMARY KEY (SBId)) ENGINE=MEMORY;"
299
+ returnValueOrRaise(self._update(sqlCmd))
300
+ try:
301
+ # Insert SBIds into temporary table
302
+ sqlCmd = "INSERT INTO to_delete_SBId (SBId) VALUES (%s)"
303
+ returnValueOrRaise(self._updatemany(sqlCmd, [(sbid,) for sbid in SBIdList]))
304
+ # Delete from sb_EntityMapping first (to respect FK constraints if any)
305
+ sqlCmd = "DELETE FROM `sb_EntityMapping` WHERE SBId IN (SELECT SBId FROM to_delete_SBId)"
306
+ returnValueOrRaise(self._update(sqlCmd))
307
+ # Delete from sb_SandBoxes
308
+ sqlCmd = "DELETE FROM `sb_SandBoxes` WHERE SBId IN (SELECT SBId FROM to_delete_SBId)"
309
+ returnValueOrRaise(self._update(sqlCmd))
310
+ finally:
311
+ # Drop temporary table
312
+ sqlCmd = "DROP TEMPORARY TABLE to_delete_SBId"
313
+ returnValueOrRaise(self._update(sqlCmd))
331
314
  return S_OK()
332
315
 
333
- def getSandboxId(self, SEName, SEPFN, requesterName, requesterGroup, field="SBId", requesterDN=None):
316
+ def getSandboxId(self, SEName, SEPFN, requesterName, requesterGroup, field="SBId"):
334
317
  """
335
318
  Get the sandboxId if it exists
336
319
 
@@ -350,7 +333,7 @@ class SandboxMetadataDB(DB):
350
333
  "s.OwnerId=o.OwnerId",
351
334
  ]
352
335
  sqlCmd = f"SELECT s.{field} FROM `sb_SandBoxes` s, `sb_Owners` o WHERE"
353
- requesterProps = Registry.getPropertiesForEntity(requesterGroup, name=requesterName, dn=requesterDN)
336
+ requesterProps = Registry.getPropertiesForEntity(requesterGroup, name=requesterName)
354
337
  if Properties.JOB_ADMINISTRATOR in requesterProps or Properties.JOB_MONITOR in requesterProps:
355
338
  # Do nothing, just ensure it doesn't fit in the other cases
356
339
  pass
@@ -370,24 +353,3 @@ class SandboxMetadataDB(DB):
370
353
  if not data:
371
354
  return S_ERROR("No sandbox matches the requirements")
372
355
  return S_OK(data[0][0])
373
-
374
- def getSandboxOwner(self, SEName, SEPFN, requesterDN, requesterGroup):
375
- """get the sandbox owner, if such sandbox exists
376
-
377
- :param SEName: name of the StorageElement
378
- :param SEPFN: PFN of the Sandbox
379
- :param requesterDN: host DN used as credentials
380
- :param requesterGroup: group used to use as credentials (should be 'hosts')
381
-
382
- :returns: S_OK with tuple (owner, ownerGroup, VO)
383
- """
384
- if not (res := self.getSandboxId(SEName, SEPFN, None, requesterGroup, "OwnerId", requesterDN=requesterDN))[
385
- "OK"
386
- ]:
387
- return res
388
-
389
- if not (
390
- res := self._query(f"SELECT `Owner`, `OwnerGroup`, `VO` FROM `sb_Owners` WHERE `OwnerId` = {res['Value']}")
391
- )["OK"]:
392
- return res
393
- return S_OK(res["Value"][0])
@@ -0,0 +1,125 @@
1
+ from DIRAC import S_ERROR, S_OK, gLogger
2
+ from DIRAC.StorageManagementSystem.DB.StorageManagementDB import StorageManagementDB
3
+ from DIRAC.WorkloadManagementSystem.Client import JobStatus
4
+ from DIRAC.WorkloadManagementSystem.DB.JobDB import JobDB
5
+ from DIRAC.WorkloadManagementSystem.DB.PilotAgentsDB import PilotAgentsDB
6
+ from DIRAC.WorkloadManagementSystem.DB.TaskQueueDB import TaskQueueDB
7
+ from DIRAC.WorkloadManagementSystem.Service.JobPolicy import RIGHT_DELETE, RIGHT_KILL
8
+ from DIRAC.WorkloadManagementSystem.Utilities.jobAdministration import _filterJobStateTransition
9
+
10
+
11
+ def _deleteJob(jobID, force=False):
12
+ """Set the job status to "Deleted"
13
+ and remove the pilot that ran and its logging info if the pilot is finished.
14
+
15
+ :param int jobID: job ID
16
+ :return: S_OK()/S_ERROR()
17
+ """
18
+ if not (result := JobDB().setJobStatus(jobID, JobStatus.DELETED, "Checking accounting", force=force))["OK"]:
19
+ gLogger.warn("Failed to set job Deleted status", result["Message"])
20
+ return result
21
+
22
+ if not (result := TaskQueueDB().deleteJob(jobID))["OK"]:
23
+ gLogger.warn("Failed to delete job from the TaskQueue")
24
+
25
+ # if it was the last job for the pilot
26
+ result = PilotAgentsDB().getPilotsForJobID(jobID)
27
+ if not result["OK"]:
28
+ gLogger.error("Failed to get Pilots for JobID", result["Message"])
29
+ return result
30
+ for pilot in result["Value"]:
31
+ res = PilotAgentsDB().getJobsForPilot(pilot)
32
+ if not res["OK"]:
33
+ gLogger.error("Failed to get jobs for pilot", res["Message"])
34
+ return res
35
+ if not res["Value"]: # if list of jobs for pilot is empty, delete pilot
36
+ result = PilotAgentsDB().getPilotInfo(pilotID=pilot)
37
+ if not result["OK"]:
38
+ gLogger.error("Failed to get pilot info", result["Message"])
39
+ return result
40
+ ret = PilotAgentsDB().deletePilot(result["Value"]["PilotJobReference"])
41
+ if not ret["OK"]:
42
+ gLogger.error("Failed to delete pilot from PilotAgentsDB", ret["Message"])
43
+ return ret
44
+
45
+ return S_OK()
46
+
47
+
48
+ def _killJob(jobID, sendKillCommand=True, force=False):
49
+ """Kill one job
50
+
51
+ :param int jobID: job ID
52
+ :param bool sendKillCommand: send kill command
53
+
54
+ :return: S_OK()/S_ERROR()
55
+ """
56
+ if sendKillCommand:
57
+ if not (result := JobDB().setJobCommand(jobID, "Kill"))["OK"]:
58
+ gLogger.warn("Failed to set job Kill command", result["Message"])
59
+ return result
60
+
61
+ gLogger.info("Job marked for termination", jobID)
62
+ if not (result := JobDB().setJobStatus(jobID, JobStatus.KILLED, "Marked for termination", force=force))["OK"]:
63
+ gLogger.warn("Failed to set job Killed status", result["Message"])
64
+ if not (result := TaskQueueDB().deleteJob(jobID))["OK"]:
65
+ gLogger.warn("Failed to delete job from the TaskQueue", result["Message"])
66
+
67
+ return S_OK()
68
+
69
+
70
+ def kill_delete_jobs(right, validJobList, nonauthJobList=[], force=False):
71
+ """Kill (== set the status to "KILLED") or delete (== set the status to "DELETED") jobs as necessary
72
+
73
+ :param str right: RIGHT_KILL or RIGHT_DELETE
74
+
75
+ :return: S_OK()/S_ERROR()
76
+ """
77
+ badIDs = []
78
+
79
+ killJobList = []
80
+ deleteJobList = []
81
+ if validJobList:
82
+ result = JobDB().getJobsAttributes(validJobList, ["Status"])
83
+ if not result["OK"]:
84
+ return result
85
+ jobStates = result["Value"]
86
+
87
+ # Get the jobs allowed to transition to the Killed state
88
+ killJobList.extend(_filterJobStateTransition(jobStates, JobStatus.KILLED))
89
+
90
+ if right == RIGHT_DELETE:
91
+ # Get the jobs allowed to transition to the Deleted state
92
+ deleteJobList.extend(_filterJobStateTransition(jobStates, JobStatus.DELETED))
93
+
94
+ for jobID in killJobList:
95
+ result = _killJob(jobID, force=force)
96
+ if not result["OK"]:
97
+ badIDs.append(jobID)
98
+
99
+ for jobID in deleteJobList:
100
+ result = _deleteJob(jobID, force=force)
101
+ if not result["OK"]:
102
+ badIDs.append(jobID)
103
+
104
+ # Look for jobs that are in the Staging state to send kill signal to the stager
105
+ stagingJobList = [jobID for jobID, sDict in jobStates.items() if sDict["Status"] == JobStatus.STAGING]
106
+
107
+ if stagingJobList:
108
+ stagerDB = StorageManagementDB()
109
+ gLogger.info("Going to send killing signal to stager as well!")
110
+ result = stagerDB.killTasksBySourceTaskID(stagingJobList)
111
+ if not result["OK"]:
112
+ gLogger.warn("Failed to kill some Stager tasks", result["Message"])
113
+
114
+ if nonauthJobList or badIDs:
115
+ result = S_ERROR("Some jobs failed deletion")
116
+ if nonauthJobList:
117
+ gLogger.warn("Non-authorized JobIDs won't be deleted", str(nonauthJobList))
118
+ result["NonauthorizedJobIDs"] = nonauthJobList
119
+ if badIDs:
120
+ gLogger.warn("JobIDs failed to be deleted", str(badIDs))
121
+ result["FailedJobIDs"] = badIDs
122
+ return result
123
+
124
+ jobsList = killJobList if right == RIGHT_KILL else deleteJobList
125
+ return S_OK(jobsList)
@@ -28,7 +28,7 @@ def test_getInputData(jobDB: JobDB):
28
28
  """Test the getInputData method from JobDB"""
29
29
  # Arrange
30
30
  jobDB._escapeString = MagicMock(return_value=S_OK())
31
- jobDB._query = MagicMock(return_value=S_OK((("/vo/user/lfn1",), ("LFN:/vo/user/lfn2",))))
31
+ jobDB._query = MagicMock(return_value=S_OK([(1234, "/vo/user/lfn1"), (1234, "LFN:/vo/user/lfn2")]))
32
32
 
33
33
  # Act
34
34
  res = jobDB.getInputData(1234)
@@ -0,0 +1,28 @@
1
+ """ unit test (pytest) of JobAdministration module
2
+ """
3
+
4
+ from unittest.mock import MagicMock
5
+
6
+ import pytest
7
+
8
+ # sut
9
+ from DIRAC.WorkloadManagementSystem.DB.StatusUtils import kill_delete_jobs
10
+
11
+
12
+ @pytest.mark.parametrize(
13
+ "jobIDs_list, right",
14
+ [
15
+ ([], "Kill"),
16
+ ([], "Delete"),
17
+ (1, "Kill"),
18
+ ([1, 2], "Kill"),
19
+ ],
20
+ )
21
+ def test___kill_delete_jobs(mocker, jobIDs_list, right):
22
+ mocker.patch("DIRAC.WorkloadManagementSystem.DB.StatusUtils.JobDB", MagicMock())
23
+ mocker.patch("DIRAC.WorkloadManagementSystem.DB.StatusUtils.TaskQueueDB", MagicMock())
24
+ mocker.patch("DIRAC.WorkloadManagementSystem.DB.StatusUtils.PilotAgentsDB", MagicMock())
25
+ mocker.patch("DIRAC.WorkloadManagementSystem.DB.StatusUtils.StorageManagementDB", MagicMock())
26
+
27
+ res = kill_delete_jobs(right, jobIDs_list)
28
+ assert res["OK"]
@@ -1,6 +1,6 @@
1
1
  """ The Job Sanity executor assigns sandboxes to the job """
2
2
  from DIRAC import S_OK
3
- from DIRAC.WorkloadManagementSystem.Client.SandboxStoreClient import SandboxStoreClient
3
+ from DIRAC.WorkloadManagementSystem.DB.SandboxMetadataDB import SandboxMetadataDB
4
4
  from DIRAC.WorkloadManagementSystem.Executor.Base.OptimizerExecutor import OptimizerExecutor
5
5
 
6
6
 
@@ -15,7 +15,6 @@ class JobSanity(OptimizerExecutor):
15
15
  @classmethod
16
16
  def initializeOptimizer(cls):
17
17
  """Initialize specific parameters for JobSanityAgent."""
18
- cls.sandboxClient = SandboxStoreClient(useCertificates=True, smdb=True)
19
18
  return S_OK()
20
19
 
21
20
  def optimizeJob(self, jid, jobState):
@@ -48,6 +47,7 @@ class JobSanity(OptimizerExecutor):
48
47
  ownerName = manifest.getOption("Owner")
49
48
  ownerGroup = manifest.getOption("OwnerGroup")
50
49
  isbList = manifest.getOption("InputSandbox", [])
50
+ vo = manifest.getOption("VirtualOrganization")
51
51
  sbsToAssign = []
52
52
  for isb in isbList:
53
53
  if isb.startswith("SB:"):
@@ -55,8 +55,9 @@ class JobSanity(OptimizerExecutor):
55
55
  numSBsToAssign = len(sbsToAssign)
56
56
  if not numSBsToAssign:
57
57
  return S_OK(0)
58
- self.jobLog.info("Assigning sandboxes", f"({numSBsToAssign} on behalf of {ownerName}@{ownerGroup})")
59
- result = self.sandboxClient.assignSandboxesToJob(jobState.jid, sbsToAssign, ownerName, ownerGroup)
58
+ self.jobLog.info("Assigning sandboxes", f"({numSBsToAssign} on behalf of {ownerName}@{ownerGroup}@{vo})")
59
+ eId = f"Job:{jobState.jid}"
60
+ result = SandboxMetadataDB().assignSandboxesToEntities({eId: sbsToAssign}, ownerName, ownerGroup)
60
61
  if not result["OK"]:
61
62
  self.jobLog.error("Could not assign sandboxes in the SandboxStore")
62
63
  return result
@@ -549,6 +549,10 @@ class JobScheduling(OptimizerExecutor):
549
549
  if not result["OK"]:
550
550
  return result
551
551
 
552
+ result = jobState.commitChanges()
553
+ if not result["OK"]:
554
+ self.jobLog.error("Could not save changes for job", result["Message"])
555
+
552
556
  return S_OK(stageLFNs)
553
557
 
554
558
  def __updateSharedSESites(self, jobManifest, stageSite, stagedLFNs, opData):
@@ -1,11 +1,17 @@
1
- import importlib
1
+ from __future__ import annotations
2
+
2
3
  import functools
4
+ import time
3
5
  from datetime import datetime, timezone
4
-
6
+ from typing import TYPE_CHECKING
5
7
 
6
8
  from DIRAC.Core.Security.DiracX import DiracXClient, FutureClient, addRPCStub
7
- from DIRAC.Core.Utilities.ReturnValues import convertToReturnValue
9
+ from DIRAC.Core.Utilities.ReturnValues import convertToReturnValue, returnValueOrRaise
8
10
  from DIRAC.Core.Utilities.TimeUtilities import fromString
11
+ from DIRAC.WorkloadManagementSystem.Client import JobStatus
12
+
13
+ if TYPE_CHECKING:
14
+ from diracx.client.models import JobCommand
9
15
 
10
16
 
11
17
  def stripValueIfOK(func):
@@ -21,30 +27,49 @@ def stripValueIfOK(func):
21
27
  def wrapper(*args, **kwargs):
22
28
  result = func(*args, **kwargs)
23
29
  if result.get("OK"):
24
- assert result.pop("Value") is None, "Value should be None if OK"
30
+ if result.get("Value") is None:
31
+ result.pop("Value")
25
32
  return result
26
33
 
27
34
  return wrapper
28
35
 
29
36
 
30
37
  class JobStateUpdateClient(FutureClient):
31
- @stripValueIfOK
32
38
  @convertToReturnValue
33
39
  def sendHeartBeat(self, jobID: str | int, dynamicData: dict, staticData: dict):
34
- print("HACK: This is a no-op until we decide what to do")
40
+ """Send a heartbeat from a Job.
41
+
42
+ The behaviour of this function is not strictly the same as in legacy
43
+ DIRAC. Most notably, in legacy DIRAC the heartbeat always overrides the
44
+ job status to Running whereas in DiracX the job state machine is still
45
+ respected. Additionally, DiracX updates the job logging information
46
+ when status transitions occur as a result of the heartbeat.
47
+ """
48
+
49
+ with DiracXClient() as api:
50
+ body = {jobID: dynamicData | staticData}
51
+ if len(body[jobID]) != len(dynamicData) + len(staticData):
52
+ raise NotImplementedError(f"Duplicate keys: {dynamicData=} {staticData=}")
53
+ commands: list[JobCommand] = api.jobs.add_heartbeat(body)
54
+ # Legacy DIRAC returns a dictionary of {command: arguments}
55
+ result = {}
56
+ for command in commands:
57
+ if command.job_id != jobID:
58
+ raise NotImplementedError(f"Job ID mismatch: {jobID=} {command.job_id=}")
59
+ result[command.command] = command.arguments
60
+ return result
35
61
 
36
62
  @stripValueIfOK
37
63
  @convertToReturnValue
38
64
  def setJobApplicationStatus(self, jobID: str | int, appStatus: str, source: str = "Unknown"):
39
65
  statusDict = {
40
- "application_status": appStatus,
66
+ "ApplicationStatus": appStatus,
41
67
  }
42
68
  if source:
43
69
  statusDict["Source"] = source
44
70
  with DiracXClient() as api:
45
- api.jobs.set_single_job_status(
46
- jobID,
47
- {datetime.now(tz=timezone.utc): statusDict},
71
+ api.jobs.set_job_statuses(
72
+ {jobID: {datetime.now(tz=timezone.utc): statusDict}},
48
73
  )
49
74
 
50
75
  @stripValueIfOK
@@ -52,34 +77,29 @@ class JobStateUpdateClient(FutureClient):
52
77
  def setJobAttribute(self, jobID: str | int, attribute: str, value: str):
53
78
  with DiracXClient() as api:
54
79
  if attribute == "Status":
55
- api.jobs.set_single_job_status(
56
- jobID,
57
- {datetime.now(tz=timezone.utc): {"status": value}},
80
+ return api.jobs.set_job_statuses(
81
+ {jobID: {datetime.now(tz=timezone.utc): {"Status": value}}},
58
82
  )
59
83
  else:
60
- api.jobs.set_single_job_properties(jobID, {attribute: value})
61
-
62
- @stripValueIfOK
63
- @convertToReturnValue
64
- def setJobFlag(self, jobID: str | int, flag: str):
65
- with DiracXClient() as api:
66
- api.jobs.set_single_job_properties(jobID, {flag: True})
84
+ return api.jobs.patch_metadata({jobID: {attribute: value}})
67
85
 
68
86
  @stripValueIfOK
69
87
  @convertToReturnValue
70
88
  def setJobParameter(self, jobID: str | int, name: str, value: str):
71
- print("HACK: This is a no-op until we decide what to do")
89
+ with DiracXClient() as api:
90
+ api.jobs.patch_metadata({jobID: {name: value}})
72
91
 
73
92
  @stripValueIfOK
74
93
  @convertToReturnValue
75
94
  def setJobParameters(self, jobID: str | int, parameters: list):
76
- print("HACK: This is a no-op until we decide what to do")
95
+ with DiracXClient() as api:
96
+ api.jobs.patch_metadata({jobID: {k: v for k, v in parameters}})
77
97
 
78
98
  @stripValueIfOK
79
99
  @convertToReturnValue
80
100
  def setJobSite(self, jobID: str | int, site: str):
81
101
  with DiracXClient() as api:
82
- api.jobs.set_single_job_properties(jobID, {"Site": site})
102
+ api.jobs.patch_metadata({jobID: {"Site": site}})
83
103
 
84
104
  @stripValueIfOK
85
105
  @convertToReturnValue
@@ -102,9 +122,8 @@ class JobStateUpdateClient(FutureClient):
102
122
  if datetime_ is None:
103
123
  datetime_ = datetime.utcnow()
104
124
  with DiracXClient() as api:
105
- api.jobs.set_single_job_status(
106
- jobID,
107
- {fromString(datetime_).replace(tzinfo=timezone.utc): statusDict},
125
+ api.jobs.set_job_statuses(
126
+ {jobID: {fromString(datetime_).replace(tzinfo=timezone.utc): statusDict}},
108
127
  force=force,
109
128
  )
110
129
 
@@ -114,7 +133,7 @@ class JobStateUpdateClient(FutureClient):
114
133
  def setJobStatusBulk(self, jobID: str | int, statusDict: dict, force=False):
115
134
  statusDict = {fromString(k).replace(tzinfo=timezone.utc): v for k, v in statusDict.items()}
116
135
  with DiracXClient() as api:
117
- api.jobs.set_job_status_bulk(
136
+ api.jobs.set_job_statuses(
118
137
  {jobID: statusDict},
119
138
  force=force,
120
139
  )
@@ -122,13 +141,36 @@ class JobStateUpdateClient(FutureClient):
122
141
  @stripValueIfOK
123
142
  @convertToReturnValue
124
143
  def setJobsParameter(self, jobsParameterDict: dict):
125
- print("HACK: This is a no-op until we decide what to do")
144
+ with DiracXClient() as api:
145
+ updates = {job_id: {k: v} for job_id, (k, v) in jobsParameterDict.items()}
146
+ api.jobs.patch_metadata(updates)
126
147
 
127
148
  @stripValueIfOK
128
149
  @convertToReturnValue
129
- def unsetJobFlag(self, jobID: str | int, flag: str):
130
- with DiracXClient() as api:
131
- api.jobs.set_single_job_properties(jobID, {flag: False})
132
-
133
150
  def updateJobFromStager(self, jobID: str | int, status: str):
134
- raise NotImplementedError("TODO")
151
+ if status == "Done":
152
+ jobStatus = JobStatus.CHECKING
153
+ minorStatus = "JobScheduling"
154
+ else:
155
+ jobStatus = None
156
+ minorStatus = "Staging input files failed"
157
+
158
+ trials = 10
159
+ query = [{"parameter": "JobID", "operator": "eq", "value": jobID}]
160
+ with DiracXClient() as api:
161
+ for i in range(trials):
162
+ result = api.jobs.search(parameters=["Status"], search=query)
163
+ if not result:
164
+ return None
165
+ if result[0]["Status"] == JobStatus.STAGING:
166
+ break
167
+ time.sleep(1)
168
+ else:
169
+ return f"Job is not in Staging after {trials} seconds"
170
+
171
+ retVal = self.setJobStatus(jobID, status=jobStatus, minorStatus=minorStatus, source="StagerSystem")
172
+ # As there might not be a value (see stripValueIfOK), only call
173
+ # returnValueOrRaise if the return value is not OK
174
+ if not retVal["OK"]: # pylint: disable=unsubscriptable-object
175
+ returnValueOrRaise(retVal)
176
+ return None if i == 0 else f"Found job in Staging after {i} seconds"