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,1381 @@
1
+ """
2
+ Module to provide primitive methods for DDM
3
+
4
+ """
5
+
6
+ import datetime
7
+ import hashlib
8
+ import json
9
+ import re
10
+ import sys
11
+ import threading
12
+ import time
13
+ import traceback
14
+ from typing import Dict, List
15
+
16
+ from pandacommon.pandalogger.LogWrapper import LogWrapper
17
+ from pandacommon.pandalogger.PandaLogger import PandaLogger
18
+ from pandacommon.pandautils.PandaUtils import naive_utcnow
19
+ from rucio.client import Client as RucioClient
20
+ from rucio.common.exception import (
21
+ CannotAuthenticate,
22
+ DataIdentifierAlreadyExists,
23
+ DataIdentifierNotFound,
24
+ Duplicate,
25
+ DuplicateContent,
26
+ DuplicateRule,
27
+ FileAlreadyExists,
28
+ InvalidObject,
29
+ InvalidRSEExpression,
30
+ RSENotFound,
31
+ RuleNotFound,
32
+ UnsupportedOperation,
33
+ )
34
+
35
+ from pandaserver.srvcore import CoreUtils
36
+
37
+ # logger
38
+ _logger = PandaLogger().getLogger("ddm_rucio_api")
39
+
40
+
41
+ # rucio
42
+ class RucioAPI:
43
+ """
44
+ A class to interact with Rucio API
45
+ """
46
+
47
+ # constructor
48
+ def __init__(self):
49
+ """
50
+ Initialize RucioAPI instance
51
+ """
52
+ # how frequently update DN/token map
53
+ self.update_interval = datetime.timedelta(seconds=60 * 10)
54
+ # thread-safe client initialization
55
+ self._client_lock = threading.Lock()
56
+ self._rucio_client = None
57
+
58
+ # get thread-safe RucioClient with retry logic
59
+ def _get_rucio_client(self, max_retries: int = 3):
60
+ """
61
+ Get or create a Rucio client with thread-safe initialization and retry logic.
62
+ Handles transient auth failures by clearing cache and retrying with a fresh client.
63
+
64
+ Parameters:
65
+ max_retries (int): Maximum number of retry attempts on auth failure. Defaults to 3.
66
+
67
+ Returns:
68
+ RucioClient: A valid Rucio client instance
69
+
70
+ Raises:
71
+ CannotAuthenticate: If all retries are exhausted
72
+ """
73
+ for attempt in range(max_retries):
74
+ try:
75
+ with self._client_lock:
76
+ # try to reuse cached client on first attempt
77
+ if attempt == 0 and self._rucio_client is not None:
78
+ return self._rucio_client
79
+ # create fresh client (previous one may have stale token cache)
80
+ client = RucioClient()
81
+ self._rucio_client = client
82
+ return client
83
+ except CannotAuthenticate as e:
84
+ # clear cache on auth failure
85
+ with self._client_lock:
86
+ self._rucio_client = None
87
+ if attempt < max_retries - 1:
88
+ # exponential backoff: 0.1s, 0.2s, 0.4s
89
+ sleep_time = 0.1 * (2**attempt)
90
+ time.sleep(sleep_time)
91
+ continue
92
+ # re-raise on final attempt
93
+ raise
94
+ except Exception:
95
+ # for non-auth errors, clear cache and re-raise
96
+ with self._client_lock:
97
+ self._rucio_client = None
98
+ raise
99
+
100
+ # extract scope
101
+ def extract_scope(self, dataset_name: str, strip_slash: bool = False) -> tuple:
102
+ """
103
+ Extract scope from a given dataset name
104
+
105
+ Parameters:
106
+ dataset_name (str): Dataset name
107
+ strip_slash (bool): Wheter to strip trailing slash (about continer collection) in data_name
108
+
109
+ Returns:
110
+ tuple: A tuple containing scope and dataset name
111
+ """
112
+ if strip_slash:
113
+ if dataset_name.endswith("/"):
114
+ dataset_name = re.sub("/$", "", dataset_name)
115
+ if ":" in dataset_name:
116
+ return dataset_name.split(":")[:2]
117
+ scope = dataset_name.split(".")[0]
118
+ if dataset_name.startswith("user") or dataset_name.startswith("group"):
119
+ scope = ".".join(dataset_name.split(".")[0:2])
120
+ return scope, dataset_name
121
+
122
+ # register dataset
123
+ def register_dataset(
124
+ self,
125
+ dataset_name: str,
126
+ lfns: list = None,
127
+ guids: list = None,
128
+ sizes: list = None,
129
+ checksums: list = None,
130
+ lifetime: int = None,
131
+ scope: str = None,
132
+ metadata: dict = None,
133
+ ) -> dict:
134
+ """
135
+ Register a dataset in Rucio
136
+
137
+ Parameters:
138
+ dataset_name (str): Dataset name
139
+ lfns (list, optional): List of logical file names. Defaults to None.
140
+ guids (list, optional): List of GUIDs. Defaults to None.
141
+ sizes (list, optional): List of file sizes. Defaults to None.
142
+ checksums (list, optional): List of checksums. Defaults to None.
143
+ lifetime (int, optional): Lifetime of the dataset in seconds. Defaults to None.
144
+ scope (str, optional): Scope of the dataset. Defaults to None.
145
+ metadata (dict, optional): Metadata of the dataset. Defaults to None.
146
+
147
+ Returns:
148
+ dict: A dictionary containing duid, version and vuid of the dataset
149
+ """
150
+ if lfns is None:
151
+ lfns = []
152
+ if guids is None:
153
+ guids = []
154
+ if sizes is None:
155
+ sizes = []
156
+ if checksums is None:
157
+ checksums = []
158
+ preset_scope = scope
159
+ files = []
160
+ for lfn, guid, size, checksum in zip(lfns, guids, sizes, checksums):
161
+ if lfn.find(":") > -1:
162
+ file_scope, lfn = lfn.split(":")[0], lfn.split(":")[1]
163
+ else:
164
+ file_scope = scope
165
+ file = {"scope": file_scope, "name": lfn, "bytes": size, "meta": {"guid": guid}}
166
+ if checksum.startswith("md5:"):
167
+ file["md5"] = checksum[4:]
168
+ elif checksum.startswith("ad:"):
169
+ file["adler32"] = checksum[3:]
170
+ files.append(file)
171
+ # register dataset
172
+ client = self._get_rucio_client()
173
+ try:
174
+ scope, dataset_name = self.extract_scope(dataset_name)
175
+ if preset_scope is not None:
176
+ scope = preset_scope
177
+ client.add_dataset(scope=scope, name=dataset_name, meta=metadata)
178
+ if lifetime is not None:
179
+ client.set_metadata(scope, dataset_name, key="lifetime", value=lifetime * 86400)
180
+ except DataIdentifierAlreadyExists:
181
+ pass
182
+ # open dataset just in case
183
+ try:
184
+ client.set_status(scope, dataset_name, open=True)
185
+ except Exception:
186
+ pass
187
+ # add files
188
+ if len(files) > 0:
189
+ i_files = 0
190
+ n_files = 1000
191
+ while i_files < len(files):
192
+ tmp_files = files[i_files : i_files + n_files]
193
+ try:
194
+ client.add_files_to_dataset(scope=scope, name=dataset_name, files=tmp_files, rse=None)
195
+ except FileAlreadyExists:
196
+ for tmp_file in tmp_files:
197
+ try:
198
+ client.add_files_to_dataset(scope=scope, name=dataset_name, files=[tmp_file], rse=None)
199
+ except FileAlreadyExists:
200
+ pass
201
+ i_files += n_files
202
+ # Format is a 36-character string divided into five groups separated by hyphens. The groups have 8, 4, 4, 4, and 12 characters.
203
+ # After the formatting, the vuid string would look something like this: 12345678-1234-1234-1234-123456789012
204
+ vuid = hashlib.md5((scope + ":" + dataset_name).encode()).hexdigest()
205
+ vuid = f"{vuid[0:8]}-{vuid[8:12]}-{vuid[12:16]}-{vuid[16:20]}-{vuid[20:32]}"
206
+ duid = vuid
207
+ return {"duid": duid, "version": 1, "vuid": vuid}
208
+
209
+ def register_dataset_location(
210
+ self,
211
+ dataset_name: str,
212
+ rses: list,
213
+ lifetime: int = None,
214
+ owner: str = None,
215
+ activity: str = None,
216
+ scope: str = None,
217
+ grouping: str = "DATASET",
218
+ notify: str = "N",
219
+ ) -> bool:
220
+ """
221
+ Register a dataset location in Rucio
222
+
223
+ Parameters:
224
+ dataset_name (str): Dataset name
225
+ rses (list): List of RSEs
226
+ lifetime (int, optional): Lifetime of the dataset in seconds. Defaults to None.
227
+ owner (str, optional): Owner of the dataset. Defaults to None.
228
+ activity (str, optional): Activity associated with the dataset. Defaults to None.
229
+ scope (str, optional): Scope of the dataset. Defaults to None.
230
+ grouping (str, optional): Grouping of the dataset. Defaults to "DATASET".
231
+ notify (str, optional): Notification option. Defaults to "N".
232
+
233
+ Returns:
234
+ bool: True if the operation is successful, False otherwise
235
+ """
236
+ grouping = "DATASET" if grouping is None else grouping
237
+ preset_scope = scope
238
+ lifetime = lifetime * 24 * 60 * 60 if lifetime else None
239
+ scope, dataset_name = self.extract_scope(dataset_name)
240
+ scope = preset_scope if preset_scope else scope
241
+ did = {"scope": scope, "name": dataset_name}
242
+ dids = [did]
243
+ # make location
244
+ rses.sort()
245
+ location = "|".join(rses)
246
+ # check if a replication rule already exists
247
+ client = self._get_rucio_client()
248
+ # owner
249
+ owner = client.account if owner is None else owner
250
+ for rule in client.list_did_rules(scope=scope, name=dataset_name):
251
+ if (rule["rse_expression"] == location) and (rule["account"] == owner):
252
+ return True
253
+ try:
254
+ client.add_replication_rule(
255
+ dids=dids,
256
+ copies=1,
257
+ rse_expression=location,
258
+ weight=None,
259
+ lifetime=lifetime,
260
+ grouping=grouping,
261
+ account=owner,
262
+ locked=False,
263
+ activity=activity,
264
+ notify=notify,
265
+ ignore_availability=True,
266
+ )
267
+ except (Duplicate, DuplicateRule):
268
+ pass
269
+ return True
270
+
271
+ # get user
272
+ def get_user(self, client, distinguished_name: str) -> str:
273
+ """
274
+ This method retrieves the account name associated with a given distinguished name (dn) from the Rucio client.
275
+ If no account is found, it returns the default account of the Rucio client.
276
+
277
+ Parameters:
278
+ client (RucioClient): The Rucio client instance to interact with the Rucio server.
279
+ dn (str): The distinguished name (dn) for which the associated account is to be retrieved.
280
+
281
+ Returns:
282
+ str: The account name associated with the given dn. If no account is found, it returns the default account of the Rucio client.
283
+ """
284
+ tmp_list = list(client.list_accounts("user", distinguished_name))
285
+ if tmp_list:
286
+ owner = tmp_list[0]["account"]
287
+ return owner
288
+ return client.account
289
+
290
+ # register dataset subscription
291
+ def register_dataset_subscription(
292
+ self, dataset_name: str, rses: list, lifetime: int = None, owner: str = None, activity: str = None, distinguished_name: str = None, comment: str = None
293
+ ) -> bool:
294
+ """
295
+ Register a dataset subscription in Rucio.
296
+
297
+ This method registers a dataset subscription in Rucio by creating a replication rule for the dataset.
298
+ If a replication rule already exists for the dataset, the method simply returns True.
299
+
300
+ Parameters:
301
+ dataset_name (str): The name of the dataset.
302
+ rses (list): A list of RSEs where the dataset should be replicated.
303
+ lifetime (int, optional): The lifetime of the replication rule in seconds. Defaults to None.
304
+ owner (str, optional): The owner of the replication rule. Defaults to None.
305
+ activity (str, optional): The activity associated with the replication rule. Defaults to None.
306
+ distinguished_name (str, optional): The distinguished name of the user. Defaults to None.
307
+ comment (str, optional): A comment to be associated with the replication rule. Defaults to None.
308
+
309
+ Returns:
310
+ bool: True if the operation is successful, False otherwise.
311
+ """
312
+ lifetime = lifetime * 24 * 60 * 60 if lifetime else lifetime
313
+ scope, dataset_name = self.extract_scope(dataset_name)
314
+ did = {"scope": scope, "name": dataset_name}
315
+ dids = [did]
316
+ # make location
317
+ rses.sort()
318
+ location = "|".join(rses)
319
+ # check if a replication rule already exists
320
+ client = self._get_rucio_client()
321
+ # owner
322
+ if owner is None:
323
+ if distinguished_name is not None:
324
+ owner = self.get_user(client, distinguished_name)
325
+ else:
326
+ owner = client.account
327
+ for rule in client.list_did_rules(scope=scope, name=dataset_name):
328
+ if (rule["rse_expression"] == location) and (rule["account"] == owner):
329
+ return True
330
+ try:
331
+ client.add_replication_rule(
332
+ dids=dids,
333
+ copies=1,
334
+ rse_expression=location,
335
+ weight=None,
336
+ lifetime=lifetime,
337
+ grouping="DATASET",
338
+ account=owner,
339
+ locked=False,
340
+ activity=activity,
341
+ notify="C",
342
+ ignore_availability=True,
343
+ comment=comment,
344
+ )
345
+ except (Duplicate, DuplicateRule):
346
+ pass
347
+ return True
348
+
349
+ # convert file attribute
350
+ def convert_file_attributes(self, tmp_file: dict, scope: str) -> dict:
351
+ """
352
+ Convert file attribute to a dictionary
353
+
354
+ Parameters:
355
+ tmp_file (dict): File attribute
356
+ scope (str): Scope of the file
357
+
358
+ Returns:
359
+ dict: A dictionary containing file attributes
360
+ """
361
+ lfn = tmp_file.get("name", tmp_file.get("lfn"))
362
+ file_scope, lfn = lfn.split(":") if ":" in lfn else (scope, lfn)
363
+ # set metadata
364
+ meta_keys = ["guid", "events", "lumiblocknr", "panda_id", "campaign", "task_id"]
365
+ meta = {key: tmp_file[key] for key in meta_keys if key in tmp_file}
366
+ file_size = tmp_file.get("bytes", tmp_file.get("size"))
367
+ file = {"scope": file_scope, "name": lfn, "bytes": file_size, "meta": meta}
368
+ if "checksum" in tmp_file:
369
+ checksum = tmp_file["checksum"]
370
+ if checksum.startswith("md5:"):
371
+ file["md5"] = checksum[4:]
372
+ elif checksum.startswith("ad:"):
373
+ file["adler32"] = checksum[3:]
374
+ if "surl" in tmp_file:
375
+ file["pfn"] = tmp_file["surl"]
376
+ return file
377
+
378
+ # register files in dataset
379
+ def register_files_in_dataset(self, id_map: dict, files_without_rses: list = None) -> bool:
380
+ """
381
+ Register files in a dataset
382
+
383
+ Parameters:
384
+ id_map (dict): A dictionary containing dataset information. Maps RSEs to datasets and files.
385
+ files_without_rses (list, optional): List of files without RSEs. Defaults to None.
386
+
387
+ Returns:
388
+ bool: True if the operation is successful, False otherwise
389
+ """
390
+ # loop over all rse
391
+ attachment_list = []
392
+ for rse in id_map:
393
+ tmp_map = id_map[rse]
394
+ # loop over all datasets
395
+ for dataset_name in tmp_map:
396
+ file_list = tmp_map[dataset_name]
397
+ # extract scope from dataset
398
+ scope, given_dataset_name = self.extract_scope(dataset_name)
399
+ files_with_rse = []
400
+ files_without_rse = []
401
+ for tmp_file in file_list:
402
+ # convert file attribute
403
+ file = self.convert_file_attributes(tmp_file, scope)
404
+ # append files
405
+ if rse is not None and (files_without_rses is None or file["name"] not in files_without_rses):
406
+ files_with_rse.append(file)
407
+ else:
408
+ if "pfn" in file:
409
+ del file["pfn"]
410
+ files_without_rse.append(file)
411
+ # add attachment
412
+ if len(files_with_rse) > 0:
413
+ n_files = 100
414
+ i_files = 0
415
+ while i_files < len(files_with_rse):
416
+ attachment = {
417
+ "scope": scope,
418
+ "name": given_dataset_name,
419
+ "dids": files_with_rse[i_files : i_files + n_files],
420
+ "rse": rse,
421
+ }
422
+ attachment_list.append(attachment)
423
+ i_files += n_files
424
+ if len(files_without_rse) > 0:
425
+ n_files = 100
426
+ i_files = 0
427
+ while i_files < len(files_without_rse):
428
+ attachment = {
429
+ "scope": scope,
430
+ "name": given_dataset_name,
431
+ "dids": files_without_rse[i_files : i_files + n_files],
432
+ }
433
+ attachment_list.append(attachment)
434
+ i_files += n_files
435
+ # add files
436
+ client = self._get_rucio_client()
437
+ client.add_files_to_datasets(attachment_list, ignore_duplicate=True)
438
+ return True
439
+
440
+ # register zip files
441
+ def register_zip_files(self, zip_map: dict) -> None:
442
+ """
443
+ Register zip files in Rucio.
444
+
445
+ This method takes a map of zip file names to their attributes and registers the zip files in Rucio.
446
+ It first converts the zip file attributes to a dictionary format that Rucio can understand.
447
+ Then, it loops over all the files contained in the zip file, converts their attributes to the Rucio dictionary format, and appends them to a list.
448
+ Finally, it registers the zip file and its contained files in Rucio.
449
+
450
+ Parameters:
451
+ zip_map (dict): A dictionary mapping zip file names to their attributes. The structure is {zipFileName: zipFileAttr, ...}.
452
+
453
+ Returns:
454
+ None
455
+ """
456
+ # no zip files
457
+ if len(zip_map) == 0:
458
+ return
459
+ client = self._get_rucio_client()
460
+ # loop over all zip files
461
+ for zip_file_name in zip_map:
462
+ zip_file_attr = zip_map[zip_file_name]
463
+ # convert file attribute
464
+ zip_file = self.convert_file_attributes(zip_file_attr, zip_file_attr["scope"])
465
+ # loop over all contents
466
+ files = []
467
+ for con_file_attr in zip_file_attr["files"]:
468
+ # get scope
469
+ scope, _ = self.extract_scope(con_file_attr["ds"])
470
+ # convert file attribute
471
+ con_file = self.convert_file_attributes(con_file_attr, scope)
472
+ con_file["type"] = "FILE"
473
+ if "pfn" in con_file:
474
+ del con_file["pfn"]
475
+ # append files
476
+ files.append(con_file)
477
+ # register zip file
478
+ for rse in zip_file_attr["rse"]:
479
+ client.add_replicas(rse=rse, files=[zip_file])
480
+ # add files
481
+ n_files = 100
482
+ i_files = 0
483
+ while i_files < len(files):
484
+ client.add_files_to_archive(
485
+ scope=zip_file["scope"],
486
+ name=zip_file["name"],
487
+ files=files[i_files : i_files + n_files],
488
+ )
489
+ i_files += n_files
490
+
491
+ # list datasets
492
+ def list_datasets(self, dataset_name: str, old: bool = False):
493
+ """
494
+ List datasets in Rucio.
495
+
496
+ This method lists datasets in Rucio by extracting the scope from the dataset name and getting the DIDs (Data Identifiers).
497
+ It generates a unique identifier (vuid) for each DID and stores it in a dictionary along with the dataset name.
498
+ If an exception occurs during the process, it returns None and the exception message.
499
+
500
+ Parameters:
501
+ dataset_name (str): The name of the dataset.
502
+ old (bool, optional): A flag to indicate if the dataset is old. Defaults to False.
503
+
504
+ Returns:
505
+ Tuple[Union[dict, None], str]: A tuple containing a dictionary of datasets and their unique identifiers, and a string message.
506
+ If an exception occurs, the dictionary is None and the string contains the exception message.
507
+ """
508
+ result = {}
509
+ # extract scope from dataset
510
+ scope, given_dataset_name = self.extract_scope(dataset_name)
511
+ if given_dataset_name.endswith("/"):
512
+ given_dataset_name = given_dataset_name[:-1]
513
+ collection = "container"
514
+ else:
515
+ collection = "dataset"
516
+ filters = {"name": given_dataset_name}
517
+ try:
518
+ # get dids
519
+ client = self._get_rucio_client()
520
+ for name in client.list_dids(scope, filters, collection):
521
+ vuid = hashlib.md5((scope + ":" + name).encode()).hexdigest()
522
+ # Format is a 36-character string divided into five groups separated by hyphens. The groups have 8, 4, 4, 4, and 12 characters.
523
+ # After the formatting, the vuid string would look something like this: 12345678-1234-1234-1234-123456789012
524
+ vuid = f"{vuid[0:8]}-{vuid[8:12]}-{vuid[12:16]}-{vuid[16:20]}-{vuid[20:32]}"
525
+ duid = vuid
526
+ # add /
527
+ if dataset_name.endswith("/") and not name.endswith("/"):
528
+ name += "/"
529
+ if old or ":" not in dataset_name:
530
+ key_name = name
531
+ else:
532
+ key_name = str(f"{scope}:{name}")
533
+ if key_name not in result:
534
+ result[key_name] = {"duid": duid, "vuids": [vuid]}
535
+ return result, ""
536
+ except Exception as error:
537
+ return None, f"{str(error)} {traceback.format_exc()}"
538
+
539
+ # list datasets in container
540
+ def list_datasets_in_container(self, container_name: str):
541
+ """
542
+ List datasets in a Rucio container.
543
+
544
+ This method lists datasets in a Rucio container by extracting the scope from the container name and getting the DIDs (Data Identifiers).
545
+ It generates a unique identifier (vuid) for each DID and stores it in a list.
546
+ If an exception occurs during the process, it returns None and the exception message.
547
+
548
+ Parameters:
549
+ container_name (str): The name of the container.
550
+
551
+ Returns:
552
+ Tuple[Union[None, List[str]], str]: A tuple containing a list of datasets and a string message.
553
+ If an exception occurs, the list is None and the string contains the exception message.
554
+ """
555
+ result = []
556
+ # extract scope from dataset
557
+ scope, container_name = self.extract_scope(container_name)
558
+ if container_name.endswith("/"):
559
+ container_name = container_name[:-1]
560
+ try:
561
+ # get dids
562
+ client = self._get_rucio_client()
563
+ for content in client.list_content(scope, container_name):
564
+ if content["type"] == "DATASET":
565
+ result.append(str(f"{content['scope']}:{content['name']}"))
566
+ return result, ""
567
+ except Exception:
568
+ err_type, err_value = sys.exc_info()[:2]
569
+ return None, f"{err_type} {err_value}"
570
+
571
+ # list dataset replicas
572
+ def list_dataset_replicas(self, dataset_name: str):
573
+ """
574
+ List dataset replicas in Rucio.
575
+
576
+ This method lists dataset replicas in Rucio by extracting the scope from the dataset name and getting the replicas.
577
+ It stores the replicas in a dictionary and returns it along with a status code.
578
+ If an exception occurs during the process, it returns the error type and value.
579
+
580
+ Parameters:
581
+ dataset_name (str): The name of the dataset.
582
+
583
+ Returns:
584
+ Tuple[int, Union[str, dict]]: A tuple containing a status code and a dictionary of dataset replicas or an error message.
585
+ If an exception occurs, the dictionary is None and the string contains the error message.
586
+ """
587
+ return_map = {}
588
+ # extract scope from dataset
589
+ scope, dataset_name = self.extract_scope(dataset_name)
590
+ try:
591
+ # get replicas
592
+ client = self._get_rucio_client()
593
+ replica_iterator = client.list_dataset_replicas(scope, dataset_name)
594
+ for item in replica_iterator:
595
+ rse = item["rse"]
596
+ return_map[rse] = [
597
+ {
598
+ "total": item["length"],
599
+ "found": item["available_length"],
600
+ "immutable": 1,
601
+ }
602
+ ]
603
+ return 0, return_map
604
+ except Exception:
605
+ err_type, err_value = sys.exc_info()[:2]
606
+ return 1, f"{err_type} {err_value}"
607
+
608
+ # get metadata
609
+ def get_metadata(self, dataset_name: str, scope: str = None):
610
+ """
611
+ Get metadata of a dataset in Rucio.
612
+
613
+ This method retrieves the metadata of a dataset in Rucio by extracting the scope from the dataset name and getting the metadata.
614
+ If an exception occurs during the process, it returns False and the exception message.
615
+
616
+ Parameters:
617
+ dataset_name (str): The name of the dataset.
618
+ scope (str, optional): The scope of the dataset. Defaults to None.
619
+
620
+ Returns:
621
+ Tuple[bool, Union[None, dict]]: A tuple containing a boolean indicating the success of the operation and a dictionary of metadata or an error message.
622
+ If an exception occurs, the boolean is False and the string contains the error message.
623
+ """
624
+ # register dataset
625
+ client = self._get_rucio_client()
626
+ try:
627
+ scope, dataset_name = self.extract_scope(dataset_name)
628
+ return True, client.get_metadata(scope, dataset_name)
629
+ except DataIdentifierNotFound:
630
+ return True, None
631
+ except Exception:
632
+ err_type, err_value = sys.exc_info()[:2]
633
+ return False, f"{err_type} {err_value}"
634
+
635
+ # delete dataset
636
+ def erase_dataset(self, dataset_name: str, scope: str = None, grace_period: int = None):
637
+ """
638
+ Delete a dataset in Rucio.
639
+
640
+ This method deletes a dataset in Rucio by extracting the scope from the dataset name and calling the erase method.
641
+ If an exception occurs during the process, it returns False and the exception message.
642
+
643
+ Parameters:
644
+ dataset_name (str): The name of the dataset.
645
+ scope (str, optional): The scope of the dataset. Defaults to None.
646
+ grace_period (int, optional): The grace period before deletion. Defaults to None.
647
+
648
+ Returns:
649
+ Tuple[bool, str]: A tuple containing a boolean indicating the success of the operation and a string message.
650
+ If an exception occurs, the boolean is False and the string contains the error message.
651
+ """
652
+ preset_scope = scope
653
+ # register dataset
654
+ client = self._get_rucio_client()
655
+ try:
656
+ scope, dataset_name = self.extract_scope(dataset_name)
657
+ if preset_scope is not None:
658
+ scope = preset_scope
659
+ if grace_period is not None:
660
+ value = grace_period * 60 * 60
661
+ else:
662
+ value = 0.0001
663
+ client.set_metadata(scope=scope, name=dataset_name, key="lifetime", value=value)
664
+ except DataIdentifierNotFound:
665
+ pass
666
+ except Exception as error:
667
+ return False, str(error)
668
+ return True, ""
669
+
670
+ # close dataset
671
+ def close_dataset(self, dataset_name: str) -> bool:
672
+ """
673
+ Close a dataset in Rucio.
674
+
675
+ This method closes a dataset in Rucio by extracting the scope from the dataset name and setting the status of the dataset to closed.
676
+ If an exception occurs during the process, it ignores the exception and returns True.
677
+
678
+ Parameters:
679
+ dataset_name (str): The name of the dataset.
680
+
681
+ Returns:
682
+ bool: True if the operation is successful, False otherwise.
683
+ """
684
+ # register dataset
685
+ client = self._get_rucio_client()
686
+ try:
687
+ scope, dataset_name = self.extract_scope(dataset_name)
688
+ client.set_status(scope, dataset_name, open=False)
689
+ except (UnsupportedOperation, DataIdentifierNotFound):
690
+ pass
691
+ return True
692
+
693
+ # list file replicas
694
+ def list_file_replicas(self, scopes: List[str], lfns: List[str], rses: List[str] = None):
695
+ """
696
+ List file replicas in Rucio.
697
+
698
+ This method lists file replicas in Rucio by creating a list of dictionaries containing the scope and name of each file.
699
+ It then retrieves the replicas for these files from Rucio and stores them in a dictionary.
700
+ If an exception occurs during the process, it returns False and the exception message.
701
+
702
+ Parameters:
703
+ scopes (List[str]): A list of scopes for the files.
704
+ lfns (List[str]): A list of Logical File Names (LFNs) for the files.
705
+ rses (List[str], optional): A list of Rucio Storage Elements (RSEs) where the files should be replicated. Defaults to None.
706
+
707
+ Returns:
708
+ Tuple[bool, Union[str, Dict[str, List[str]]]]: A tuple containing a boolean indicating the success of the operation and a dictionary of file replicas or an error message.
709
+ If an exception occurs, the boolean is False and the string contains the error message.
710
+ """
711
+ try:
712
+ client = self._get_rucio_client()
713
+ dids = []
714
+ i_guid = 0
715
+ batch_size = 1000
716
+ ret_val = {}
717
+ for scope, lfn in zip(scopes, lfns):
718
+ i_guid += 1
719
+ dids.append({"scope": scope, "name": lfn})
720
+ if len(dids) % batch_size == 0 or i_guid == len(lfns):
721
+ for tmp_dict in client.list_replicas(dids):
722
+ tmp_lfn = str(tmp_dict["name"])
723
+ tmp_rses = list(tmp_dict["rses"])
724
+ # RSE selection
725
+ if rses is not None:
726
+ tmp_rses = [tmp_rse for tmp_rse in tmp_rses if tmp_rse in rses]
727
+ if len(tmp_rses) > 0:
728
+ ret_val[tmp_lfn] = tmp_rses
729
+ dids = []
730
+ return True, ret_val
731
+ except Exception:
732
+ err_type, err_value = sys.exc_info()[:2]
733
+ return False, f"{err_type} {err_value}"
734
+
735
+ # get zip files
736
+ def get_zip_files(self, dids: List[str], rses: List[str]):
737
+ """
738
+ Get zip files from Rucio.
739
+
740
+ This method retrieves zip files from Rucio by creating a list of dictionaries containing the scope and name of each file.
741
+ It then retrieves the replicas for these files from Rucio and stores them in a dictionary.
742
+ If an exception occurs during the process, it returns False and the exception message.
743
+
744
+ Parameters:
745
+ dids (List[str]): A list of Data Identifiers (DIDs) for which to retrieve the associated zip files.
746
+ rses (List[str]): A list of Rucio Storage Elements (RSEs) where the files should be replicated.
747
+
748
+ Returns:
749
+ Tuple[bool, Union[str, Dict[str, Dict[str, Any]]]]: A tuple containing a boolean indicating the success of the operation and a dictionary of zip files or an error message.
750
+ If an exception occurs, the boolean is False and the string contains the error message.
751
+ """
752
+ try:
753
+ client = self._get_rucio_client()
754
+ data = []
755
+ i_guid = 0
756
+ batch_size = 1000
757
+ ret_val = {}
758
+ for did in dids:
759
+ i_guid += 1
760
+ scope, lfn = did.split(":")
761
+ data.append({"scope": scope, "name": lfn})
762
+ if len(data) % batch_size == 0 or i_guid == len(dids):
763
+ for tmp_dict in client.list_replicas(data):
764
+ tmp_scope = str(tmp_dict["scope"])
765
+ tmp_lfn = str(tmp_dict["name"])
766
+ tmp_did = f"{tmp_scope}:{tmp_lfn}"
767
+ # RSE selection
768
+ for pfn in tmp_dict["pfns"]:
769
+ pfn_data = tmp_dict["pfns"][pfn]
770
+ if (rses is None or pfn_data["rse"] in rses) and pfn_data["domain"] == "zip":
771
+ zip_file_name = pfn.split("/")[-1]
772
+ zip_file_name = re.sub("\?.+$", "", zip_file_name)
773
+ ret_val[tmp_did] = client.get_metadata(tmp_scope, zip_file_name)
774
+ break
775
+ data = []
776
+ return True, ret_val
777
+ except Exception:
778
+ err_type, err_value = sys.exc_info()[:2]
779
+ return False, f"{err_type} {err_value}"
780
+
781
+ # list files in dataset
782
+ def list_files_in_dataset(self, dataset_name: str, long: bool = False, file_list: List[str] = None):
783
+ """
784
+ List files in a Rucio dataset.
785
+
786
+ This method lists files in a Rucio dataset by extracting the scope from the dataset name and getting the files.
787
+ It stores the files in a dictionary and returns it along with a status code.
788
+ If an exception occurs during the process, it returns the error type and value.
789
+
790
+ Parameters:
791
+ dataset_name (str): The name of the dataset.
792
+ long (bool, optional): A flag to indicate if the file list should be long. Defaults to False.
793
+ file_list (List[str], optional): A list of files to be listed. Defaults to None.
794
+
795
+ Returns:
796
+ Tuple[Dict[str, Dict[str, Any]], Optional[str]]: A tuple containing a dictionary of files and a string message.
797
+ If an exception occurs, the dictionary is None and the string contains the error message.
798
+ """
799
+ # extract scope from dataset
800
+ scope, dataset_name = self.extract_scope(dataset_name)
801
+ if dataset_name.endswith("/"):
802
+ dataset_name = dataset_name[:-1]
803
+ client = self._get_rucio_client()
804
+ return_dict = {}
805
+ for file_info in client.list_files(scope, dataset_name, long=long):
806
+ tmp_lfn = str(file_info["name"])
807
+ if file_list is not None:
808
+ gen_lfn = re.sub("\.\d+$", "", tmp_lfn)
809
+ if tmp_lfn not in file_list and gen_lfn not in file_list:
810
+ continue
811
+ rucio_attrs = {}
812
+ rucio_attrs["chksum"] = "ad:" + str(file_info["adler32"])
813
+ rucio_attrs["md5sum"] = rucio_attrs["chksum"]
814
+ rucio_attrs["checksum"] = rucio_attrs["chksum"]
815
+ rucio_attrs["fsize"] = file_info["bytes"]
816
+ rucio_attrs["filesize"] = rucio_attrs["fsize"]
817
+ rucio_attrs["scope"] = str(file_info["scope"])
818
+ rucio_attrs["events"] = str(file_info["events"])
819
+ if long:
820
+ rucio_attrs["lumiblocknr"] = str(file_info["lumiblocknr"])
821
+ guid = str(f"{file_info['guid'][0:8]}-{file_info['guid'][8:12]}-{file_info['guid'][12:16]}-{file_info['guid'][16:20]}-{file_info['guid'][20:32]}")
822
+ rucio_attrs["guid"] = guid
823
+ return_dict[tmp_lfn] = rucio_attrs
824
+ return (return_dict, None)
825
+
826
+ # get # of files in dataset
827
+ def get_number_of_files(self, dataset_name: str, preset_scope: str = None):
828
+ """
829
+ Get the number of files in a Rucio dataset.
830
+
831
+ This method retrieves the number of files in a Rucio dataset by extracting the scope from the dataset name and getting the files.
832
+ If an exception occurs during the process, it returns False and the exception message.
833
+
834
+ Parameters:
835
+ dataset_name (str): The name of the dataset.
836
+ preset_scope (str, optional): The scope of the dataset. Defaults to None.
837
+
838
+ Returns:
839
+ Tuple[bool, Union[int, str]]: A tuple containing a boolean indicating the success of the operation and the number of files or an error message.
840
+ If an exception occurs, the boolean is False and the string contains the error message.
841
+ """
842
+ # make logger
843
+ method_name = "get_number_of_files"
844
+ method_name = f"{method_name} dataset_name={dataset_name}"
845
+ tmp_log = LogWrapper(_logger, method_name)
846
+ tmp_log.debug("start")
847
+ # extract scope from dataset
848
+ scope, dataset_name = self.extract_scope(dataset_name)
849
+ if preset_scope is not None:
850
+ scope = preset_scope
851
+ try:
852
+ client = self._get_rucio_client()
853
+ n_files = 0
854
+ for _ in client.list_files(scope, dataset_name):
855
+ n_files += 1
856
+ return True, n_files
857
+ except DataIdentifierNotFound:
858
+ tmp_log.debug("dataset not found")
859
+ return None, "dataset not found"
860
+ except Exception:
861
+ err_type, err_value = sys.exc_info()[:2]
862
+ err_msg = f"{err_type.__name__} {err_value}"
863
+ tmp_log.error(f"got error ; {traceback.format_exc()}")
864
+ return False, err_msg
865
+
866
+ # list datasets with GUIDs
867
+ def list_datasets_by_guids(self, guids: List[str]) -> Dict[str, List[str]]:
868
+ """
869
+ List datasets in Rucio by GUIDs.
870
+
871
+ This method lists datasets in Rucio by their GUIDs. It retrieves the datasets associated with each GUID
872
+ and stores them in a dictionary where the keys are the GUIDs and the values are lists of datasets.
873
+
874
+ Parameters:
875
+ guids (List[str]): A list of GUIDs for which to retrieve the associated datasets.
876
+
877
+ Returns:
878
+ Dict[str, List[str]]: A dictionary mapping each GUID to a list of its associated datasets.
879
+ """
880
+ client = self._get_rucio_client()
881
+ result = {}
882
+ for guid in guids:
883
+ datasets = [str(f"{i['scope']}:{i['name']}") for i in client.get_dataset_by_guid(guid)]
884
+ result[guid] = datasets
885
+ return result
886
+
887
+ # register container
888
+ def register_container(self, container_name: str, datasets: List[str] = None, preset_scope: str = None) -> bool:
889
+ """
890
+ Register a container in Rucio.
891
+
892
+ This method registers a container in Rucio by extracting the scope from the container name and adding the container.
893
+ If the container already exists, it ignores the exception and continues.
894
+ If a list of datasets is provided, it adds these datasets to the container.
895
+
896
+ Parameters:
897
+ container_name (str): The name of the container.
898
+ datasets (List[str], optional): A list of datasets to be added to the container. Defaults to None.
899
+ preset_scope (str, optional): The scope of the container. Defaults to None.
900
+
901
+ Returns:
902
+ bool: True if the operation is successful, False otherwise.
903
+ """
904
+ if container_name.endswith("/"):
905
+ container_name = container_name[:-1]
906
+ # register container
907
+ client = self._get_rucio_client()
908
+ try:
909
+ scope, dataset_name = self.extract_scope(container_name)
910
+ if preset_scope is not None:
911
+ scope = preset_scope
912
+ client.add_container(scope=scope, name=container_name)
913
+ except DataIdentifierAlreadyExists:
914
+ pass
915
+ # add files
916
+ if datasets is not None and len(datasets) > 0:
917
+ try:
918
+ dataset_names = []
919
+ for dataset in datasets:
920
+ dataset_scope, dataset_name = self.extract_scope(dataset)
921
+ if dataset_scope:
922
+ dataset_name = {"scope": dataset_scope, "name": dataset_name}
923
+ else:
924
+ dataset_name = {"scope": scope, "name": dataset}
925
+ dataset_names.append(dataset_name)
926
+ client.add_datasets_to_container(scope=scope, name=container_name, dsns=dataset_names)
927
+ except DuplicateContent:
928
+ for dataset in dataset_names:
929
+ try:
930
+ client.add_datasets_to_container(scope=scope, name=container_name, dsns=[dataset])
931
+ except DuplicateContent:
932
+ pass
933
+ return True
934
+
935
+ # finger
936
+ def finger(self, distinguished_name: str):
937
+ """
938
+ Retrieve user information from Rucio based on the distinguished name (dn).
939
+
940
+ This method retrieves user information from Rucio by using the distinguished name (dn) to identify the user.
941
+ It first checks if the user is identified by an X509 certificate or an OIDC token.
942
+ It then iterates over the list of accounts in Rucio, looking for a match with the user's distinguished name.
943
+ If a match is found, it retrieves the user's nickname and email and stores them in a dictionary.
944
+ If no match is found, it attempts to retrieve the account information directly using the distinguished name.
945
+ If an exception occurs during the process, it returns the error message.
946
+
947
+ Parameters:
948
+ distinguished_name (str): The distinguished name of the user.
949
+
950
+ Returns:
951
+ Tuple[bool, Union[dict, str]]: A tuple containing a boolean indicating the success of the operation and a dictionary of user information or an error message.
952
+ If an exception occurs, the boolean is False and the string contains the error message.
953
+ """
954
+ try:
955
+ return_value = False
956
+ # get rucio API
957
+ client = self._get_rucio_client()
958
+ user_info = None
959
+ return_value = False
960
+ x509_user_name = CoreUtils.get_bare_dn(distinguished_name)
961
+ oidc_user_name = CoreUtils.get_id_from_dn(distinguished_name)
962
+ if oidc_user_name == x509_user_name:
963
+ oidc_user_name = None
964
+ else:
965
+ x509_user_name = None
966
+ for account_type in ["USER", "GROUP"]:
967
+ if x509_user_name is not None:
968
+ user_names = CoreUtils.get_distinguished_name_list(x509_user_name)
969
+ for user_name in user_names:
970
+ for i in client.list_accounts(account_type=account_type, identity=user_name):
971
+ user_info = {"nickname": i["account"], "email": i["email"]}
972
+ break
973
+ if user_info is not None:
974
+ break
975
+ else:
976
+ user_name = oidc_user_name
977
+ try:
978
+ if user_info is None:
979
+ account = client.get_account(user_name)
980
+ user_info = {"nickname": account["account"], "email": account["email"]}
981
+ except Exception:
982
+ pass
983
+ if user_info is not None:
984
+ return_value = True
985
+ break
986
+ except Exception as error:
987
+ error_message = f"{str(error)}"
988
+ user_info = error_message
989
+ return return_value, user_info
990
+
991
+ #####################################
992
+ # Migrated from JEDI AtlasDDMClient #
993
+ #####################################
994
+
995
+ # get DID string as scope:name
996
+ def get_did_str(self, raw_name: str) -> str:
997
+ scope, name = self.extract_scope(raw_name, strip_slash=True)
998
+ return f"{scope}:{name}"
999
+
1000
+ # wrapper for list_content
1001
+ def wp_list_content(self, client, scope, dsn):
1002
+ if dsn.endswith("/"):
1003
+ dsn = dsn[:-1]
1004
+ ret_list = []
1005
+ # get contents
1006
+ for data in client.list_content(scope, dsn):
1007
+ if data["type"] == "CONTAINER":
1008
+ ret_list += self.wp_list_content(client, data["scope"], data["name"])
1009
+ elif data["type"] == "DATASET":
1010
+ ret_list.append(f"{data['scope']}:{data['name']}")
1011
+ else:
1012
+ pass
1013
+ return ret_list
1014
+
1015
+ # convert output of rucio list_dataset_replicas
1016
+ def convert_list_dataset_replicas(self, dataset_name, use_file_lookup=False, use_vp=False, skip_incomplete_element=False):
1017
+ retMap = {}
1018
+ # get rucio API
1019
+ client = self._get_rucio_client()
1020
+ # get scope and name
1021
+ scope, dsn = self.extract_scope(dataset_name, strip_slash=True)
1022
+ # get replicas
1023
+ itr = client.list_dataset_replicas(scope, dsn, deep=use_file_lookup)
1024
+ items = []
1025
+ for item in itr:
1026
+ if "vp" not in item:
1027
+ item["vp"] = False
1028
+ items.append(item)
1029
+ # deep lookup if shallow gave nothing
1030
+ if items == [] and not use_file_lookup:
1031
+ itr = client.list_dataset_replicas(scope, dsn, deep=True)
1032
+ for item in itr:
1033
+ if "vp" not in item:
1034
+ item["vp"] = False
1035
+ items.append(item)
1036
+ # VP
1037
+ if use_vp:
1038
+ itr = client.list_dataset_replicas_vp(scope, dsn)
1039
+ for item in itr:
1040
+ if item["vp"]:
1041
+ # add dummy
1042
+ if "length" not in item:
1043
+ item["length"] = 1
1044
+ if "available_length" not in item:
1045
+ item["available_length"] = 1
1046
+ if "bytes" not in item:
1047
+ item["bytes"] = 1
1048
+ if "available_bytes" not in item:
1049
+ item["available_bytes"] = 1
1050
+ if "site" in item and "rse" not in item:
1051
+ item["rse"] = item["site"]
1052
+ items.append(item)
1053
+ # loop over all RSEs
1054
+ for item in items:
1055
+ rse = item["rse"]
1056
+ if skip_incomplete_element and (not item["available_length"] or item["length"] != item["available_length"]):
1057
+ continue
1058
+ retMap[rse] = [
1059
+ {
1060
+ "total": item["length"],
1061
+ "found": item["available_length"],
1062
+ "tsize": item["bytes"],
1063
+ "asize": item["available_bytes"],
1064
+ "vp": item["vp"],
1065
+ "immutable": 1,
1066
+ }
1067
+ ]
1068
+ return retMap
1069
+
1070
+ # list RSEs
1071
+ def list_rses(self, filter=None):
1072
+ method_name = "list_rses"
1073
+ method_name += f" filter={filter}"
1074
+ tmp_log = LogWrapper(_logger, method_name)
1075
+ tmp_log.debug("start")
1076
+ ret_list = []
1077
+ try:
1078
+ # get rucio API
1079
+ client = self._get_rucio_client()
1080
+ # get RSEs
1081
+ result = client.list_rses(filter)
1082
+ if result:
1083
+ # res is a generator yielding {"rse": "name_of_rse"}
1084
+ for x in result:
1085
+ rse = x["rse"]
1086
+ ret_list.append(rse)
1087
+ except InvalidRSEExpression:
1088
+ tmp_log.warning("Provided filter is considered invalid")
1089
+ except Exception:
1090
+ tmp_log.error(f"got error ; {traceback.format_exc()}")
1091
+ return None
1092
+ tmp_log.debug(f"got {ret_list}")
1093
+ return ret_list
1094
+
1095
+ # list DID rules
1096
+ def list_did_rules(self, dataset_name, all_accounts=False):
1097
+ method_name = "list_did_rules"
1098
+ method_name += f" dataset_name={dataset_name} all_accounts={all_accounts}"
1099
+ tmp_log = LogWrapper(_logger, method_name)
1100
+ tmp_log.debug("start")
1101
+ ret_list = []
1102
+ try:
1103
+ # get rucio API
1104
+ client = self._get_rucio_client()
1105
+ # get scope and name
1106
+ scope, dsn = self.extract_scope(dataset_name, strip_slash=True)
1107
+ # get rules
1108
+ for rule in client.list_did_rules(scope=scope, name=dsn):
1109
+ if rule["account"] == client.account or all_accounts:
1110
+ ret_list.append(rule)
1111
+ except Exception as e:
1112
+ tmp_log.error(f"got error ; {traceback.format_exc()}")
1113
+ return None
1114
+ tmp_log.debug(f"got {len(ret_list)} rules")
1115
+ return ret_list
1116
+
1117
+ # get dataset metadata
1118
+ def get_dataset_metadata(self, dataset_name, ignore_missing=False):
1119
+ # make logger
1120
+ method_name = "get_dataset_metadata"
1121
+ method_name = f"{method_name} dataset_name={dataset_name}"
1122
+ tmp_log = LogWrapper(_logger, method_name)
1123
+ tmp_log.debug("start")
1124
+ try:
1125
+ # get rucio API
1126
+ client = self._get_rucio_client()
1127
+ # get scope and name
1128
+ scope, dsn = self.extract_scope(dataset_name, strip_slash=True)
1129
+ # get metadata
1130
+ if dsn.endswith("/"):
1131
+ dsn = dsn[:-1]
1132
+ tmpRet = client.get_metadata(scope, dsn)
1133
+ # set state
1134
+ if tmpRet["is_open"] is True and tmpRet["did_type"] != "CONTAINER":
1135
+ tmpRet["state"] = "open"
1136
+ else:
1137
+ tmpRet["state"] = "closed"
1138
+ tmp_log.debug(str(tmpRet))
1139
+ return tmpRet
1140
+ except DataIdentifierNotFound as e:
1141
+ if ignore_missing:
1142
+ tmp_log.warning(e)
1143
+ tmpRet = {}
1144
+ tmpRet["state"] = "missing"
1145
+ return tmpRet
1146
+ except Exception as e:
1147
+ tmp_log.error(f"got error ; {traceback.format_exc()}")
1148
+ return None
1149
+
1150
+ # get files in dataset
1151
+ def get_files_in_dataset(self, dataset_name, skip_duplicate=True, ignore_unknown=False, long_format=False, lfn_only=False):
1152
+ method_name = "get_files_in_dataset"
1153
+ method_name += f" <dataset_name={dataset_name}>"
1154
+ tmp_log = LogWrapper(_logger, method_name)
1155
+ tmp_log.debug("start")
1156
+ try:
1157
+ # get Rucio API
1158
+ client = self._get_rucio_client()
1159
+ # extract scope from dataset
1160
+ scope, dsn = self.extract_scope(dataset_name)
1161
+ if dsn.endswith("/"):
1162
+ dsn = dsn[:-1]
1163
+ # get length
1164
+ tmpMeta = client.get_metadata(scope, dsn)
1165
+ # get files
1166
+ fileMap = {}
1167
+ baseLFNmap = {}
1168
+ fileSet = set()
1169
+ for x in client.list_files(scope, dsn, long=long_format):
1170
+ # convert to old dict format
1171
+ lfn = str(x["name"])
1172
+ if lfn_only:
1173
+ fileSet.add(lfn)
1174
+ continue
1175
+ attrs = {}
1176
+ attrs["lfn"] = lfn
1177
+ attrs["chksum"] = "ad:" + str(x["adler32"])
1178
+ attrs["md5sum"] = attrs["chksum"]
1179
+ attrs["checksum"] = attrs["chksum"]
1180
+ attrs["fsize"] = x["bytes"]
1181
+ attrs["filesize"] = attrs["fsize"]
1182
+ attrs["scope"] = str(x["scope"])
1183
+ attrs["events"] = str(x["events"])
1184
+ if long_format:
1185
+ attrs["lumiblocknr"] = str(x["lumiblocknr"])
1186
+ guid = str(f"{x['guid'][0:8]}-{x['guid'][8:12]}-{x['guid'][12:16]}-{x['guid'][16:20]}-{x['guid'][20:32]}")
1187
+ attrs["guid"] = guid
1188
+ # skip duplicated files
1189
+ if skip_duplicate:
1190
+ # extract base LFN and attempt number
1191
+ baseLFN = re.sub("(\.(\d+))$", "", lfn)
1192
+ attNr = re.sub(baseLFN + "\.*", "", lfn)
1193
+ if attNr == "":
1194
+ # without attempt number
1195
+ attNr = -1
1196
+ else:
1197
+ attNr = int(attNr)
1198
+ # compare attempt numbers
1199
+ addMap = False
1200
+ if baseLFN in baseLFNmap:
1201
+ # use larger attempt number
1202
+ oldMap = baseLFNmap[baseLFN]
1203
+ if oldMap["attNr"] < attNr:
1204
+ del fileMap[oldMap["guid"]]
1205
+ addMap = True
1206
+ else:
1207
+ addMap = True
1208
+ # append
1209
+ if not addMap:
1210
+ continue
1211
+ baseLFNmap[baseLFN] = {"guid": guid, "attNr": attNr}
1212
+ fileMap[guid] = attrs
1213
+ if lfn_only:
1214
+ return_list = fileSet
1215
+ else:
1216
+ return_list = fileMap
1217
+ tmp_log.debug(f"done len={len(return_list)} meta={tmpMeta['length']}")
1218
+ if tmpMeta["length"] and tmpMeta["length"] > len(return_list):
1219
+ err_msg = f"file list length mismatch len={len(return_list)} != meta={tmpMeta['length']}"
1220
+ tmp_log.error(err_msg)
1221
+ return None
1222
+ return return_list
1223
+ except DataIdentifierNotFound as e:
1224
+ if ignore_unknown:
1225
+ return {}
1226
+ errType = e
1227
+ except Exception as e:
1228
+ tmp_log.error(f"got error ; {traceback.format_exc()}")
1229
+ return None
1230
+
1231
+ # list datasets in container
1232
+ def list_datasets_in_container_JEDI(self, container_name):
1233
+ method_name = "list_datasets_in_container_JEDI"
1234
+ method_name += f" <container_name={container_name}>"
1235
+ tmp_log = LogWrapper(_logger, method_name)
1236
+ tmp_log.debug("start")
1237
+ try:
1238
+ # get rucio
1239
+ client = self._get_rucio_client()
1240
+ # get scope and name
1241
+ scope, dsn = self.extract_scope(container_name, strip_slash=True)
1242
+ # get contents
1243
+ dsList = self.wp_list_content(client, scope, dsn)
1244
+ tmp_log.debug("got " + str(dsList))
1245
+ return dsList
1246
+ except Exception as e:
1247
+ tmp_log.error(f"got error ; {traceback.format_exc()}")
1248
+ return None
1249
+
1250
+ # make staging rule
1251
+ def make_staging_rule(self, dataset_name, expression, activity, lifetime=None, weight=None, notify="N", source_replica_expression=None):
1252
+ method_name = "make_staging_rule"
1253
+ method_name = f"{method_name} dataset_name={dataset_name} expression={expression} activity={activity} lifetime={lifetime}"
1254
+ tmp_log = LogWrapper(_logger, method_name)
1255
+ tmp_log.debug("start")
1256
+ ruleID = None
1257
+ try:
1258
+ if lifetime is not None:
1259
+ lifetime = lifetime * 24 * 60 * 60
1260
+ # get rucio API
1261
+ client = self._get_rucio_client()
1262
+ # get scope and name
1263
+ scope, dsn = self.extract_scope(dataset_name, strip_slash=True)
1264
+ # check if a replication rule already exists
1265
+ if ruleID is None:
1266
+ dids = [{"scope": scope, "name": dsn}]
1267
+ for rule in client.list_did_rules(scope=scope, name=dsn):
1268
+ if rule["rse_expression"] == expression and rule["account"] == client.account and rule["activity"] == activity:
1269
+ ruleID = rule["id"]
1270
+ tmp_log.debug(f"rule already exists: ID={ruleID}")
1271
+ break
1272
+ # make new rule
1273
+ if ruleID is None:
1274
+ rule_id_list = client.add_replication_rule(
1275
+ dids=dids,
1276
+ copies=1,
1277
+ rse_expression=expression,
1278
+ weight=weight,
1279
+ lifetime=lifetime,
1280
+ grouping="DATASET",
1281
+ account=client.account,
1282
+ locked=False,
1283
+ notify=notify,
1284
+ ignore_availability=False,
1285
+ activity=activity,
1286
+ asynchronous=False,
1287
+ source_replica_expression=source_replica_expression,
1288
+ )
1289
+ ruleID = rule_id_list[0]
1290
+ tmp_log.debug(f"made new rule : ID={ruleID}")
1291
+ except Exception as e:
1292
+ tmp_log.error(f"failed to make staging rule with {str(e)} {traceback.format_exc()}")
1293
+ return None
1294
+ tmp_log.debug("done")
1295
+ return ruleID
1296
+
1297
+ # update replication rule by rule ID
1298
+ def update_rule_by_id(self, rule_id, set_map):
1299
+ method_name = "update_rule_by_id"
1300
+ method_name = f"{method_name} rule_id={rule_id} set_map={set_map}"
1301
+ tmp_log = LogWrapper(_logger, method_name)
1302
+ tmp_log.debug("start")
1303
+ try:
1304
+ # get rucio API
1305
+ client = self._get_rucio_client()
1306
+ # update rule
1307
+ client.update_replication_rule(rule_id, set_map)
1308
+ except Exception as e:
1309
+ tmp_log.error(f"got error ; {traceback.format_exc()}")
1310
+ return None
1311
+ tmp_log.debug("done")
1312
+ return True
1313
+
1314
+ # get replication rule by rule ID
1315
+ def get_rule_by_id(self, rule_id, allow_missing=True):
1316
+ method_name = "get_rule_by_id"
1317
+ method_name = f"{method_name} rule_id={rule_id}"
1318
+ tmp_log = LogWrapper(_logger, method_name)
1319
+ tmp_log.debug("start")
1320
+ try:
1321
+ # get rucio API
1322
+ client = self._get_rucio_client()
1323
+ # get rules
1324
+ rule = client.get_replication_rule(rule_id)
1325
+ except RuleNotFound as e:
1326
+ if allow_missing:
1327
+ tmp_log.warning(e)
1328
+ return False
1329
+ else:
1330
+ tmp_log.error(f"got error ; {traceback.format_exc()}")
1331
+ return None
1332
+ except Exception as e:
1333
+ tmp_log.error(f"got error ; {traceback.format_exc()}")
1334
+ return None
1335
+ tmp_log.debug(f"got rule")
1336
+ return rule
1337
+
1338
+ # list details of all replica locks for a rule by rule ID
1339
+ def list_replica_locks_by_id(self, rule_id):
1340
+ method_name = "list_replica_locks_by_id"
1341
+ method_name = f"{method_name} rule_id={rule_id}"
1342
+ tmp_log = LogWrapper(_logger, method_name)
1343
+ tmp_log.debug("start")
1344
+ try:
1345
+ # get rucio API
1346
+ client = self._get_rucio_client()
1347
+ # get generator of replica locks
1348
+ res = client.list_replica_locks(rule_id)
1349
+ # turn into list
1350
+ ret = list(res)
1351
+ except Exception as e:
1352
+ tmp_log.error(f"got error ; {traceback.format_exc()}")
1353
+ return None
1354
+ tmp_log.debug(f"got replica locks")
1355
+ return ret
1356
+
1357
+ # delete replication rule by rule ID
1358
+ def delete_replication_rule(self, rule_id, allow_missing=True):
1359
+ method_name = "delete_replication_rule"
1360
+ method_name = f"{method_name} rule_id={rule_id}"
1361
+ tmp_log = LogWrapper(_logger, method_name)
1362
+ tmp_log.debug("start")
1363
+ try:
1364
+ # get rucio API
1365
+ client = self._get_rucio_client()
1366
+ # get rules
1367
+ ret = client.delete_replication_rule(rule_id)
1368
+ except RuleNotFound as e:
1369
+ if allow_missing:
1370
+ tmp_log.debug(e)
1371
+ return False
1372
+ except Exception as e:
1373
+ tmp_log.error(f"got error ; {traceback.format_exc()}")
1374
+ return None
1375
+ tmp_log.debug(f"deleted, return {ret}")
1376
+ return ret
1377
+
1378
+
1379
+ # instantiate
1380
+ rucioAPI = RucioAPI()
1381
+ del RucioAPI