panda-server-source 0.8.0__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 (375) hide show
  1. panda_server_source-0.8.0.dist-info/METADATA +55 -0
  2. panda_server_source-0.8.0.dist-info/RECORD +375 -0
  3. panda_server_source-0.8.0.dist-info/WHEEL +4 -0
  4. panda_server_source-0.8.0.dist-info/licenses/LICENSE.txt +202 -0
  5. pandajedi/__init__.py +0 -0
  6. pandajedi/jedibrokerage/AtlasAnalJobBroker.py +2343 -0
  7. pandajedi/jedibrokerage/AtlasBrokerUtils.py +1292 -0
  8. pandajedi/jedibrokerage/AtlasProdJobBroker.py +2064 -0
  9. pandajedi/jedibrokerage/AtlasProdTaskBroker.py +629 -0
  10. pandajedi/jedibrokerage/GenJobBroker.py +381 -0
  11. pandajedi/jedibrokerage/JobBrokerBase.py +133 -0
  12. pandajedi/jedibrokerage/TaskBrokerBase.py +15 -0
  13. pandajedi/jedibrokerage/__init__.py +0 -0
  14. pandajedi/jediconfig/__init__.py +0 -0
  15. pandajedi/jediconfig/jedi_config.py +40 -0
  16. pandajedi/jedicore/FactoryBase.py +182 -0
  17. pandajedi/jedicore/Interaction.py +433 -0
  18. pandajedi/jedicore/JediDBProxy.py +56 -0
  19. pandajedi/jedicore/JediDBProxyPool.py +37 -0
  20. pandajedi/jedicore/JediDBSchemaInfo.py +17 -0
  21. pandajedi/jedicore/JediException.py +13 -0
  22. pandajedi/jedicore/JediTaskBuffer.py +984 -0
  23. pandajedi/jedicore/JediTaskBufferInterface.py +50 -0
  24. pandajedi/jedicore/MsgWrapper.py +124 -0
  25. pandajedi/jedicore/ProcessUtils.py +34 -0
  26. pandajedi/jedicore/SiteCandidate.py +90 -0
  27. pandajedi/jedicore/ThreadUtils.py +205 -0
  28. pandajedi/jedicore/__init__.py +0 -0
  29. pandajedi/jediddm/AtlasDDMClient.py +1708 -0
  30. pandajedi/jediddm/DDMClientBase.py +16 -0
  31. pandajedi/jediddm/DDMInterface.py +83 -0
  32. pandajedi/jediddm/GenDDMClient.py +17 -0
  33. pandajedi/jediddm/__init__.py +0 -0
  34. pandajedi/jedidog/AtlasAnalWatchDog.py +648 -0
  35. pandajedi/jedidog/AtlasDataCarouselWatchDog.py +189 -0
  36. pandajedi/jedidog/AtlasDataLocalityUpdaterWatchDog.py +215 -0
  37. pandajedi/jedidog/AtlasProdWatchDog.py +443 -0
  38. pandajedi/jedidog/AtlasQueueFillerWatchDog.py +800 -0
  39. pandajedi/jedidog/AtlasTaskWithholderWatchDog.py +245 -0
  40. pandajedi/jedidog/AtlasWorkflowManagerWatchDog.py +63 -0
  41. pandajedi/jedidog/GenWatchDog.py +21 -0
  42. pandajedi/jedidog/JumboWatchDog.py +147 -0
  43. pandajedi/jedidog/TypicalWatchDogBase.py +122 -0
  44. pandajedi/jedidog/WatchDogBase.py +53 -0
  45. pandajedi/jedidog/__init__.py +0 -0
  46. pandajedi/jediexec/reniceJEDI.py +25 -0
  47. pandajedi/jedigen/AtlasTaskGenerator.py +110 -0
  48. pandajedi/jedigen/TaskGeneratorBase.py +15 -0
  49. pandajedi/jedigen/__init__.py +0 -0
  50. pandajedi/jedimsgprocessor/__init__.py +0 -0
  51. pandajedi/jedimsgprocessor/ai_parameter_adjuster_msg_processor +71 -0
  52. pandajedi/jedimsgprocessor/atlas_idds_msg_processor.py +66 -0
  53. pandajedi/jedimsgprocessor/base_msg_processor.py +33 -0
  54. pandajedi/jedimsgprocessor/forwarding_msg_processor.py +29 -0
  55. pandajedi/jedimsgprocessor/hpo_msg_processor.py +93 -0
  56. pandajedi/jedimsgprocessor/jedi_contents_feeder_msg_processor.py +63 -0
  57. pandajedi/jedimsgprocessor/jedi_job_generator_msg_processor.py +185 -0
  58. pandajedi/jedimsgprocessor/jedi_post_processor_msg_processor.py +82 -0
  59. pandajedi/jedimsgprocessor/kafka_msg_processor.py +44 -0
  60. pandajedi/jedimsgprocessor/panda_callback_msg_processor.py +182 -0
  61. pandajedi/jedimsgprocessor/panda_to_jedi_msg_processor.py +74 -0
  62. pandajedi/jedimsgprocessor/processing_msg_processor.py +134 -0
  63. pandajedi/jedimsgprocessor/status_report_msg_processor.py +129 -0
  64. pandajedi/jedimsgprocessor/tape_carousel_msg_processor.py +118 -0
  65. pandajedi/jedimsgprocessor/workflow_manager_msg_processor.py +89 -0
  66. pandajedi/jediorder/ContentsFeeder.py +785 -0
  67. pandajedi/jediorder/JediDaemon.py +27 -0
  68. pandajedi/jediorder/JediKnight.py +55 -0
  69. pandajedi/jediorder/JediMaster.py +238 -0
  70. pandajedi/jediorder/JediMsgProcessor.py +32 -0
  71. pandajedi/jediorder/JobBroker.py +33 -0
  72. pandajedi/jediorder/JobGenerator.py +2763 -0
  73. pandajedi/jediorder/JobSplitter.py +308 -0
  74. pandajedi/jediorder/JobThrottler.py +28 -0
  75. pandajedi/jediorder/PostProcessor.py +166 -0
  76. pandajedi/jediorder/TaskBroker.py +268 -0
  77. pandajedi/jediorder/TaskCommando.py +334 -0
  78. pandajedi/jediorder/TaskGenerator.py +17 -0
  79. pandajedi/jediorder/TaskRefiner.py +611 -0
  80. pandajedi/jediorder/TaskSetupper.py +17 -0
  81. pandajedi/jediorder/WatchDog.py +79 -0
  82. pandajedi/jediorder/__init__.py +0 -0
  83. pandajedi/jedipprocess/AtlasAnalPostProcessor.py +404 -0
  84. pandajedi/jedipprocess/AtlasPostProcessorUtils.py +50 -0
  85. pandajedi/jedipprocess/AtlasProdPostProcessor.py +178 -0
  86. pandajedi/jedipprocess/GenPostProcessor.py +35 -0
  87. pandajedi/jedipprocess/MailTemplates.py +185 -0
  88. pandajedi/jedipprocess/PostProcessorBase.py +269 -0
  89. pandajedi/jedipprocess/__init__.py +0 -0
  90. pandajedi/jedirefine/AtlasAnalTaskRefiner.py +259 -0
  91. pandajedi/jedirefine/AtlasProdTaskRefiner.py +162 -0
  92. pandajedi/jedirefine/GenTaskRefiner.py +61 -0
  93. pandajedi/jedirefine/RefinerUtils.py +154 -0
  94. pandajedi/jedirefine/TaskRefinerBase.py +996 -0
  95. pandajedi/jedirefine/__init__.py +0 -0
  96. pandajedi/jedisetup/AtlasTaskSetupper.py +309 -0
  97. pandajedi/jedisetup/GenTaskSetupper.py +16 -0
  98. pandajedi/jedisetup/SimpleTaskSetupper.py +152 -0
  99. pandajedi/jedisetup/TaskSetupperBase.py +15 -0
  100. pandajedi/jedisetup/__init__.py +0 -0
  101. pandajedi/jeditest/SchemaChecker.py +41 -0
  102. pandajedi/jeditest/addContWoSoTask.py +54 -0
  103. pandajedi/jeditest/addDataCarouselRequest.py +47 -0
  104. pandajedi/jeditest/addEventServiceTask.py +101 -0
  105. pandajedi/jeditest/addHpoTask.py +81 -0
  106. pandajedi/jeditest/addIDDSStageInRequest.py +56 -0
  107. pandajedi/jeditest/addNonAtlasTask.py +71 -0
  108. pandajedi/jeditest/addNonAtlasTaskEnc.py +64 -0
  109. pandajedi/jeditest/addTestTask.py +124 -0
  110. pandajedi/jeditest/addTestTaskParamToDEFT.py +70 -0
  111. pandajedi/jeditest/addTestTaskWithMerger.py +97 -0
  112. pandajedi/jeditest/brokerTest.py +78 -0
  113. pandajedi/jeditest/cancelDataCarouselRequest.py +35 -0
  114. pandajedi/jeditest/contentsFeederTest.py +28 -0
  115. pandajedi/jeditest/contentsFeederTestWithTask.py +18 -0
  116. pandajedi/jeditest/ddmTest.py +4 -0
  117. pandajedi/jeditest/jobGenForOneTask.py +38 -0
  118. pandajedi/jeditest/jobGeneratorTest.py +43 -0
  119. pandajedi/jeditest/jobSplitterTest.py +104 -0
  120. pandajedi/jeditest/jobThrottlerTest.py +22 -0
  121. pandajedi/jeditest/msgProcessorTest.py +9 -0
  122. pandajedi/jeditest/postProcessorTest.py +18 -0
  123. pandajedi/jeditest/resubmitDataCarouselRequest.py +35 -0
  124. pandajedi/jeditest/resubmitTaskParam.py +20 -0
  125. pandajedi/jeditest/taskBrokerTest.py +22 -0
  126. pandajedi/jeditest/taskBrokerTestWithTask.py +38 -0
  127. pandajedi/jeditest/taskBufferTest.py +5 -0
  128. pandajedi/jeditest/taskCommandoTest.py +28 -0
  129. pandajedi/jeditest/taskRefinerTest.py +30 -0
  130. pandajedi/jeditest/testOneTaskToRefine.py +49 -0
  131. pandajedi/jeditest/testOneTaskToTaskBroker.py +42 -0
  132. pandajedi/jeditest/testPandaDDMMap.py +62 -0
  133. pandajedi/jeditest/updateDataCarouselRequestParams.py +50 -0
  134. pandajedi/jeditest/watchDogTest.py +25 -0
  135. pandajedi/jeditest/workQueueTest.py +22 -0
  136. pandajedi/jedithrottle/AtlasAnalJobThrottler.py +19 -0
  137. pandajedi/jedithrottle/AtlasProdJobThrottler.py +18 -0
  138. pandajedi/jedithrottle/GenJobThrottler.py +26 -0
  139. pandajedi/jedithrottle/JobThrottlerBase.py +496 -0
  140. pandajedi/jedithrottle/__init__.py +0 -0
  141. pandaserver/__init__.py +0 -0
  142. pandaserver/api/__init__.py +0 -0
  143. pandaserver/api/v1/__init__.py +0 -0
  144. pandaserver/api/v1/common.py +339 -0
  145. pandaserver/api/v1/credential_management_api.py +372 -0
  146. pandaserver/api/v1/data_carousel_api.py +264 -0
  147. pandaserver/api/v1/event_api.py +269 -0
  148. pandaserver/api/v1/file_server_api.py +942 -0
  149. pandaserver/api/v1/harvester_api.py +445 -0
  150. pandaserver/api/v1/http_client.py +166 -0
  151. pandaserver/api/v1/idds_api.py +180 -0
  152. pandaserver/api/v1/job_api.py +463 -0
  153. pandaserver/api/v1/metaconfig_api.py +122 -0
  154. pandaserver/api/v1/pilot_api.py +895 -0
  155. pandaserver/api/v1/statistics_api.py +207 -0
  156. pandaserver/api/v1/system_api.py +186 -0
  157. pandaserver/api/v1/task_api.py +1520 -0
  158. pandaserver/api/v1/tests/__init__.py +0 -0
  159. pandaserver/api/v1/tests/credential_management_api_tests.py +116 -0
  160. pandaserver/api/v1/tests/data_carousel_api_tests.py +82 -0
  161. pandaserver/api/v1/tests/harvester_api_tests.py +188 -0
  162. pandaserver/api/v1/tests/idds_api_tests.py +45 -0
  163. pandaserver/api/v1/tests/job_api_tests.py +112 -0
  164. pandaserver/api/v1/tests/pilot_api_tests.py +154 -0
  165. pandaserver/api/v1/tests/statistics_api_tests.py +104 -0
  166. pandaserver/api/v1/tests/system_api_tests.py +58 -0
  167. pandaserver/api/v1/tests/task_api_tests.py +590 -0
  168. pandaserver/api/v1/version.py +2 -0
  169. pandaserver/api/v1/workflow_api.py +136 -0
  170. pandaserver/brokerage/SiteMapper.py +406 -0
  171. pandaserver/brokerage/__init__.py +0 -0
  172. pandaserver/brokerage/broker.py +272 -0
  173. pandaserver/config/__init__.py +0 -0
  174. pandaserver/config/config_utils.py +15 -0
  175. pandaserver/config/daemon_config.py +29 -0
  176. pandaserver/config/mcp_config.py +38 -0
  177. pandaserver/config/panda_config.py +233 -0
  178. pandaserver/configurator/Carbon.py +65 -0
  179. pandaserver/configurator/Configurator.py +908 -0
  180. pandaserver/configurator/__init__.py +0 -0
  181. pandaserver/configurator/aux.py +73 -0
  182. pandaserver/daemons/__init__.py +0 -0
  183. pandaserver/daemons/master.py +110 -0
  184. pandaserver/daemons/master_systemd.py +79 -0
  185. pandaserver/daemons/scripts/__init__.py +0 -0
  186. pandaserver/daemons/scripts/add_main.py +264 -0
  187. pandaserver/daemons/scripts/add_sub.py +221 -0
  188. pandaserver/daemons/scripts/cache_pilots.py +47 -0
  189. pandaserver/daemons/scripts/cache_schedconfig.py +286 -0
  190. pandaserver/daemons/scripts/carbon.py +50 -0
  191. pandaserver/daemons/scripts/configurator.py +94 -0
  192. pandaserver/daemons/scripts/copyArchive.py +1387 -0
  193. pandaserver/daemons/scripts/datasetManager.py +1154 -0
  194. pandaserver/daemons/scripts/dummy_test.py +25 -0
  195. pandaserver/daemons/scripts/evpPD2P.py +144 -0
  196. pandaserver/daemons/scripts/hs_scrapers.py +264 -0
  197. pandaserver/daemons/scripts/metric_collector.py +927 -0
  198. pandaserver/daemons/scripts/panda_activeusers_query.py +80 -0
  199. pandaserver/daemons/scripts/pilotStreaming.py +109 -0
  200. pandaserver/daemons/scripts/process_workflow_files_daemon.py +166 -0
  201. pandaserver/daemons/scripts/recover_lost_files_daemon.py +177 -0
  202. pandaserver/daemons/scripts/task_evaluator.py +405 -0
  203. pandaserver/daemons/scripts/tmpwatch.py +70 -0
  204. pandaserver/daemons/scripts/worker_synchronization.py +97 -0
  205. pandaserver/daemons/utils.py +753 -0
  206. pandaserver/dataservice/DataServiceUtils.py +323 -0
  207. pandaserver/dataservice/ErrorCode.py +25 -0
  208. pandaserver/dataservice/RecoverLostFilesCore.py +322 -0
  209. pandaserver/dataservice/__init__.py +0 -0
  210. pandaserver/dataservice/activator.py +79 -0
  211. pandaserver/dataservice/adder_atlas_plugin.py +1106 -0
  212. pandaserver/dataservice/adder_dummy_plugin.py +31 -0
  213. pandaserver/dataservice/adder_gen.py +954 -0
  214. pandaserver/dataservice/adder_plugin_base.py +27 -0
  215. pandaserver/dataservice/adder_result.py +75 -0
  216. pandaserver/dataservice/adder_simple_plugin.py +160 -0
  217. pandaserver/dataservice/closer.py +247 -0
  218. pandaserver/dataservice/closer_atlas_plugin.py +90 -0
  219. pandaserver/dataservice/ddm.py +1381 -0
  220. pandaserver/dataservice/ddm_handler.py +95 -0
  221. pandaserver/dataservice/dyn_data_distributer.py +824 -0
  222. pandaserver/dataservice/event_lookup_client_ei.py +77 -0
  223. pandaserver/dataservice/event_picker.py +268 -0
  224. pandaserver/dataservice/finisher.py +194 -0
  225. pandaserver/dataservice/setupper.py +225 -0
  226. pandaserver/dataservice/setupper_atlas_plugin.py +1851 -0
  227. pandaserver/dataservice/setupper_dummy_plugin.py +47 -0
  228. pandaserver/dataservice/setupper_plugin_base.py +67 -0
  229. pandaserver/jobdispatcher/DispatcherUtils.py +35 -0
  230. pandaserver/jobdispatcher/ErrorCode.py +19 -0
  231. pandaserver/jobdispatcher/JobDispatcher.py +1625 -0
  232. pandaserver/jobdispatcher/Protocol.py +443 -0
  233. pandaserver/jobdispatcher/Watcher.py +186 -0
  234. pandaserver/jobdispatcher/__init__.py +0 -0
  235. pandaserver/pandamcp/__init__.py +0 -0
  236. pandaserver/pandamcp/mcp_main.py +56 -0
  237. pandaserver/pandamcp/mcp_test_client.py +95 -0
  238. pandaserver/pandamcp/mcp_utils.py +74 -0
  239. pandaserver/proxycache/__init__.py +0 -0
  240. pandaserver/proxycache/panda_proxy_cache.py +240 -0
  241. pandaserver/proxycache/token_cache.py +141 -0
  242. pandaserver/server/.gacl +6 -0
  243. pandaserver/server/panda.py +539 -0
  244. pandaserver/srvcore/CoreUtils.py +572 -0
  245. pandaserver/srvcore/MailUtils.py +106 -0
  246. pandaserver/srvcore/__init__.py +0 -0
  247. pandaserver/srvcore/allowed_methods.py +100 -0
  248. pandaserver/srvcore/oidc_utils.py +152 -0
  249. pandaserver/srvcore/panda_request.py +196 -0
  250. pandaserver/srvcore/srv_msg_utils.py +42 -0
  251. pandaserver/taskbuffer/ConBridge.py +513 -0
  252. pandaserver/taskbuffer/DBProxyPool.py +95 -0
  253. pandaserver/taskbuffer/DataCarousel.py +3053 -0
  254. pandaserver/taskbuffer/DatasetSpec.py +137 -0
  255. pandaserver/taskbuffer/DdmSpec.py +201 -0
  256. pandaserver/taskbuffer/ErrorCode.py +87 -0
  257. pandaserver/taskbuffer/EventServiceUtils.py +396 -0
  258. pandaserver/taskbuffer/FileSpec.py +290 -0
  259. pandaserver/taskbuffer/GlobalShares.py +244 -0
  260. pandaserver/taskbuffer/HarvesterMetricsSpec.py +92 -0
  261. pandaserver/taskbuffer/Initializer.py +82 -0
  262. pandaserver/taskbuffer/InputChunk.py +1080 -0
  263. pandaserver/taskbuffer/JediCacheSpec.py +57 -0
  264. pandaserver/taskbuffer/JediDatasetSpec.py +588 -0
  265. pandaserver/taskbuffer/JediFileSpec.py +242 -0
  266. pandaserver/taskbuffer/JediTaskSpec.py +1687 -0
  267. pandaserver/taskbuffer/JobSpec.py +1006 -0
  268. pandaserver/taskbuffer/JobUtils.py +232 -0
  269. pandaserver/taskbuffer/NucleusSpec.py +116 -0
  270. pandaserver/taskbuffer/OraDBProxy.py +174 -0
  271. pandaserver/taskbuffer/PanDAMsgProcessor.py +8 -0
  272. pandaserver/taskbuffer/PandaDBSchemaInfo.py +18 -0
  273. pandaserver/taskbuffer/ParseJobXML.py +350 -0
  274. pandaserver/taskbuffer/PickleFileSpec.py +18 -0
  275. pandaserver/taskbuffer/PickleJobSpec.py +25 -0
  276. pandaserver/taskbuffer/PrioUtil.py +40 -0
  277. pandaserver/taskbuffer/ProcessGroups.py +111 -0
  278. pandaserver/taskbuffer/ResourceSpec.py +175 -0
  279. pandaserver/taskbuffer/SQLDumper.py +37 -0
  280. pandaserver/taskbuffer/SQLManager.py +60 -0
  281. pandaserver/taskbuffer/SiteSpec.py +304 -0
  282. pandaserver/taskbuffer/SupErrors.py +10 -0
  283. pandaserver/taskbuffer/TaskBuffer.py +2882 -0
  284. pandaserver/taskbuffer/TaskBufferInterface.py +163 -0
  285. pandaserver/taskbuffer/Utils.py +848 -0
  286. pandaserver/taskbuffer/WorkQueue.py +227 -0
  287. pandaserver/taskbuffer/WorkQueueMapper.py +203 -0
  288. pandaserver/taskbuffer/WorkerSpec.py +143 -0
  289. pandaserver/taskbuffer/WrappedCursor.py +487 -0
  290. pandaserver/taskbuffer/WrappedPickle.py +88 -0
  291. pandaserver/taskbuffer/WrappedPostgresConn.py +24 -0
  292. pandaserver/taskbuffer/__init__.py +0 -0
  293. pandaserver/taskbuffer/db_proxy_mods/OraDBProxy_structure.png +0 -0
  294. pandaserver/taskbuffer/db_proxy_mods/__init__.py +7 -0
  295. pandaserver/taskbuffer/db_proxy_mods/base_module.py +841 -0
  296. pandaserver/taskbuffer/db_proxy_mods/data_carousel_module.py +772 -0
  297. pandaserver/taskbuffer/db_proxy_mods/entity_module.py +3804 -0
  298. pandaserver/taskbuffer/db_proxy_mods/job_complex_module.py +5661 -0
  299. pandaserver/taskbuffer/db_proxy_mods/job_standalone_module.py +2640 -0
  300. pandaserver/taskbuffer/db_proxy_mods/metrics_module.py +874 -0
  301. pandaserver/taskbuffer/db_proxy_mods/misc_standalone_module.py +4272 -0
  302. pandaserver/taskbuffer/db_proxy_mods/task_complex_module.py +6172 -0
  303. pandaserver/taskbuffer/db_proxy_mods/task_event_module.py +4953 -0
  304. pandaserver/taskbuffer/db_proxy_mods/task_standalone_module.py +4999 -0
  305. pandaserver/taskbuffer/db_proxy_mods/task_utils_module.py +2063 -0
  306. pandaserver/taskbuffer/db_proxy_mods/worker_module.py +1944 -0
  307. pandaserver/taskbuffer/db_proxy_mods/workflow_module.py +832 -0
  308. pandaserver/taskbuffer/retryModule.py +552 -0
  309. pandaserver/taskbuffer/task_split_rules.py +308 -0
  310. pandaserver/taskbuffer/workflow_processor.py +249 -0
  311. pandaserver/taskbuffer/wrapped_oracle_conn.py +18 -0
  312. pandaserver/test/SchemaChecker.py +40 -0
  313. pandaserver/test/__init__.py +0 -0
  314. pandaserver/test/banUser.py +43 -0
  315. pandaserver/test/boostPrio.py +19 -0
  316. pandaserver/test/boostUser.py +35 -0
  317. pandaserver/test/callbackDDM.py +11 -0
  318. pandaserver/test/daod_on_demand.py +95 -0
  319. pandaserver/test/finishJob.py +184 -0
  320. pandaserver/test/finishTaskJEDI.py +11 -0
  321. pandaserver/test/getJobs.py +53 -0
  322. pandaserver/test/killJob.py +79 -0
  323. pandaserver/test/killJobLowPrio.py +99 -0
  324. pandaserver/test/killJobsInTask.py +56 -0
  325. pandaserver/test/killTask.py +80 -0
  326. pandaserver/test/killTaskJEDI.py +11 -0
  327. pandaserver/test/killUser.py +83 -0
  328. pandaserver/test/lsst/README.txt +7 -0
  329. pandaserver/test/lsst/lsstSubmit.py +139 -0
  330. pandaserver/test/lsst/lsstSubmitMERGEtest.py +141 -0
  331. pandaserver/test/lsst/lsstSubmitPhosim332.py +53 -0
  332. pandaserver/test/lsst/prepare-client-tarball-from-bigpanda-server.sh +41 -0
  333. pandaserver/test/reassignJobs.py +17 -0
  334. pandaserver/test/reassignSite.py +82 -0
  335. pandaserver/test/reassignTask.py +117 -0
  336. pandaserver/test/reassignWaiting.py +39 -0
  337. pandaserver/test/recoverLostFiles.py +7 -0
  338. pandaserver/test/reloadInputDS.py +51 -0
  339. pandaserver/test/sendCommandToJob.py +26 -0
  340. pandaserver/test/setDebugMode.py +38 -0
  341. pandaserver/test/setPriority.py +28 -0
  342. pandaserver/test/testEvgen.py +60 -0
  343. pandaserver/test/testEvgen17.py +58 -0
  344. pandaserver/test/testG4sim.py +81 -0
  345. pandaserver/test/testG4sim17.py +90 -0
  346. pandaserver/test/testGlobalShares.py +131 -0
  347. pandaserver/test/testReco.py +103 -0
  348. pandaserver/test/testSimulReco14.py +98 -0
  349. pandaserver/test/testSiteMap.py +26 -0
  350. pandaserver/test/testUpdateWorkerPilotStatus.py +9 -0
  351. pandaserver/userinterface/Client.py +1686 -0
  352. pandaserver/userinterface/UserIF.py +1799 -0
  353. pandaserver/userinterface/__init__.py +0 -0
  354. pandaserver/workflow/__init__.py +0 -0
  355. pandaserver/workflow/data_handler_plugins/base_data_handler.py +48 -0
  356. pandaserver/workflow/data_handler_plugins/ddm_collection_data_handler.py +100 -0
  357. pandaserver/workflow/data_handler_plugins/panda_task_data_handler.py +140 -0
  358. pandaserver/workflow/pcwl_test.py +122 -0
  359. pandaserver/workflow/pcwl_utils.py +384 -0
  360. pandaserver/workflow/psnakemake_container.json +68 -0
  361. pandaserver/workflow/psnakemake_task.json +62 -0
  362. pandaserver/workflow/psnakemake_test.py +60 -0
  363. pandaserver/workflow/snakeparser/__init__.py +3 -0
  364. pandaserver/workflow/snakeparser/extensions.py +74 -0
  365. pandaserver/workflow/snakeparser/log.py +53 -0
  366. pandaserver/workflow/snakeparser/names.py +3 -0
  367. pandaserver/workflow/snakeparser/parser.py +332 -0
  368. pandaserver/workflow/snakeparser/utils.py +29 -0
  369. pandaserver/workflow/step_handler_plugins/base_step_handler.py +85 -0
  370. pandaserver/workflow/step_handler_plugins/panda_task_step_handler.py +280 -0
  371. pandaserver/workflow/workflow_base.py +531 -0
  372. pandaserver/workflow/workflow_core.py +1974 -0
  373. pandaserver/workflow/workflow_core_smoketest.py +388 -0
  374. pandaserver/workflow/workflow_parser.py +222 -0
  375. pandaserver/workflow/workflow_utils.py +1047 -0
@@ -0,0 +1,1292 @@
1
+ import datetime
2
+ import json
3
+ import os
4
+ import re
5
+ import socket
6
+ import sys
7
+ import time
8
+ import traceback
9
+ from typing import Any
10
+
11
+ from packaging import version
12
+ from pandacommon.pandautils.PandaUtils import naive_utcnow
13
+
14
+ from pandajedi.jedicore import Interaction
15
+ from pandajedi.jediddm.DDMInterface import DDMInterface
16
+ from pandaserver.brokerage.SiteMapper import SiteMapper
17
+ from pandaserver.dataservice import DataServiceUtils
18
+ from pandaserver.dataservice.DataServiceUtils import select_scope
19
+ from pandaserver.taskbuffer import JobUtils, ProcessGroups, SiteSpec
20
+
21
+
22
+ # get nuclei where data is available
23
+ def getNucleiWithData(siteMapper, ddmIF, datasetName, candidateNuclei, deepScan=False):
24
+ # get replicas
25
+ try:
26
+ replica_map = ddmIF.listReplicasPerDataset(datasetName, deepScan)
27
+ except Exception:
28
+ errtype, errvalue = sys.exc_info()[:2]
29
+ return errtype, f"ddmIF.listReplicasPerDataset failed with {errvalue}", None
30
+ # check if remote source is available at any sites (not only nuclei)
31
+ remote_source_available = False
32
+ for tmp_replica_data in replica_map.values():
33
+ for tmp_location, tmp_stat_data in tmp_replica_data.items():
34
+ if tmp_stat_data[0]["total"] in [None, 0]:
35
+ continue
36
+ if tmp_stat_data[0]["total"] != tmp_stat_data[0]["found"]:
37
+ continue
38
+ if siteMapper.is_readable_remotely(tmp_location):
39
+ remote_source_available = True
40
+ break
41
+ # loop over all clouds
42
+ return_map = {}
43
+ for tmpNucleus in candidateNuclei:
44
+ tmpNucleusSpec = siteMapper.getNucleus(tmpNucleus)
45
+ # loop over all datasets
46
+ totalNum = 0
47
+ totalSize = 0
48
+ avaNumDisk = 0
49
+ avaNumAny = 0
50
+ avaSizeDisk = 0
51
+ avaSizeAny = 0
52
+ can_be_remote_source = False
53
+ for tmpDataset, tmpRepMap in replica_map.items():
54
+ tmpTotalNum = 0
55
+ tmpTotalSize = 0
56
+ tmpAvaNumDisk = 0
57
+ tmpAvaNumAny = 0
58
+ tmpAvaSizeDisk = 0
59
+ tmpAvaSizeAny = 0
60
+ # loop over all endpoints
61
+ for tmpLoc, locData in tmpRepMap.items():
62
+ # get total
63
+ if tmpTotalNum == 0:
64
+ tmpTotalNum = locData[0]["total"]
65
+ tmpTotalSize = locData[0]["tsize"]
66
+ # check if the endpoint is associated
67
+ if tmpNucleusSpec.is_associated_for_input(tmpLoc):
68
+ # check blacklist
69
+ if siteMapper.is_readable_remotely(tmpLoc):
70
+ can_be_remote_source = True
71
+ # sum
72
+ tmpEndpoint = tmpNucleusSpec.getEndpoint(tmpLoc)
73
+ tmpAvaNum = locData[0]["found"]
74
+ tmpAvaSize = locData[0]["asize"]
75
+ # disk
76
+ if tmpEndpoint["is_tape"] != "Y":
77
+ # complete replica is available at DISK
78
+ if tmpTotalNum == tmpAvaNum and tmpTotalNum > 0:
79
+ tmpAvaNumDisk = tmpAvaNum
80
+ tmpAvaNumAny = tmpAvaNum
81
+ tmpAvaSizeDisk = tmpAvaSize
82
+ tmpAvaSizeAny = tmpAvaSize
83
+ break
84
+ if tmpAvaNum > tmpAvaNumDisk:
85
+ tmpAvaNumDisk = tmpAvaNum
86
+ tmpAvaSizeDisk = tmpAvaSize
87
+ # tape
88
+ if tmpAvaNumAny < tmpAvaNum:
89
+ tmpAvaNumAny = tmpAvaNum
90
+ tmpAvaSizeAny = tmpAvaSize
91
+ # total
92
+ totalNum = tmpTotalNum
93
+ totalSize = tmpTotalSize
94
+ avaNumDisk += tmpAvaNumDisk
95
+ avaNumAny += tmpAvaNumAny
96
+ avaSizeDisk += tmpAvaSizeDisk
97
+ avaSizeAny += tmpAvaSizeAny
98
+ # append
99
+ if tmpNucleus in candidateNuclei or avaNumAny > 0:
100
+ return_map[tmpNucleus] = {
101
+ "tot_num": totalNum,
102
+ "tot_size": totalSize,
103
+ "ava_num_disk": avaNumDisk,
104
+ "ava_num_any": avaNumAny,
105
+ "ava_size_disk": avaSizeDisk,
106
+ "ava_size_any": avaSizeAny,
107
+ "can_be_remote_source": can_be_remote_source,
108
+ }
109
+
110
+ return Interaction.SC_SUCCEEDED, return_map, remote_source_available
111
+
112
+
113
+ # get sites where data is available and check if complete replica is available at online RSE
114
+ def get_sites_with_data(
115
+ site_list: list,
116
+ site_mapper: SiteMapper,
117
+ ddm_if: DDMInterface,
118
+ dataset_name: str,
119
+ element_list: list,
120
+ max_missing_input_files: int,
121
+ min_input_completeness: int,
122
+ ) -> tuple[Any, dict | str, bool | None, bool | None, bool | None, bool | None, bool | None, list]:
123
+ """
124
+ Get sites where data is available and check if complete replica is available at online RSE
125
+ 1) regarded_as_complete_disk: True if a replica is regarded as complete at disk (missing files within threshold)
126
+ 2) complete_tape: True if a complete replica is available at tape
127
+ 3) truly_complete_disk: True if a truly complete replica is available at disk (no missing files)
128
+ 4) can_be_local_source: True if the site can read the replica locally over LAN
129
+ 5) can_be_remote_source: True if the site can read the replica remotely over WAN
130
+ 6) list_of_complete_replica_locations : list of RSEs where truly complete replica is available at disk
131
+ Note that VP replicas are not taken into account for the above flags
132
+
133
+ :param site_list: list of site names to be checked
134
+ :param site_mapper: SiteMapper object
135
+ :param ddm_if: DDMInterface object
136
+ :param dataset_name: dataset name
137
+ :param element_list: list of constituent datasets
138
+ :param max_missing_input_files: maximum number of missing files to be regarded as complete
139
+ :param min_input_completeness: minimum completeness (%) to be regarded as complete
140
+
141
+ :return: tuple of (status code or exception type, dict of sites with data availability info, regarded_as_complete_disk, complete_tape, truly_complete_disk, can_be_local_source, can_be_remote_source, list_of_complete_replica_locations)
142
+ """
143
+ # get replicas
144
+ try:
145
+ replica_map = {
146
+ dataset_name: ddm_if.listDatasetReplicas(dataset_name, use_vp=True, skip_incomplete_element=True, element_list=element_list, use_deep=True)
147
+ }
148
+ except Exception:
149
+ errtype, errvalue = sys.exc_info()[:2]
150
+ return errtype, f"ddmIF.listDatasetReplicas failed with {errvalue}", None, None, None, None, None, []
151
+
152
+ # check completeness and storage availability
153
+ is_tape = {}
154
+ replica_availability_info = {}
155
+ list_of_complete_replica_locations = []
156
+ for tmp_rse, tmp_data_list in replica_map[dataset_name].items():
157
+ # check tape attribute
158
+ try:
159
+ is_tape[tmp_rse] = ddm_if.getSiteProperty(tmp_rse, "is_tape")
160
+ except Exception:
161
+ is_tape[tmp_rse] = None
162
+ # look for complete replicas
163
+ tmp_regarded_as_complete_disk = False
164
+ tmp_truly_complete_disk = False
165
+ tmp_complete_tape = False
166
+ tmp_can_be_local_source = False
167
+ tmp_can_be_remote_source = False
168
+ for tmp_data in tmp_data_list:
169
+ if not tmp_data.get("vp"):
170
+ if tmp_data["found"] == tmp_data["total"]:
171
+ # truly complete
172
+ truly_complete = True
173
+ elif (
174
+ tmp_data["total"]
175
+ and tmp_data["found"]
176
+ and (
177
+ tmp_data["total"] - tmp_data["found"] <= max_missing_input_files
178
+ or tmp_data["found"] / tmp_data["total"] * 100 >= min_input_completeness
179
+ )
180
+ ):
181
+ # regarded as complete
182
+ truly_complete = False
183
+ else:
184
+ continue
185
+ list_of_complete_replica_locations.append(tmp_rse)
186
+ if is_tape[tmp_rse]:
187
+ tmp_complete_tape = True
188
+ else:
189
+ tmp_regarded_as_complete_disk = True
190
+ if truly_complete:
191
+ tmp_truly_complete_disk = True
192
+ # check if it is locally accessible over LAN
193
+ if site_mapper.is_readable_locally(tmp_rse):
194
+ tmp_can_be_local_source = True
195
+ # check if it is remotely accessible over WAN
196
+ if site_mapper.is_readable_remotely(tmp_rse):
197
+ tmp_can_be_remote_source = True
198
+ replica_availability_info[tmp_rse] = {
199
+ "regarded_as_complete_disk": tmp_regarded_as_complete_disk,
200
+ "truly_complete_disk": tmp_truly_complete_disk,
201
+ "complete_tape": tmp_complete_tape,
202
+ "can_be_local_source": tmp_can_be_local_source,
203
+ "can_be_remote_source": tmp_can_be_remote_source,
204
+ }
205
+
206
+ # loop over all candidate sites
207
+ regarded_as_complete_disk = False
208
+ truly_complete_disk = False
209
+ complete_tape = False
210
+ can_be_local_source = False
211
+ can_be_remote_source = False
212
+ return_map = {}
213
+ if not site_list:
214
+ # make sure at least one loop to set the flags
215
+ site_list = [None]
216
+ for tmp_site_name in site_list:
217
+ if not site_mapper.checkSite(tmp_site_name):
218
+ continue
219
+ # get associated DDM endpoints
220
+ tmp_site_spec = site_mapper.getSite(tmp_site_name)
221
+ scope_input, scope_output = select_scope(tmp_site_spec, JobUtils.ANALY_PS, JobUtils.ANALY_PS)
222
+ try:
223
+ input_endpoints = tmp_site_spec.ddm_endpoints_input[scope_input].all.keys()
224
+ except Exception:
225
+ input_endpoints = {}
226
+ # loop over all associated endpoints
227
+ for tmp_rse in input_endpoints:
228
+ if tmp_rse in replica_map[dataset_name]:
229
+ # check completeness
230
+ tmp_statistics = replica_map[dataset_name][tmp_rse][-1]
231
+ if tmp_statistics["found"] is None:
232
+ tmp_dataset_completeness = "unknown"
233
+ # refresh request
234
+ pass
235
+ elif tmp_statistics["total"] == tmp_statistics["found"]:
236
+ tmp_dataset_completeness = "complete"
237
+ else:
238
+ tmp_dataset_completeness = "incomplete"
239
+ # append
240
+ if tmp_site_name not in return_map:
241
+ return_map[tmp_site_name] = {}
242
+ return_map[tmp_site_name][tmp_rse] = {"tape": is_tape[tmp_rse], "state": tmp_dataset_completeness}
243
+ if "vp" in tmp_statistics:
244
+ return_map[tmp_site_name][tmp_rse]["vp"] = tmp_statistics["vp"]
245
+ # set flags
246
+ for tmp_rse in replica_availability_info:
247
+ if replica_availability_info[tmp_rse]["can_be_local_source"] or replica_availability_info[tmp_rse]["can_be_remote_source"]:
248
+ if replica_availability_info[tmp_rse]["regarded_as_complete_disk"]:
249
+ regarded_as_complete_disk = True
250
+ if replica_availability_info[tmp_rse]["truly_complete_disk"]:
251
+ truly_complete_disk = True
252
+ if replica_availability_info[tmp_rse]["complete_tape"]:
253
+ complete_tape = True
254
+ if replica_availability_info[tmp_rse]["can_be_local_source"]:
255
+ can_be_local_source = True
256
+ if replica_availability_info[tmp_rse]["can_be_remote_source"]:
257
+ can_be_remote_source = True
258
+
259
+ return (
260
+ Interaction.SC_SUCCEEDED,
261
+ return_map,
262
+ regarded_as_complete_disk,
263
+ complete_tape,
264
+ truly_complete_disk,
265
+ can_be_local_source,
266
+ can_be_remote_source,
267
+ list_of_complete_replica_locations,
268
+ )
269
+
270
+
271
+ # get analysis sites where data is available at disk
272
+ def getAnalSitesWithDataDisk(dataSiteMap, includeTape=False, use_vp=True, use_incomplete=False):
273
+ sites_with_complete_replicas = []
274
+ sites_with_incomplete_replicas = []
275
+ sites_with_non_vp_disk_replicas = set()
276
+ sites_using_vp = set()
277
+ for tmp_site_name, tmpSeValMap in dataSiteMap.items():
278
+ for tmpSE, tmpValMap in tmpSeValMap.items():
279
+ # VP
280
+ if tmpValMap.get("vp"):
281
+ sites_using_vp.add(tmp_site_name)
282
+ if not use_vp:
283
+ continue
284
+ # on disk or tape
285
+ if includeTape or not tmpValMap["tape"]:
286
+ # complete replica available at disk, tape, or VP
287
+ if tmpValMap["state"] == "complete":
288
+ # has complete replica
289
+ if tmp_site_name not in sites_with_complete_replicas:
290
+ sites_with_complete_replicas.append(tmp_site_name)
291
+ # has non-VP disk replica
292
+ if not tmpValMap["tape"] and not tmpValMap.get("vp"):
293
+ sites_with_non_vp_disk_replicas.add(tmp_site_name)
294
+ else:
295
+ # incomplete replica at disk
296
+ if tmp_site_name not in sites_with_incomplete_replicas:
297
+ sites_with_incomplete_replicas.append(tmp_site_name)
298
+ # remove VP if complete non-VP disk replica is unavailable
299
+ if sites_with_non_vp_disk_replicas:
300
+ for tmpSiteNameVP in sites_using_vp:
301
+ if tmpSiteNameVP in sites_with_complete_replicas:
302
+ sites_with_complete_replicas.remove(tmpSiteNameVP)
303
+ if tmpSiteNameVP in sites_with_incomplete_replicas:
304
+ sites_with_incomplete_replicas.remove(tmpSiteNameVP)
305
+ # return sites with complete
306
+ if sites_with_complete_replicas:
307
+ if not use_incomplete or sites_with_non_vp_disk_replicas:
308
+ return sites_with_complete_replicas
309
+ else:
310
+ return sites_with_complete_replicas + sites_with_incomplete_replicas
311
+ # return sites with incomplete if complete is unavailable
312
+ return sites_with_incomplete_replicas
313
+
314
+
315
+ # get the number of jobs in a status
316
+ def getNumJobs(jobStatMap, computingSite, jobStatus, cloud=None, workQueue_tag=None):
317
+ if computingSite not in jobStatMap:
318
+ return 0
319
+ nJobs = 0
320
+ # loop over all workQueues
321
+ for tmpWorkQueue, tmpWorkQueueVal in jobStatMap[computingSite].items():
322
+ # workQueue is defined
323
+ if workQueue_tag is not None and workQueue_tag != tmpWorkQueue:
324
+ continue
325
+ # loop over all job status
326
+ for tmpJobStatus, tmpCount in tmpWorkQueueVal.items():
327
+ if tmpJobStatus == jobStatus:
328
+ nJobs += tmpCount
329
+
330
+ return nJobs
331
+
332
+
333
+ def get_total_nq_nr_ratio(job_stat_map, work_queue_tag=None):
334
+ """
335
+ Get the ratio of number of queued jobs to number of running jobs
336
+ """
337
+ nRunning = 0
338
+ nQueue = 0
339
+
340
+ # loop over all workQueues
341
+ for siteVal in job_stat_map.values():
342
+ for tmpWorkQueue in siteVal:
343
+ # workQueue is defined
344
+ if work_queue_tag is not None and work_queue_tag != tmpWorkQueue:
345
+ continue
346
+ tmpWorkQueueVal = siteVal[tmpWorkQueue]
347
+ # loop over all job status
348
+ for tmpJobStatus in ["defined", "assigned", "activated", "starting"]:
349
+ if tmpJobStatus in tmpWorkQueueVal:
350
+ nQueue += tmpWorkQueueVal[tmpJobStatus]
351
+ if "running" in tmpWorkQueueVal:
352
+ nRunning += tmpWorkQueueVal["running"]
353
+ try:
354
+ ratio = float(nQueue) / float(nRunning)
355
+ except Exception:
356
+ ratio = None
357
+
358
+ return ratio
359
+
360
+
361
+ def hasZeroShare(site_spec, task_spec, ignore_priority, tmp_log):
362
+ """
363
+ Check if the site has a zero share for the given task. Zero share means there is a policy preventing the site to be used for the task.
364
+
365
+ :param site_spec: SiteSpec object describing the site being checked in brokerage
366
+ :param task_spec: TaskSpec object describing the task being brokered
367
+ :param ignore_priority: Merging job chunks will skip the priority check
368
+ :param tmp_log: Logger object
369
+
370
+ :return: False means there is no policy defined and the site can be used.
371
+ True means the site has a fair share policy that prevents the site to be used for the task
372
+ """
373
+
374
+ # there is no per-site share defined (CRIC "fairsharePolicy" field), the site can be used
375
+ if site_spec.fairsharePolicy in ["", None]:
376
+ return False
377
+
378
+ try:
379
+ # get the group of processing types from a pre-defined mapping
380
+ processing_group = ProcessGroups.getProcessGroup(task_spec.processingType)
381
+
382
+ # don't suppress test tasks - the site can be used
383
+ if processing_group in ["test"]:
384
+ return False
385
+
386
+ # loop over all policies
387
+ for policy in site_spec.fairsharePolicy.split(","):
388
+ # Examples of policies are:
389
+ # type=evgen:100%,type=simul:100%,type=any:0%
390
+ # type=evgen:100%,type=simul:100%,type=any:0%,group=(AP_Higgs|AP_Susy|AP_Exotics|Higgs):0%
391
+ # gshare=Express:100%,gshare=any:0%
392
+ # priority>400:0
393
+ tmp_processing_type = None
394
+ tmp_working_group = None
395
+ tmp_priority = None
396
+ tmp_gshare = None
397
+ tmp_fair_share = policy.split(":")[-1]
398
+
399
+ # break down each fair share policy into its fields
400
+ for tmp_field in policy.split(":"):
401
+ if tmp_field.startswith("type="):
402
+ tmp_processing_type = tmp_field.split("=")[-1]
403
+ elif tmp_field.startswith("group="):
404
+ tmp_working_group = tmp_field.split("=")[-1]
405
+ elif tmp_field.startswith("gshare="):
406
+ tmp_gshare = tmp_field.split("=")[-1]
407
+ elif tmp_field.startswith("priority"):
408
+ tmp_priority = re.sub("priority", "", tmp_field)
409
+
410
+ # check for a matching processing type
411
+ if tmp_processing_type not in ["any", None]:
412
+ if "*" in tmp_processing_type:
413
+ tmp_processing_type = tmp_processing_type.replace("*", ".*")
414
+ # if there is no match between the site's fair share policy and the task's processing type,
415
+ # so continue looking for other policies that trigger the zero share condition
416
+ if re.search(f"^{tmp_processing_type}$", processing_group) is None:
417
+ continue
418
+
419
+ # check for matching working group
420
+ if tmp_working_group not in ["any", None]:
421
+ # None causes an exception in re.search, so convert to empty string
422
+ task_working_group = task_spec.workingGroup or ""
423
+ if "*" in tmp_working_group:
424
+ tmp_working_group = tmp_working_group.replace("*", ".*")
425
+ # if there is no match between the site's fair share policy and the task's working group
426
+ # continue looking for other policies that trigger the zero share condition
427
+ if re.search(f"^{tmp_working_group}$", task_working_group) is None:
428
+ continue
429
+
430
+ # check for matching gshare. Note that this only works for "leave gshares" in the fairsharePolicy,
431
+ # i.e. the ones that have no sub-gshares, since the task only gets "leave gshares" assigned
432
+ if tmp_gshare not in ["any", None] and task_spec.gshare is not None:
433
+ # None causes an exception in re.search, so convert to empty string
434
+ task_gshare = task_spec.gshare or ""
435
+ if "*" in tmp_gshare:
436
+ tmp_gshare = tmp_gshare.replace("*", ".*")
437
+ # if there is no match between the site's fair share policy and the task's gshare
438
+ # continue looking for other policies that trigger the zero share condition
439
+ if re.search(f"^{tmp_gshare}$", task_gshare) is None:
440
+ continue
441
+
442
+ # check priority
443
+ if tmp_priority is not None and not ignore_priority:
444
+ try:
445
+ exec(f"tmpStat = {task_spec.currentPriority}{tmp_priority}", globals())
446
+ tmp_log.debug(
447
+ f"Priority check for {site_spec.sitename}, {task_spec.currentPriority}): " f"{task_spec.currentPriority}{tmp_priority} = {tmpStat}"
448
+ )
449
+ if not tmpStat:
450
+ continue
451
+ except Exception:
452
+ error_type, error_value = sys.exc_info()[:2]
453
+ tmp_log.error(f"Priority check for {site_spec.sitename} failed with {error_type}:{error_value}")
454
+
455
+ # check fair share value
456
+ # if 0, we need to skip the site
457
+ # if different than 0, the site can be used
458
+ if tmp_fair_share in ["0", "0%"]:
459
+ return True
460
+ else:
461
+ return False
462
+
463
+ except Exception:
464
+ error_type, error_value = sys.exc_info()[:2]
465
+ tmp_log.error(f"hasZeroShare failed with {error_type}:{error_value}")
466
+
467
+ # if we reach this point, it means there is no policy preventing the site to be used
468
+ return False
469
+
470
+
471
+ # check if site name is matched with one of list items
472
+ def isMatched(siteName, nameList):
473
+ for tmpName in nameList:
474
+ # ignore empty
475
+ if tmpName == "":
476
+ continue
477
+ # wild card
478
+ if "*" in tmpName:
479
+ tmpName = tmpName.replace("*", ".*")
480
+ if re.search(tmpName, siteName) is not None:
481
+ return True
482
+ else:
483
+ # normal pattern
484
+ if tmpName == siteName:
485
+ return True
486
+
487
+ return False
488
+
489
+
490
+ # get dict to set nucleus
491
+ def getDictToSetNucleus(nucleusSpec, tmpDatasetSpecs):
492
+ # get destinations
493
+ return_map = {"datasets": [], "nucleus": nucleusSpec.name}
494
+ for datasetSpec in tmpDatasetSpecs:
495
+ # skip distributed datasets
496
+ if DataServiceUtils.getDistributedDestination(datasetSpec.storageToken) is not None:
497
+ continue
498
+ # get endpoint relevant to token
499
+ endPoint = nucleusSpec.getAssociatedEndpoint(datasetSpec.storageToken)
500
+ if endPoint is None and not nucleusSpec.is_nucleus() and nucleusSpec.get_default_endpoint_out():
501
+ # use default endpoint for satellite that doesn't have relevant endpoint
502
+ endPoint = nucleusSpec.get_default_endpoint_out()
503
+ if endPoint is None:
504
+ continue
505
+ token = endPoint["ddm_endpoint_name"]
506
+ # add original token
507
+ if datasetSpec.storageToken not in ["", None]:
508
+ token += f"/{datasetSpec.storageToken.split('/')[-1]}"
509
+ return_map["datasets"].append({"datasetID": datasetSpec.datasetID, "token": f"dst:{token}", "destination": f"nucleus:{nucleusSpec.name}"})
510
+ return return_map
511
+
512
+
513
+ # remove problematic sites
514
+ def skipProblematicSites(candidateSpecList, ngSites, sitesUsedByTask, preSetSiteSpec, maxNumSites, tmpLog):
515
+ skippedSites = set()
516
+ usedSitesGood = []
517
+ newSitesGood = []
518
+ # collect sites already used by the task
519
+ for candidateSpec in candidateSpecList:
520
+ # check if problematic
521
+ if (candidateSpec.siteName in ngSites or candidateSpec.unifiedName in ngSites) and (
522
+ preSetSiteSpec is None or candidateSpec.siteName != preSetSiteSpec.siteName
523
+ ):
524
+ skippedSites.add(candidateSpec.siteName)
525
+ else:
526
+ if candidateSpec.siteName in sitesUsedByTask or candidateSpec.unifiedName in sitesUsedByTask:
527
+ usedSitesGood.append(candidateSpec)
528
+ else:
529
+ newSitesGood.append(candidateSpec)
530
+ # set number of sites if undefined
531
+ if maxNumSites in [0, None]:
532
+ maxNumSites = len(candidateSpecList)
533
+ newcandidateSpecList = usedSitesGood + newSitesGood
534
+ newcandidateSpecList = newcandidateSpecList[:maxNumSites]
535
+ # dump
536
+ for skippedSite in skippedSites:
537
+ tmpLog.debug(f"getting rid of problematic site {skippedSite}")
538
+ return newcandidateSpecList
539
+
540
+
541
+ # get mapping between sites and input storage endpoints
542
+ def getSiteInputStorageEndpointMap(site_list, site_mapper, prod_source_label, job_label):
543
+ # make a map of panda sites to ddm endpoints
544
+ ret_map = {}
545
+ for site_name in site_list:
546
+ tmp_site_spec = site_mapper.getSite(site_name)
547
+ scope_input, scope_output = select_scope(tmp_site_spec, prod_source_label, job_label)
548
+
549
+ # skip if scope not available
550
+ if scope_input not in tmp_site_spec.ddm_endpoints_input:
551
+ continue
552
+
553
+ # add the schedconfig.ddm endpoints
554
+ ret_map[site_name] = list(tmp_site_spec.ddm_endpoints_input[scope_input].all.keys())
555
+
556
+ return ret_map
557
+
558
+
559
+ # get to-running rate of sites from various resources
560
+ CACHE_SiteToRunRateStats = {}
561
+
562
+
563
+ def getSiteToRunRateStats(tbIF, vo, time_window=21600, cutoff=300, cache_lifetime=600):
564
+ # initialize
565
+ ret_val = False
566
+ ret_map = {}
567
+ # DB cache keys
568
+ dc_main_key = "AtlasSites"
569
+ dc_sub_key = "SiteToRunRate"
570
+ # arguments for process lock
571
+ this_prodsourcelabel = "user"
572
+ this_pid = f"{socket.getfqdn().split('.')[0]}-{os.getpid()}_{os.getpgrp()}-broker"
573
+ this_component = "Cache.SiteToRunRate"
574
+ # timestamps
575
+ current_time = naive_utcnow()
576
+ starttime_max = current_time - datetime.timedelta(seconds=cutoff)
577
+ starttime_min = current_time - datetime.timedelta(seconds=time_window)
578
+ # rounded with 10 minutes
579
+ starttime_max_rounded = starttime_max.replace(minute=starttime_max.minute // 10 * 10, second=0, microsecond=0)
580
+ starttime_min_rounded = starttime_min.replace(minute=starttime_min.minute // 10 * 10, second=0, microsecond=0)
581
+ real_interval_hours = (starttime_max_rounded - starttime_min_rounded).total_seconds() / 3600
582
+ # local cache key
583
+ local_cache_key = (starttime_min_rounded, starttime_max_rounded)
584
+ # condition of query
585
+ if local_cache_key in CACHE_SiteToRunRateStats and current_time <= CACHE_SiteToRunRateStats[local_cache_key]["exp"]:
586
+ # query from local cache
587
+ ret_val = True
588
+ ret_map = CACHE_SiteToRunRateStats[local_cache_key]["data"]
589
+ else:
590
+ # try some times
591
+ for _ in range(99):
592
+ # skip if too long after original current time
593
+ if naive_utcnow() - current_time > datetime.timedelta(seconds=min(10, cache_lifetime / 4)):
594
+ # break trying
595
+ break
596
+ try:
597
+ # query from DB cache
598
+ cache_spec = tbIF.getCache_JEDI(main_key=dc_main_key, sub_key=dc_sub_key)
599
+ if cache_spec is not None:
600
+ expired_time = cache_spec.last_update + datetime.timedelta(seconds=cache_lifetime)
601
+ if current_time <= expired_time:
602
+ # valid DB cache
603
+ ret_val = True
604
+ ret_map = json.loads(cache_spec.data)
605
+ # fill local cache
606
+ CACHE_SiteToRunRateStats[local_cache_key] = {"exp": expired_time, "data": ret_map}
607
+ # break trying
608
+ break
609
+ # got process lock
610
+ got_lock = tbIF.lockProcess_JEDI(
611
+ vo=vo,
612
+ prodSourceLabel=this_prodsourcelabel,
613
+ cloud=None,
614
+ workqueue_id=None,
615
+ resource_name=None,
616
+ component=this_component,
617
+ pid=this_pid,
618
+ timeLimit=5,
619
+ )
620
+ if not got_lock:
621
+ # not getting lock, sleep and query cache again
622
+ time.sleep(1)
623
+ continue
624
+ # query from PanDA DB directly
625
+ ret_val, ret_map = tbIF.getSiteToRunRateStats(vo=vo, exclude_rwq=False, starttime_min=starttime_min, starttime_max=starttime_max)
626
+ if ret_val:
627
+ # expired time
628
+ expired_time = current_time + datetime.timedelta(seconds=cache_lifetime)
629
+ # fill local cache
630
+ CACHE_SiteToRunRateStats[local_cache_key] = {"exp": expired_time, "data": ret_map}
631
+ # json of data
632
+ data_json = json.dumps(ret_map)
633
+ # fill DB cache
634
+ tbIF.updateCache_JEDI(main_key=dc_main_key, sub_key=dc_sub_key, data=data_json)
635
+ # unlock process
636
+ tbIF.unlockProcess_JEDI(
637
+ vo=vo, prodSourceLabel=this_prodsourcelabel, cloud=None, workqueue_id=None, resource_name=None, component=this_component, pid=this_pid
638
+ )
639
+ # break trying
640
+ break
641
+ except Exception as e:
642
+ # dump error message
643
+ err_str = f"AtlasBrokerUtils.getSiteToRunRateStats got {e.__class__.__name__}: {e} \n"
644
+ sys.stderr.write(err_str)
645
+ # break trying
646
+ break
647
+ # delete outdated entries in local cache
648
+ for lc_key in list(CACHE_SiteToRunRateStats.keys()):
649
+ lc_time_min, lc_time_max = lc_key
650
+ if lc_time_max < starttime_max_rounded - datetime.timedelta(seconds=cache_lifetime):
651
+ try:
652
+ del CACHE_SiteToRunRateStats[lc_key]
653
+ except Exception as e:
654
+ err_str = f"AtlasBrokerUtils.getSiteToRunRateStats when deleting outdated entries got {e.__class__.__name__}: {e} \n"
655
+ sys.stderr.write(err_str)
656
+
657
+ return ret_val, ret_map
658
+
659
+
660
+ # get users jobs stats from various resources
661
+ CACHE_UsersJobsStats = {}
662
+
663
+
664
+ def getUsersJobsStats(tbIF, vo, prod_source_label, cache_lifetime=60):
665
+ # initialize
666
+ ret_val = False
667
+ ret_map = {}
668
+ # DB cache keys
669
+ dc_main_key = "AtlasSites"
670
+ dc_sub_key = "UsersJobsStats"
671
+ # arguments for process lock
672
+ this_prodsourcelabel = prod_source_label
673
+ this_pid = this_pid = f"{socket.getfqdn().split('.')[0]}-{os.getpid()}_{os.getpgrp()}-broker"
674
+ this_component = "Cache.UsersJobsStats"
675
+ # local cache key; a must if not using global variable
676
+ local_cache_key = "_main"
677
+ # timestamps
678
+ current_time = naive_utcnow()
679
+ # condition of query
680
+ if local_cache_key in CACHE_UsersJobsStats and current_time <= CACHE_UsersJobsStats[local_cache_key]["exp"]:
681
+ # query from local cache
682
+ ret_val = True
683
+ ret_map = CACHE_UsersJobsStats[local_cache_key]["data"]
684
+ else:
685
+ # try some times
686
+ for _ in range(99):
687
+ # skip if too long after original current time
688
+ if naive_utcnow() - current_time > datetime.timedelta(seconds=min(15, cache_lifetime / 4)):
689
+ # break trying
690
+ break
691
+ try:
692
+ # query from DB cache
693
+ cache_spec = tbIF.getCache_JEDI(main_key=dc_main_key, sub_key=dc_sub_key)
694
+ if cache_spec is not None:
695
+ expired_time = cache_spec.last_update + datetime.timedelta(seconds=cache_lifetime)
696
+ if current_time <= expired_time:
697
+ # valid DB cache
698
+ ret_val = True
699
+ ret_map = json.loads(cache_spec.data)
700
+ # fill local cache
701
+ CACHE_UsersJobsStats[local_cache_key] = {"exp": expired_time, "data": ret_map}
702
+ # break trying
703
+ break
704
+ # got process lock
705
+ got_lock = tbIF.lockProcess_JEDI(
706
+ vo=vo,
707
+ prodSourceLabel=this_prodsourcelabel,
708
+ cloud=None,
709
+ workqueue_id=None,
710
+ resource_name=None,
711
+ component=this_component,
712
+ pid=this_pid,
713
+ timeLimit=(cache_lifetime * 0.75 / 60),
714
+ )
715
+ if not got_lock:
716
+ # not getting lock, sleep and query cache again
717
+ time.sleep(1)
718
+ continue
719
+ # query from PanDA DB directly
720
+ ret_map = tbIF.getUsersJobsStats_JEDI(prod_source_label=this_prodsourcelabel)
721
+ if ret_map is not None:
722
+ # expired time
723
+ expired_time = current_time + datetime.timedelta(seconds=cache_lifetime)
724
+ # fill local cache
725
+ CACHE_UsersJobsStats[local_cache_key] = {"exp": expired_time, "data": ret_map}
726
+ # json of data
727
+ data_json = json.dumps(ret_map)
728
+ # fill DB cache
729
+ tbIF.updateCache_JEDI(main_key=dc_main_key, sub_key=dc_sub_key, data=data_json)
730
+ # make True return
731
+ ret_val = True
732
+ # unlock process
733
+ tbIF.unlockProcess_JEDI(
734
+ vo=vo, prodSourceLabel=this_prodsourcelabel, cloud=None, workqueue_id=None, resource_name=None, component=this_component, pid=this_pid
735
+ )
736
+ # break trying
737
+ break
738
+ except Exception as e:
739
+ # dump error message
740
+ err_str = f"AtlasBrokerUtils.getUsersJobsStats got {e.__class__.__name__}: {e} \n"
741
+ sys.stderr.write(err_str)
742
+ # break trying
743
+ break
744
+
745
+ return ret_val, ret_map
746
+
747
+
748
+ # get gshare usage
749
+ def getGShareUsage(tbIF, gshare, fresher_than_minutes_ago=15):
750
+ # initialize
751
+ ret_val = False
752
+ ret_map = {}
753
+ # timestamps
754
+ current_time = naive_utcnow()
755
+ # try some times
756
+ for _ in range(99):
757
+ now_time = naive_utcnow()
758
+ # skip if too long after original current time
759
+ if now_time - current_time > datetime.timedelta(seconds=max(3, fresher_than_minutes_ago / 3)):
760
+ # break trying
761
+ break
762
+ try:
763
+ # query from PanDA DB directly
764
+ sql_get_gshare = "SELECT m.value_json FROM ATLAS_PANDA.Metrics m WHERE m.metric=:metric AND m.gshare=:gshare AND m.timestamp>=:min_timestamp "
765
+ var_map = {
766
+ ":metric": "gshare_preference",
767
+ ":gshare": gshare,
768
+ ":min_timestamp": now_time - datetime.timedelta(minutes=fresher_than_minutes_ago),
769
+ }
770
+ # result
771
+ res = tbIF.querySQL(sql_get_gshare, var_map)
772
+ if res:
773
+ value_json = res[0][0]
774
+ # json of data
775
+ ret_map = json.loads(value_json)
776
+ # make True return
777
+ ret_val = True
778
+ # break trying
779
+ break
780
+ except Exception as e:
781
+ # dump error message
782
+ err_str = f"AtlasBrokerUtils.getGShareUsage got {e.__class__.__name__}: {e} \n"
783
+ sys.stderr.write(err_str)
784
+ # break trying
785
+ break
786
+
787
+ return ret_val, ret_map
788
+
789
+
790
+ # get user evaluation
791
+ def getUserEval(tbIF, user, fresher_than_minutes_ago=20):
792
+ # initialize
793
+ ret_val = False
794
+ ret_map = {}
795
+ # timestamps
796
+ current_time = naive_utcnow()
797
+ # try some times
798
+ for _ in range(99):
799
+ now_time = naive_utcnow()
800
+ # skip if too long after original current time
801
+ if now_time - current_time > datetime.timedelta(seconds=max(3, fresher_than_minutes_ago / 3)):
802
+ # break trying
803
+ break
804
+ try:
805
+ # query from PanDA DB directly
806
+ sql_get_user_eval = f'SELECT m.value_json."{user}" FROM ATLAS_PANDA.Metrics m WHERE m.metric=:metric AND m.timestamp>=:min_timestamp '
807
+ var_map = {
808
+ ":metric": "analy_user_eval",
809
+ ":min_timestamp": now_time - datetime.timedelta(minutes=fresher_than_minutes_ago),
810
+ }
811
+ # result
812
+ res = tbIF.querySQL(sql_get_user_eval, var_map)
813
+ if res:
814
+ value_json = res[0][0]
815
+ # json of data
816
+ ret_map = json.loads(value_json) if value_json else None
817
+ # make True return
818
+ ret_val = True
819
+ # break trying
820
+ break
821
+ except Exception as e:
822
+ # dump error message
823
+ err_str = f"AtlasBrokerUtils.getUserEval got {e.__class__.__name__}: {e} \n"
824
+ sys.stderr.write(err_str)
825
+ # break trying
826
+ break
827
+
828
+ return ret_val, ret_map
829
+
830
+
831
+ # get user task evaluation
832
+ def getUserTaskEval(tbIF, taskID, fresher_than_minutes_ago=15):
833
+ # initialize
834
+ ret_val = False
835
+ ret_map = {}
836
+ # timestamps
837
+ current_time = naive_utcnow()
838
+ # try some times
839
+ for _ in range(99):
840
+ now_time = naive_utcnow()
841
+ # skip if too long after original current time
842
+ if now_time - current_time > datetime.timedelta(seconds=max(3, fresher_than_minutes_ago / 3)):
843
+ # break trying
844
+ break
845
+ try:
846
+ # query from PanDA DB directly
847
+ sql_get_task_eval = (
848
+ "SELECT tev.value_json "
849
+ "FROM ATLAS_PANDA.Task_Evaluation tev "
850
+ "WHERE tev.metric=:metric "
851
+ "AND tev.jediTaskID=:taskID "
852
+ "AND tev.timestamp>=:min_timestamp "
853
+ )
854
+ var_map = {
855
+ ":metric": "analy_task_eval",
856
+ ":taskID": taskID,
857
+ ":min_timestamp": now_time - datetime.timedelta(minutes=fresher_than_minutes_ago),
858
+ }
859
+
860
+ res = tbIF.querySQL(sql_get_task_eval, var_map)
861
+ if res:
862
+ value_json = res[0][0]
863
+ # json of data
864
+ ret_map = json.loads(value_json) if value_json else None
865
+ # make True return
866
+ ret_val = True
867
+ # break trying
868
+ break
869
+ except Exception as e:
870
+ # dump error message
871
+ err_str = f"AtlasBrokerUtils.getUserTaskEval got {e.__class__.__name__}: {e} \n"
872
+ sys.stderr.write(err_str)
873
+ # break trying
874
+ break
875
+
876
+ return ret_val, ret_map
877
+
878
+
879
+ # get analysis sites class
880
+ def getAnalySitesClass(tbIF, fresher_than_minutes_ago=60):
881
+ # initialize
882
+ ret_val = False
883
+ ret_map = {}
884
+ # timestamps
885
+ current_time = naive_utcnow()
886
+ # try some times
887
+ for _ in range(99):
888
+ now_time = naive_utcnow()
889
+ # skip if too long after original current time
890
+ if now_time - current_time > datetime.timedelta(seconds=max(3, fresher_than_minutes_ago / 3)):
891
+ # break trying
892
+ break
893
+ try:
894
+ # query from PanDA DB directly
895
+ sql_get_task_eval = "SELECT m.computingSite, m.value_json.class FROM ATLAS_PANDA.Metrics m WHERE m.metric=:metric AND m.timestamp>=:min_timestamp "
896
+ var_map = {
897
+ ":metric": "analy_site_eval",
898
+ ":min_timestamp": now_time - datetime.timedelta(minutes=fresher_than_minutes_ago),
899
+ }
900
+ res = tbIF.querySQL(sql_get_task_eval, var_map)
901
+ if res:
902
+ for site, class_value in res:
903
+ ret_map[site] = int(class_value)
904
+ # make True return
905
+ ret_val = True
906
+ # break trying
907
+ break
908
+ except Exception as e:
909
+ # dump error message
910
+ err_str = f"AtlasBrokerUtils.getAnalySitesEval got {e.__class__.__name__}: {e} \n"
911
+ sys.stderr.write(err_str)
912
+ # break trying
913
+ break
914
+
915
+ return ret_val, ret_map
916
+
917
+
918
+ def compare_version_string(version_string, comparison_string):
919
+ """
920
+ Compares a version string with another string composed of a comparison operator and a version string.
921
+
922
+ Args:
923
+ version_string (str): The version string to compare.
924
+ comparison_string (str): The string containing the comparison operator and version string (e.g., ">=2.0").
925
+
926
+ Returns:
927
+ bool or None: True if the version string satisfies the comparison, False if it doesn't,
928
+ or None if the comparison string is invalid.
929
+ """
930
+ match = re.match(r"([=><]+)(.+)", comparison_string)
931
+ if not match:
932
+ return None
933
+
934
+ operator = match.group(1).strip()
935
+ version_to_compare = match.group(2).strip()
936
+
937
+ try:
938
+ version1 = version.parse(version_string)
939
+ version2 = version.parse(version_to_compare)
940
+ except version.InvalidVersion:
941
+ return None
942
+
943
+ if operator == "==":
944
+ return version1 == version2
945
+ elif operator == "!=":
946
+ return version1 != version2
947
+ elif operator == ">=":
948
+ return version1 >= version2
949
+ elif operator == "<=":
950
+ return version1 <= version2
951
+ elif operator == ">":
952
+ return version1 > version2
953
+ elif operator == "<":
954
+ return version1 < version2
955
+ else:
956
+ return None
957
+
958
+
959
+ # check SW with json
960
+ class JsonSoftwareCheck:
961
+ # constructor
962
+ def __init__(self, site_mapper, sw_map, wn_architecture_level_map):
963
+ self.siteMapper = site_mapper
964
+ self.sw_map = sw_map
965
+ self.wn_architecture_level_map = wn_architecture_level_map
966
+
967
+ # get lists
968
+ def check(
969
+ self,
970
+ site_list,
971
+ cvmfs_tag,
972
+ sw_project,
973
+ sw_version,
974
+ cmt_config,
975
+ need_cvmfs,
976
+ cmt_config_only,
977
+ need_container=False,
978
+ container_name=None,
979
+ only_tags_fc=False,
980
+ host_cpu_specs=None,
981
+ host_cpu_pref=None,
982
+ host_gpu_spec=None,
983
+ log_stream=None,
984
+ ):
985
+ ok_sites = []
986
+ no_auto_sites = []
987
+ preference_weight_map = {}
988
+
989
+ # Does the task define a preferred architecture level?
990
+ preferred_architecture_level = host_cpu_pref.get("preferred_architecture_level") if host_cpu_pref else None
991
+
992
+ for tmp_site_name in site_list:
993
+ tmp_site_spec = self.siteMapper.getSite(tmp_site_name)
994
+ if tmp_site_spec.releases == ["AUTO"] and tmp_site_name in self.sw_map:
995
+ go_ahead = False
996
+ try:
997
+ # convert to a dict
998
+ architecture_map = {}
999
+ if "architectures" in self.sw_map[tmp_site_name]:
1000
+ for arch_spec in self.sw_map[tmp_site_name]["architectures"]:
1001
+ if "type" in arch_spec:
1002
+ architecture_map[arch_spec["type"]] = arch_spec
1003
+
1004
+ # check if need CPU
1005
+ if "cpu" in architecture_map:
1006
+ need_cpu = False
1007
+ for k in architecture_map["cpu"]:
1008
+ if isinstance(architecture_map["cpu"][k], list):
1009
+ if "excl" in architecture_map["cpu"][k]:
1010
+ need_cpu = True
1011
+ break
1012
+ if need_cpu and host_cpu_specs is None:
1013
+ continue
1014
+
1015
+ # check if need GPU
1016
+ if "gpu" in architecture_map:
1017
+ need_gpu = False
1018
+ for k in architecture_map["gpu"]:
1019
+ if isinstance(architecture_map["gpu"][k], list):
1020
+ if "excl" in architecture_map["gpu"][k]:
1021
+ need_gpu = True
1022
+ break
1023
+ if need_gpu and host_gpu_spec is None:
1024
+ continue
1025
+
1026
+ # calculate the preference_weight_map for the site
1027
+ if preferred_architecture_level:
1028
+ preference_weight_map[tmp_site_name] = (
1029
+ 1 + self.wn_architecture_level_map.get(tmp_site_name, {}).get(preferred_architecture_level, {}).get("pct_within_queue", 0) / 100
1030
+ )
1031
+
1032
+ if host_cpu_specs or host_gpu_spec:
1033
+ # skip since the PQ doesn't describe HW spec
1034
+ if not architecture_map:
1035
+ continue
1036
+ # check CPU
1037
+ if host_cpu_specs:
1038
+ host_ok = False
1039
+ for host_cpu_spec in host_cpu_specs:
1040
+ # CPU not specified
1041
+ if "cpu" not in architecture_map:
1042
+ continue
1043
+
1044
+ # check architecture
1045
+ if host_cpu_spec["arch"] == "*":
1046
+ if "excl" in architecture_map["cpu"]["arch"]:
1047
+ continue
1048
+ else:
1049
+ if "any" not in architecture_map["cpu"]["arch"]:
1050
+ if host_cpu_spec["arch"] not in architecture_map["cpu"]["arch"]:
1051
+ # check with regex
1052
+ if not [True for iii in architecture_map["cpu"]["arch"] if re.search("^" + host_cpu_spec["arch"] + "$", iii)]:
1053
+ continue
1054
+
1055
+ # check vendor
1056
+ if host_cpu_spec["vendor"] == "*":
1057
+ # task doesn't specify a vendor and PQ explicitly requests a specific vendor
1058
+ if "vendor" in architecture_map["cpu"] and "excl" in architecture_map["cpu"]["vendor"]:
1059
+ continue
1060
+ else:
1061
+ # task specifies a vendor and PQ doesn't request any specific vendor
1062
+ if "vendor" not in architecture_map["cpu"]:
1063
+ continue
1064
+ # task specifies a vendor and PQ doesn't accept any vendor or the specific vendor
1065
+ if "any" not in architecture_map["cpu"]["vendor"] and host_cpu_spec["vendor"] not in architecture_map["cpu"]["vendor"]:
1066
+ continue
1067
+
1068
+ # check instruction set
1069
+ if host_cpu_spec["instr"] == "*":
1070
+ if "instr" in architecture_map["cpu"] and "excl" in architecture_map["cpu"]["instr"]:
1071
+ continue
1072
+ else:
1073
+ if "instr" not in architecture_map["cpu"]:
1074
+ continue
1075
+ if "any" not in architecture_map["cpu"]["instr"] and host_cpu_spec["instr"] not in architecture_map["cpu"]["instr"]:
1076
+ continue
1077
+ host_ok = True
1078
+ break
1079
+ if not host_ok:
1080
+ continue
1081
+
1082
+ # check GPU
1083
+ if host_gpu_spec:
1084
+ # GPU not specified
1085
+ if "gpu" not in architecture_map:
1086
+ continue
1087
+
1088
+ # check vendor
1089
+ if host_gpu_spec["vendor"] == "*":
1090
+ # task doesn't specify CPU vendor and PQ explicitly requests a specific vendor
1091
+ if "vendor" in architecture_map["gpu"] and "excl" in architecture_map["gpu"]["vendor"]:
1092
+ continue
1093
+ else:
1094
+ # task specifies a vendor and PQ doesn't request any specific vendor
1095
+ if "vendor" not in architecture_map["gpu"]:
1096
+ continue
1097
+ # task specifies a vendor and PQ doesn't accept any vendor or the specific vendor
1098
+ if "any" not in architecture_map["gpu"]["vendor"] and host_gpu_spec["vendor"] not in architecture_map["gpu"]["vendor"]:
1099
+ continue
1100
+
1101
+ # check model
1102
+ if host_gpu_spec["model"] == "*":
1103
+ if "model" in architecture_map["gpu"] and "excl" in architecture_map["gpu"]["model"]:
1104
+ continue
1105
+ else:
1106
+ if isinstance(host_gpu_spec["model"], dict):
1107
+ model_pattern = host_gpu_spec["model"]["pattern"]
1108
+ model_excl = host_gpu_spec["model"].get("excl", False)
1109
+ else:
1110
+ model_pattern = host_gpu_spec["model"]
1111
+ model_excl = False
1112
+ if "model" not in architecture_map["gpu"] or (
1113
+ "any" not in architecture_map["gpu"]["model"]
1114
+ and any(re.match(model_pattern, m, re.IGNORECASE) for m in architecture_map["gpu"]["model"])
1115
+ == model_excl
1116
+ ):
1117
+ continue
1118
+
1119
+ # check version
1120
+ if "version" in host_gpu_spec:
1121
+ if "version" not in architecture_map["gpu"]:
1122
+ # PQ doesn't specify version
1123
+ continue
1124
+ elif "any" == architecture_map["gpu"]["version"]:
1125
+ # PQ accepts any version
1126
+ pass
1127
+ else:
1128
+ # check version at PQ
1129
+ if not compare_version_string(architecture_map["gpu"]["version"], host_gpu_spec["version"]):
1130
+ continue
1131
+ go_ahead = True
1132
+ except Exception as e:
1133
+ if log_stream:
1134
+ log_stream.error(f"json check {str(architecture_map)} failed for {tmp_site_name} {str(e)} {traceback.format_exc()} ")
1135
+ if not go_ahead:
1136
+ continue
1137
+ # only HW check
1138
+ if not (cvmfs_tag or cmt_config or sw_project or sw_version or container_name) and (host_cpu_specs or host_gpu_spec):
1139
+ ok_sites.append(tmp_site_name)
1140
+ continue
1141
+ # check for fat container
1142
+ if container_name:
1143
+ # check for container
1144
+ if not only_tags_fc and ("any" in self.sw_map[tmp_site_name]["containers"] or "/cvmfs" in self.sw_map[tmp_site_name]["containers"]):
1145
+ # any in containers
1146
+ ok_sites.append(tmp_site_name)
1147
+ elif container_name in set([t["container_name"] for t in self.sw_map[tmp_site_name]["tags"] if t["container_name"]]):
1148
+ # logical name in tags or any in containers
1149
+ ok_sites.append(tmp_site_name)
1150
+ elif container_name in set([s for t in self.sw_map[tmp_site_name]["tags"] for s in t["sources"] if t["sources"]]):
1151
+ # full path in sources
1152
+ ok_sites.append(tmp_site_name)
1153
+ elif not only_tags_fc:
1154
+ # get sources in all tag list
1155
+ if "ALL" in self.sw_map:
1156
+ source_list_in_all_tag = [s for t in self.sw_map["ALL"]["tags"] for s in t["sources"] if t["container_name"] == container_name]
1157
+ else:
1158
+ source_list_in_all_tag = []
1159
+ # prefix with full path
1160
+ for tmp_prefix in self.sw_map[tmp_site_name]["containers"]:
1161
+ if container_name.startswith(tmp_prefix):
1162
+ ok_sites.append(tmp_site_name)
1163
+ break
1164
+ toBreak = False
1165
+ for source_in_all_tag in source_list_in_all_tag:
1166
+ if source_in_all_tag.startswith(tmp_prefix):
1167
+ ok_sites.append(tmp_site_name)
1168
+ toBreak = True
1169
+ break
1170
+ if toBreak:
1171
+ break
1172
+ continue
1173
+ # only cmt config check
1174
+ if cmt_config_only:
1175
+ ok_sites.append(tmp_site_name)
1176
+ continue
1177
+ # check if CVMFS is available
1178
+ if "any" in self.sw_map[tmp_site_name]["cvmfs"] or cvmfs_tag in self.sw_map[tmp_site_name]["cvmfs"]:
1179
+ # check if container is available
1180
+ if "any" in self.sw_map[tmp_site_name]["containers"] or "/cvmfs" in self.sw_map[tmp_site_name]["containers"]:
1181
+ ok_sites.append(tmp_site_name)
1182
+ # check cmt config
1183
+ elif not need_container and cmt_config in self.sw_map[tmp_site_name]["cmtconfigs"]:
1184
+ ok_sites.append(tmp_site_name)
1185
+ elif not need_cvmfs:
1186
+ if not need_container or "any" in self.sw_map[tmp_site_name]["containers"]:
1187
+ # check tags
1188
+ for tag in self.sw_map[tmp_site_name]["tags"]:
1189
+ if tag["cmtconfig"] == cmt_config and tag["project"] == sw_project and tag["release"] == sw_version:
1190
+ ok_sites.append(tmp_site_name)
1191
+ break
1192
+ # don't pass to subsequent check if AUTO is enabled
1193
+ continue
1194
+ # use only AUTO for container or HW
1195
+ if container_name is not None or host_cpu_specs is not None or host_gpu_spec is not None:
1196
+ continue
1197
+ no_auto_sites.append(tmp_site_name)
1198
+
1199
+ return ok_sites, no_auto_sites, preference_weight_map
1200
+
1201
+
1202
+ # resolve cmt_config
1203
+ def resolve_cmt_config(queue_name: str, cmt_config: str, base_platform, sw_map: dict) -> str | None:
1204
+ """
1205
+ resolve cmt config at a given queue_name
1206
+ :param queue_name: queue name
1207
+ :param cmt_config: cmt confing to resolve
1208
+ :param base_platform: base platform
1209
+ :param sw_map: software map
1210
+ :return: resolved cmt config or None if unavailable or valid
1211
+ """
1212
+ # return None if queue_name is unavailable
1213
+ if queue_name not in sw_map:
1214
+ return None
1215
+ # return None if cmt_config is valid
1216
+ if cmt_config in sw_map[queue_name]["cmtconfigs"]:
1217
+ return None
1218
+ # check if cmt_config matches with any of the queue's cmt_configs
1219
+ for tmp_cmt_config in sw_map[queue_name]["cmtconfigs"]:
1220
+ if re.search("^" + cmt_config + "$", tmp_cmt_config):
1221
+ if base_platform:
1222
+ # add base_platform if necessary
1223
+ tmp_cmt_config = tmp_cmt_config + "@" + base_platform
1224
+ return tmp_cmt_config
1225
+ # return None if cmt_config is unavailable
1226
+ return None
1227
+
1228
+
1229
+ def check_endpoints_with_blacklist(
1230
+ site_spec: SiteSpec.SiteSpec, scope_input: str, scope_output: str, sites_in_nucleus: list, remote_source_available: bool
1231
+ ) -> str | None:
1232
+ """
1233
+ Check if site's endpoints are in the blacklist
1234
+
1235
+ :param site_spec: site spec
1236
+ :param scope_input: input scope
1237
+ :param scope_output: output scope
1238
+ :param sites_in_nucleus: list of sites in nucleus
1239
+ :param remote_source_available: if remote source is available
1240
+
1241
+ :return: description of blacklisted reason or None
1242
+ """
1243
+ tmp_msg = None
1244
+ receive_input_over_wan = False
1245
+ read_input_over_lan = False
1246
+ write_output_over_lan = False
1247
+ send_output_over_wan = False
1248
+ tmp_site_name = site_spec.sitename
1249
+ if scope_input in site_spec.ddm_endpoints_input:
1250
+ for tmp_input_endpoint in site_spec.ddm_endpoints_input[scope_input].all.values():
1251
+ tmp_read_input_over_lan = tmp_input_endpoint["detailed_status"].get("read_lan")
1252
+ tmp_receive_input_over_wan = tmp_input_endpoint["detailed_status"].get("write_wan")
1253
+ # can read input from local
1254
+ if tmp_read_input_over_lan not in ["OFF", "TEST"]:
1255
+ read_input_over_lan = True
1256
+ # can receive input from remote to local
1257
+ if tmp_site_name not in sites_in_nucleus:
1258
+ # satellite sites
1259
+ if tmp_receive_input_over_wan not in ["OFF", "TEST"]:
1260
+ receive_input_over_wan = True
1261
+ else:
1262
+ # NA for nucleus sites
1263
+ receive_input_over_wan = True
1264
+ remote_source_available = True
1265
+ if scope_output in site_spec.ddm_endpoints_output:
1266
+ for tmp_output_endpoint in site_spec.ddm_endpoints_output[scope_output].all.values():
1267
+ tmp_write_output_over_lan = tmp_output_endpoint["detailed_status"].get("write_lan")
1268
+ tmp_send_output_over_wan = tmp_output_endpoint["detailed_status"].get("read_wan")
1269
+ # can write output to local
1270
+ if tmp_write_output_over_lan not in ["OFF", "TEST"]:
1271
+ write_output_over_lan = True
1272
+ # can send output from local to remote
1273
+ if tmp_site_name not in sites_in_nucleus:
1274
+ # satellite sites
1275
+ if tmp_send_output_over_wan not in ["OFF", "TEST"]:
1276
+ send_output_over_wan = True
1277
+ else:
1278
+ # NA for nucleus sites
1279
+ send_output_over_wan = True
1280
+ remote_source_available = True
1281
+ # take the status for logging
1282
+ if not read_input_over_lan:
1283
+ tmp_msg = f" skip site={tmp_site_name} since input endpoints cannot read over LAN, read_lan is not ON criteria=-read_lan_blacklist"
1284
+ elif not write_output_over_lan:
1285
+ tmp_msg = f" skip site={tmp_site_name} since output endpoints cannot write over LAN, write_lan is not ON criteria=-write_lan_blacklist"
1286
+ elif not receive_input_over_wan:
1287
+ tmp_msg = f" skip site={tmp_site_name} since input endpoints cannot receive files over WAN, write_wan is not ON criteria=-write_wan_blacklist"
1288
+ elif not send_output_over_wan:
1289
+ tmp_msg = f" skip site={tmp_site_name} since output endpoints cannot send out files over WAN, read_wan is not ON criteria=-read_wan_blacklist"
1290
+ elif not remote_source_available:
1291
+ tmp_msg = f" skip site={tmp_site_name} since source endpoints cannot transfer files over WAN and it is satellite criteria=-source_blacklist"
1292
+ return tmp_msg