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,1708 @@
1
+ import datetime
2
+ import json
3
+ import math
4
+ import os
5
+ import re
6
+ import sys
7
+ import traceback
8
+ from typing import Any
9
+
10
+ import requests
11
+
12
+ try:
13
+ import urllib3
14
+
15
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
16
+ except Exception:
17
+ pass
18
+
19
+ from pandacommon.pandalogger.PandaLogger import PandaLogger
20
+ from pandacommon.pandautils.PandaUtils import naive_utcnow
21
+ from rucio.client import Client as RucioClient
22
+ from rucio.common.exception import (
23
+ DataIdentifierAlreadyExists,
24
+ DataIdentifierNotFound,
25
+ DuplicateContent,
26
+ DuplicateRule,
27
+ InvalidObject,
28
+ RSENotFound,
29
+ RuleNotFound,
30
+ UnsupportedOperation,
31
+ )
32
+
33
+ from pandajedi.jediconfig import jedi_config
34
+ from pandajedi.jedicore.Interaction import StatusCode
35
+ from pandajedi.jedicore.MsgWrapper import MsgWrapper
36
+ from pandaserver.dataservice import DataServiceUtils, ddm
37
+
38
+ from .DDMClientBase import DDMClientBase
39
+
40
+ logger = PandaLogger().getLogger(__name__.split(".")[-1])
41
+
42
+
43
+ # class to access to ATLAS DDM
44
+ class AtlasDDMClient(DDMClientBase):
45
+ # constructor
46
+ def __init__(self, con):
47
+ # initialize base class
48
+ DDMClientBase.__init__(self, con)
49
+ # the list of fatal error
50
+ self.fatalErrors = []
51
+ # how frequently update DN/token map
52
+ self.timeIntervalBL = datetime.timedelta(seconds=60 * 10)
53
+ # dict of endpoints
54
+ self.endPointDict = {}
55
+ # time of last update for endpoint dict
56
+ self.lastUpdateEP = None
57
+ # how frequently update endpoint dict
58
+ self.timeIntervalEP = datetime.timedelta(seconds=60 * 10)
59
+ # pid
60
+ self.pid = os.getpid()
61
+
62
+ # get files in dataset
63
+ def getFilesInDataset(self, datasetName, getNumEvents=False, skipDuplicate=True, ignoreUnknown=False, longFormat=False, lfn_only=False):
64
+ methodName = "getFilesInDataset"
65
+ methodName += f" pid={self.pid}"
66
+ methodName += f" <datasetName={datasetName}>"
67
+ tmpLog = MsgWrapper(logger, methodName)
68
+ tmpLog.debug("start")
69
+ try:
70
+ # get Rucio API
71
+ client = RucioClient()
72
+ # extract scope from dataset
73
+ scope, dsn = self.extract_scope(datasetName)
74
+ if dsn.endswith("/"):
75
+ dsn = dsn[:-1]
76
+ # get length
77
+ tmpMeta = client.get_metadata(scope, dsn)
78
+ # get files
79
+ fileMap = {}
80
+ baseLFNmap = {}
81
+ fileSet = set()
82
+ for x in client.list_files(scope, dsn, long=longFormat):
83
+ # convert to old dict format
84
+ lfn = str(x["name"])
85
+ if lfn_only:
86
+ fileSet.add(lfn)
87
+ continue
88
+ attrs = {}
89
+ attrs["lfn"] = lfn
90
+ attrs["chksum"] = "ad:" + str(x["adler32"])
91
+ attrs["md5sum"] = attrs["chksum"]
92
+ attrs["checksum"] = attrs["chksum"]
93
+ attrs["fsize"] = x["bytes"]
94
+ attrs["filesize"] = attrs["fsize"]
95
+ attrs["scope"] = str(x["scope"])
96
+ attrs["events"] = str(x["events"])
97
+ if longFormat:
98
+ attrs["lumiblocknr"] = str(x["lumiblocknr"])
99
+ guid = str(f"{x['guid'][0:8]}-{x['guid'][8:12]}-{x['guid'][12:16]}-{x['guid'][16:20]}-{x['guid'][20:32]}")
100
+ attrs["guid"] = guid
101
+ # skip duplicated files
102
+ if skipDuplicate:
103
+ # extract base LFN and attempt number
104
+ baseLFN = re.sub("(\.(\d+))$", "", lfn)
105
+ attNr = re.sub(baseLFN + "\.*", "", lfn)
106
+ if attNr == "":
107
+ # without attempt number
108
+ attNr = -1
109
+ else:
110
+ attNr = int(attNr)
111
+ # compare attempt numbers
112
+ addMap = False
113
+ if baseLFN in baseLFNmap:
114
+ # use larger attempt number
115
+ oldMap = baseLFNmap[baseLFN]
116
+ if oldMap["attNr"] < attNr:
117
+ del fileMap[oldMap["guid"]]
118
+ addMap = True
119
+ else:
120
+ addMap = True
121
+ # append
122
+ if not addMap:
123
+ continue
124
+ baseLFNmap[baseLFN] = {"guid": guid, "attNr": attNr}
125
+ fileMap[guid] = attrs
126
+ if lfn_only:
127
+ return_list = fileSet
128
+ else:
129
+ return_list = fileMap
130
+ tmpLog.debug(f"done len={len(return_list)} meta={tmpMeta['length']}")
131
+ if tmpMeta["length"] and tmpMeta["length"] > len(return_list):
132
+ errMsg = f"file list length mismatch len={len(return_list)} != meta={tmpMeta['length']}"
133
+ tmpLog.error(errMsg)
134
+ return self.SC_FAILED, errMsg
135
+ return self.SC_SUCCEEDED, return_list
136
+ except DataIdentifierNotFound as e:
137
+ if ignoreUnknown:
138
+ return self.SC_SUCCEEDED, {}
139
+ errType = e
140
+ except Exception as e:
141
+ errType = e
142
+ errCode, errMsg = self.checkError(errType)
143
+ tmpLog.error(errMsg)
144
+ return errCode, f"{methodName} : {errMsg}"
145
+
146
+ # list dataset replicas
147
+ def listDatasetReplicas(self, datasetName, use_vp=False, detailed=False, skip_incomplete_element=False, use_deep=False, element_list=None):
148
+ methodName = "listDatasetReplicas"
149
+ methodName += f" pid={self.pid}"
150
+ methodName += f" <datasetName={datasetName}>"
151
+ tmpLog = MsgWrapper(logger, methodName)
152
+ tmpLog.debug("start")
153
+ try:
154
+ if not datasetName.endswith("/"):
155
+ # get file list
156
+ tmpRet = self.convertOutListDatasetReplicas(datasetName, usefileLookup=use_deep, use_vp=use_vp)
157
+ tmpLog.debug("got new " + str(tmpRet))
158
+ if detailed:
159
+ return self.SC_SUCCEEDED, tmpRet, {datasetName: tmpRet}
160
+ return self.SC_SUCCEEDED, tmpRet
161
+ else:
162
+ # list of attributes summed up
163
+ retMap = {}
164
+ detailedRetMap = {}
165
+ # get constituent datasets
166
+ if element_list:
167
+ dsList = ["{}:{}".format(*self.extract_scope(n)) for n in element_list]
168
+ else:
169
+ tmpS, dsList = self.listDatasetsInContainer(datasetName)
170
+ grandTotal = 0
171
+ for tmpName in dsList:
172
+ tmpLog.debug(tmpName)
173
+ tmp_status, tmp_output = self.getDatasetMetaData(tmpName)
174
+ if tmp_status != self.SC_SUCCEEDED:
175
+ raise RuntimeError(f"failed to get metadata with {tmp_output}")
176
+ try:
177
+ totalFiles = tmp_output["length"]
178
+ if not totalFiles:
179
+ totalFiles = 0
180
+ except Exception:
181
+ totalFiles = 0
182
+ tmpRet = self.convertOutListDatasetReplicas(tmpName, usefileLookup=use_deep, use_vp=use_vp, skip_incomplete_element=skip_incomplete_element)
183
+ detailedRetMap[tmpName] = tmpRet
184
+ # loop over all sites
185
+ for tmpSite, tmpValMap in tmpRet.items():
186
+ # add site
187
+ retMap.setdefault(tmpSite, [{"found": 0}])
188
+ # sum
189
+ try:
190
+ retMap[tmpSite][-1]["found"] += int(tmpValMap[-1]["found"])
191
+ except Exception:
192
+ pass
193
+ # total
194
+ try:
195
+ if totalFiles < int(tmpValMap[-1]["total"]):
196
+ totalFiles = int(tmpValMap[-1]["total"])
197
+ except Exception:
198
+ pass
199
+ grandTotal += totalFiles
200
+ # set total
201
+ for tmpSite in retMap.keys():
202
+ retMap[tmpSite][-1]["total"] = grandTotal
203
+ # return
204
+ tmpLog.debug("got " + str(retMap))
205
+ if detailed:
206
+ return self.SC_SUCCEEDED, retMap, detailedRetMap
207
+ return self.SC_SUCCEEDED, retMap
208
+ except Exception as e:
209
+ errType = e
210
+ errCode, errMsg = self.checkError(errType)
211
+ tmpLog.error(errMsg + traceback.format_exc())
212
+ if detailed:
213
+ return errCode, f"{methodName} : {errMsg}", None
214
+ return errCode, f"{methodName} : {errMsg}"
215
+
216
+ # list replicas per dataset
217
+ def listReplicasPerDataset(self, datasetName, deepScan=False):
218
+ methodName = "listReplicasPerDataset"
219
+ methodName += f" pid={self.pid}"
220
+ methodName += f" <datasetName={datasetName}>"
221
+ tmpLog = MsgWrapper(logger, methodName)
222
+ tmpLog.debug(f"start with deepScan={deepScan}")
223
+ try:
224
+ # get rucio API
225
+ client = RucioClient()
226
+ # get scope and name
227
+ scope, dsn = self.extract_scope(datasetName)
228
+ datasets = []
229
+ if not datasetName.endswith("/"):
230
+ datasets = [dsn]
231
+ else:
232
+ # get constituent datasets
233
+ itr = client.list_content(scope, dsn)
234
+ datasets = [i["name"] for i in itr]
235
+ retMap = {}
236
+ for tmpName in datasets:
237
+ retMap[tmpName] = self.convertOutListDatasetReplicas(tmpName, deepScan)
238
+ tmpLog.debug("got " + str(retMap))
239
+ return self.SC_SUCCEEDED, retMap
240
+ except Exception as e:
241
+ errType = e
242
+ errCode, errMsg = self.checkError(errType)
243
+ tmpLog.error(errMsg)
244
+ return errCode, f"{methodName} : {errMsg}"
245
+
246
+ # get site property
247
+ def getSiteProperty(self, seName, attribute):
248
+ methodName = "getSiteProperty"
249
+ methodName += f" pid={self.pid}"
250
+ self.updateEndPointDict()
251
+ try:
252
+ retVal = self.endPointDict[seName][attribute]
253
+ return self.SC_SUCCEEDED, retVal
254
+ except Exception as e:
255
+ errType = e
256
+ errCode, errMsg = self.checkError(errType)
257
+ return errCode, f"{methodName} : {errMsg}"
258
+
259
+ # get site alternateName
260
+ def getSiteAlternateName(self, se_name):
261
+ self.updateEndPointDict()
262
+ if se_name in self.endPointDict:
263
+ return [self.endPointDict[se_name]["site"]]
264
+ return None
265
+
266
+ def SiteHasCompleteReplica(self, dataset_replica_map, endpoint, total_files_in_dataset):
267
+ """
268
+ Checks the #found files at site == #total files at site == #files in dataset. VP is regarded as complete
269
+ :return: True or False
270
+ """
271
+ try:
272
+ if "vp" in dataset_replica_map[endpoint][-1]:
273
+ if dataset_replica_map[endpoint][-1]["vp"]:
274
+ return True
275
+ found_tmp = dataset_replica_map[endpoint][-1]["found"]
276
+ total_tmp = dataset_replica_map[endpoint][-1]["total"]
277
+ if found_tmp is not None and total_tmp == found_tmp and total_tmp >= total_files_in_dataset:
278
+ return True
279
+ except KeyError:
280
+ pass
281
+ return False
282
+
283
+ def getAvailableFiles(
284
+ self,
285
+ dataset_spec,
286
+ site_endpoint_map,
287
+ site_mapper,
288
+ check_LFC=False,
289
+ check_completeness=True,
290
+ storage_token=None,
291
+ complete_only=False,
292
+ use_vp=True,
293
+ file_scan_in_container=True,
294
+ use_deep=False,
295
+ element_list=None,
296
+ ):
297
+ """
298
+ :param dataset_spec: dataset spec object
299
+ :param site_endpoint_map: panda sites to ddm endpoints map. The list of panda sites includes the ones to scan
300
+ :param site_mapper: site mapper object
301
+ :param check_LFC: check/ask Tadashi/probably obsolete
302
+ :param check_completeness:
303
+ :param storage_token:
304
+ :param complete_only: check only for complete replicas
305
+ :param use_vp: use virtual placement
306
+ :param file_scan_in_container: enable file lookup for container
307
+ :param use_deep: use deep option for replica lookup
308
+ :param element_list: interesting elements in dataset container
309
+
310
+ TODO: do we need NG, do we need alternate names
311
+ TODO: the storage_token is not used anymore
312
+ :return:
313
+ """
314
+ # make logger
315
+ method_name = "getAvailableFiles"
316
+ method_name += f" pid={self.pid}"
317
+ method_name += f" < jediTaskID={dataset_spec.jediTaskID} datasetID={dataset_spec.datasetID} >"
318
+ tmp_log = MsgWrapper(logger, method_name)
319
+ loopStart = naive_utcnow()
320
+ try:
321
+ tmp_log.debug(
322
+ "start datasetName={} check_completeness={} nFiles={} nSites={} "
323
+ "complete_only={}".format(dataset_spec.datasetName, check_completeness, len(dataset_spec.Files), len(site_endpoint_map), complete_only)
324
+ )
325
+ # update the definition of all endpoints from AGIS
326
+ self.updateEndPointDict()
327
+
328
+ # get the file map
329
+ tmp_status, tmp_output = self.getDatasetMetaData(dataset_spec.datasetName)
330
+ if tmp_status != self.SC_SUCCEEDED:
331
+ regTime = naive_utcnow() - loopStart
332
+ tmp_log.error(f"failed in {regTime.seconds} sec to get metadata with {tmp_output}")
333
+ return tmp_status, tmp_output
334
+ total_files_in_dataset = tmp_output["length"]
335
+ if total_files_in_dataset is None:
336
+ total_files_in_dataset = 0
337
+ if tmp_output["did_type"] == "CONTAINER":
338
+ is_container = True
339
+ else:
340
+ is_container = False
341
+
342
+ # get the dataset replica map
343
+ tmp_status, tmp_output, detailed_replica_map = self.listDatasetReplicas(
344
+ dataset_spec.datasetName, use_vp=use_vp, detailed=True, use_deep=use_deep, element_list=element_list
345
+ )
346
+ if tmp_status != self.SC_SUCCEEDED:
347
+ regTime = naive_utcnow() - loopStart
348
+ tmp_log.error(f"failed in {regTime.seconds} sec to get dataset replicas with {tmp_output}")
349
+ return tmp_status, tmp_output
350
+ dataset_replica_map = tmp_output
351
+
352
+ # collect GUIDs and LFNs
353
+ file_map = {} # GUID to LFN
354
+ lfn_filespec_map = {} # LFN to file spec
355
+ scope_map = {} # LFN to scope list
356
+ for tmp_file in dataset_spec.Files:
357
+ file_map[tmp_file.GUID] = tmp_file.lfn
358
+ lfn_filespec_map.setdefault(tmp_file.lfn, [])
359
+ lfn_filespec_map[tmp_file.lfn].append(tmp_file)
360
+ scope_map[tmp_file.lfn] = tmp_file.scope
361
+
362
+ complete_replica_map = {}
363
+ endpoint_storagetype_map = {}
364
+ rse_list = []
365
+
366
+ # figure out complete replicas and storage types
367
+ for site_name, endpoint_list in site_endpoint_map.items():
368
+ tmp_site_spec = site_mapper.getSite(site_name)
369
+
370
+ has_complete = False
371
+ tmp_rse_list = []
372
+
373
+ # loop over all endpoints
374
+ for endpoint in endpoint_list:
375
+ # storage type
376
+ tmp_status, is_tape = self.getSiteProperty(endpoint, "is_tape")
377
+ if is_tape:
378
+ storage_type = "localtape"
379
+ else:
380
+ storage_type = "localdisk"
381
+
382
+ if (
383
+ self.SiteHasCompleteReplica(dataset_replica_map, endpoint, total_files_in_dataset)
384
+ or (endpoint in dataset_replica_map and not check_completeness)
385
+ or DataServiceUtils.isCachedFile(dataset_spec.datasetName, tmp_site_spec)
386
+ ):
387
+ complete_replica_map[endpoint] = storage_type
388
+ has_complete = True
389
+
390
+ # no scan for many-time datasets or disabled completeness check
391
+ if dataset_spec.isManyTime() or (not check_completeness and endpoint not in dataset_replica_map) or complete_only:
392
+ continue
393
+
394
+ # disable file lookup if unnecessary
395
+ if endpoint not in rse_list and (file_scan_in_container or not is_container):
396
+ tmp_rse_list.append(endpoint)
397
+
398
+ endpoint_storagetype_map[endpoint] = storage_type
399
+
400
+ # add to list to trigger file lookup if complete replica is unavailable
401
+ if not has_complete and tmp_rse_list:
402
+ rse_list += tmp_rse_list
403
+
404
+ # get the file locations from Rucio
405
+ if len(rse_list) > 0:
406
+ tmp_log.debug(f"lookup file replicas in Rucio for RSEs: {rse_list}")
407
+ tmp_status, rucio_lfn_to_rse_map = self.jedi_list_replicas(file_map, rse_list, scopes=scope_map)
408
+ tmp_log.debug(f"lookup file replicas return status: {str(tmp_status)}")
409
+ if tmp_status != self.SC_SUCCEEDED:
410
+ raise RuntimeError(rucio_lfn_to_rse_map)
411
+ else:
412
+ rucio_lfn_to_rse_map = dict()
413
+ if not file_scan_in_container and is_container:
414
+ # remove incomplete
415
+ detailed_comp_replica_map = dict()
416
+ for tmp_ds_name, tmp_ds_value in detailed_replica_map.items():
417
+ new_map = {}
418
+ for tmp_k, tmp_v in tmp_ds_value.items():
419
+ if tmp_v[0]["total"] and tmp_v[0]["total"] == tmp_v[0]["found"]:
420
+ new_map[tmp_k] = tmp_v
421
+ if new_map:
422
+ detailed_comp_replica_map[tmp_ds_name] = new_map
423
+ # make file list from detailed replica map
424
+ files_in_container = {}
425
+ for tmp_ds_name in detailed_comp_replica_map.keys():
426
+ tmp_status, tmp_files = self.getFilesInDataset(tmp_ds_name, ignoreUnknown=True, lfn_only=True)
427
+ if tmp_status != self.SC_SUCCEEDED:
428
+ raise RuntimeError(tmp_files)
429
+ for tmp_lfn in tmp_files:
430
+ files_in_container[tmp_lfn] = tmp_ds_name
431
+ for tmp_file in dataset_spec.Files:
432
+ if tmp_file.lfn in files_in_container and files_in_container[tmp_file.lfn] in detailed_comp_replica_map:
433
+ rucio_lfn_to_rse_map[tmp_file.lfn] = detailed_comp_replica_map[files_in_container[tmp_file.lfn]]
434
+
435
+ # initialize the return map and add complete/cached replicas
436
+ return_map = {}
437
+ checked_dst = set()
438
+ for site_name, tmp_endpoints in site_endpoint_map.items():
439
+ return_map.setdefault(site_name, {"localdisk": [], "localtape": [], "cache": [], "remote": []})
440
+ tmp_site_spec = site_mapper.getSite(site_name)
441
+
442
+ # check if the dataset is cached
443
+ if DataServiceUtils.isCachedFile(dataset_spec.datasetName, tmp_site_spec):
444
+ # add to cached file list
445
+ return_map[site_name]["cache"] += dataset_spec.Files
446
+
447
+ # complete replicas
448
+ if not check_LFC:
449
+ for tmp_endpoint in tmp_endpoints:
450
+ if tmp_endpoint in complete_replica_map:
451
+ storage_type = complete_replica_map[tmp_endpoint]
452
+ return_map[site_name][storage_type] += dataset_spec.Files
453
+ checked_dst.add(site_name)
454
+
455
+ # loop over all available LFNs
456
+ available_lfns = sorted(rucio_lfn_to_rse_map.keys())
457
+ for tmp_lfn in available_lfns:
458
+ tmp_filespec_list = lfn_filespec_map[tmp_lfn]
459
+ tmp_filespec = lfn_filespec_map[tmp_lfn][0]
460
+ for site in site_endpoint_map:
461
+ for endpoint in site_endpoint_map[site]:
462
+ if endpoint in rucio_lfn_to_rse_map[tmp_lfn] and endpoint in endpoint_storagetype_map:
463
+ storage_type = endpoint_storagetype_map[endpoint]
464
+ if tmp_filespec not in return_map[site][storage_type]:
465
+ return_map[site][storage_type] += tmp_filespec_list
466
+ checked_dst.add(site)
467
+ break
468
+
469
+ # aggregate all types of storage types into the 'all' key
470
+ for site, storage_type_files in return_map.items():
471
+ site_all_file_list = set()
472
+ for storage_type, file_list in storage_type_files.items():
473
+ for tmp_file_spec in file_list:
474
+ site_all_file_list.add(tmp_file_spec)
475
+ storage_type_files["all"] = site_all_file_list
476
+
477
+ # dump for logging
478
+ logging_str = ""
479
+ for site, storage_type_file in return_map.items():
480
+ logging_str += f"{site}:("
481
+ for storage_type, file_list in storage_type_file.items():
482
+ logging_str += f"{storage_type}:{len(file_list)},"
483
+ logging_str = logging_str[:-1]
484
+ logging_str += ") "
485
+ logging_str = logging_str[:-1]
486
+ tmp_log.debug(logging_str)
487
+
488
+ # return
489
+ regTime = naive_utcnow() - loopStart
490
+ tmp_log.debug(f"done in {regTime.seconds} sec")
491
+ return self.SC_SUCCEEDED, return_map
492
+ except Exception as e:
493
+ regTime = naive_utcnow() - loopStart
494
+ error_message = f"failed in {regTime.seconds} sec with {str(e)} {traceback.format_exc()} "
495
+ tmp_log.error(error_message)
496
+ return self.SC_FAILED, f"{self.__class__.__name__}.{method_name} {error_message}"
497
+
498
+ def jedi_list_replicas(self, files, storages, scopes={}):
499
+ try:
500
+ method_name = "jedi_list_replicas"
501
+ method_name += f" pid={self.pid}"
502
+ tmp_log = MsgWrapper(logger, method_name)
503
+ client = RucioClient()
504
+ i_guid = 0
505
+ max_guid = 1000 # do 1000 guids in each Rucio call
506
+ lfn_to_rses_map = {}
507
+ dids = []
508
+ i_loop = 0
509
+ startTime = naive_utcnow()
510
+ tmp_log.debug("start")
511
+ for guid, lfn in files.items():
512
+ i_guid += 1
513
+ scope = scopes[lfn]
514
+ dids.append({"scope": scope, "name": lfn})
515
+ if len(dids) % max_guid == 0 or i_guid == len(files):
516
+ i_loop += 1
517
+ tmp_log.debug(f"lookup {i_loop} start")
518
+ loopStart = naive_utcnow()
519
+ x = client.list_replicas(dids, resolve_archives=True)
520
+ regTime = naive_utcnow() - loopStart
521
+ tmp_log.info(f"rucio.list_replicas took {regTime.seconds} sec for {len(dids)} files")
522
+ loopStart = naive_utcnow()
523
+ for tmp_dict in x:
524
+ try:
525
+ tmp_LFN = str(tmp_dict["name"])
526
+ lfn_to_rses_map[tmp_LFN] = tmp_dict["rses"]
527
+ except Exception:
528
+ pass
529
+ # reset the dids list for the next bulk for Rucio
530
+ dids = []
531
+ regTime = naive_utcnow() - loopStart
532
+ tmp_log.debug(f"lookup {i_loop} end in {regTime.seconds} sec")
533
+ regTime = naive_utcnow() - startTime
534
+ tmp_log.debug(f"end in {regTime.seconds} sec")
535
+ except Exception as e:
536
+ regTime = naive_utcnow() - startTime
537
+ tmp_log.error(f"failed in {regTime.seconds} sec")
538
+ return self.SC_FAILED, f"file lookup failed with {str(e)} {traceback.format_exc()}"
539
+
540
+ return self.SC_SUCCEEDED, lfn_to_rses_map
541
+
542
+ # list file replicas with dataset name/scope
543
+ def jedi_list_replicas_with_dataset(self, datasetName):
544
+ try:
545
+ scope, dsn = self.extract_scope(datasetName)
546
+ client = RucioClient()
547
+ lfn_to_rses_map = {}
548
+ dids = [{"scope": scope, "name": dsn}]
549
+ for tmp_dict in client.list_replicas(dids, resolve_archives=True):
550
+ try:
551
+ tmp_LFN = str(tmp_dict["name"])
552
+ except Exception:
553
+ continue
554
+ lfn_to_rses_map[tmp_LFN] = tmp_dict["rses"]
555
+ except Exception:
556
+ err_type, err_value = sys.exc_info()[:2]
557
+ return self.SC_FAILED, f"file lookup failed with {err_type}:{err_value} {traceback.format_exc()}"
558
+ return self.SC_SUCCEEDED, lfn_to_rses_map
559
+
560
+ # get dataset metadata
561
+ def getDatasetMetaData(self, datasetName, ignore_missing=False):
562
+ # make logger
563
+ methodName = "getDatasetMetaData"
564
+ methodName += f" pid={self.pid}"
565
+ methodName = f"{methodName} datasetName={datasetName}"
566
+ tmpLog = MsgWrapper(logger, methodName)
567
+ tmpLog.debug("start")
568
+ try:
569
+ # get rucio API
570
+ client = RucioClient()
571
+ # get scope and name
572
+ scope, dsn = self.extract_scope(datasetName)
573
+ # get metadata
574
+ if dsn.endswith("/"):
575
+ dsn = dsn[:-1]
576
+ tmpRet = client.get_metadata(scope, dsn)
577
+ # set state
578
+ if tmpRet["is_open"] is True and tmpRet["did_type"] != "CONTAINER":
579
+ tmpRet["state"] = "open"
580
+ else:
581
+ tmpRet["state"] = "closed"
582
+ tmpLog.debug(str(tmpRet))
583
+ return self.SC_SUCCEEDED, tmpRet
584
+ except DataIdentifierNotFound as e:
585
+ errType = e
586
+ errCode, errMsg = self.checkError(errType)
587
+ if ignore_missing:
588
+ tmpLog.debug(errMsg)
589
+ tmpRet = {}
590
+ tmpRet["state"] = "missing"
591
+ return self.SC_SUCCEEDED, tmpRet
592
+ except Exception as e:
593
+ errType = e
594
+ errCode, errMsg = self.checkError(errType)
595
+ tmpLog.error(errMsg)
596
+ return errCode, f"{methodName} : {errMsg}"
597
+
598
+ # check error
599
+ def checkError(self, errType):
600
+ errMsg = f"{str(type(errType))} : {str(errType)}"
601
+ if type(errType) in self.fatalErrors:
602
+ # fatal error
603
+ return self.SC_FATAL, errMsg
604
+ else:
605
+ # temporary error
606
+ return self.SC_FAILED, errMsg
607
+
608
+ # list dataset/container
609
+ def listDatasets(self, datasetName, ignorePandaDS=True):
610
+ methodName = "listDatasets"
611
+ methodName += f" pid={self.pid}"
612
+ methodName += f" <datasetName={datasetName}>"
613
+ tmpLog = MsgWrapper(logger, methodName)
614
+ tmpLog.debug("start")
615
+ try:
616
+ # get rucio API
617
+ client = RucioClient()
618
+ # get scope and name
619
+ scope, dsn = self.extract_scope(datasetName)
620
+ filters = {}
621
+ if dsn.endswith("/"):
622
+ dsn = dsn[:-1]
623
+ filters["name"] = dsn
624
+ dsList = set()
625
+ for name in client.list_dids(scope, filters, "dataset"):
626
+ dsList.add(f"{scope}:{name}")
627
+ for name in client.list_dids(scope, filters, "container"):
628
+ dsList.add(f"{scope}:{name}/")
629
+ dsList = list(dsList)
630
+ # ignore panda internal datasets
631
+ if ignorePandaDS:
632
+ tmpDsList = []
633
+ for tmpDS in dsList:
634
+ if re.search("_dis\d+$", tmpDS) is not None or re.search("_sub\d+$", tmpDS):
635
+ continue
636
+ tmpDsList.append(tmpDS)
637
+ dsList = tmpDsList
638
+ tmpLog.debug("got " + str(dsList))
639
+ return self.SC_SUCCEEDED, dsList
640
+ except Exception as e:
641
+ errType = e
642
+ errCode, errMsg = self.checkError(errType)
643
+ tmpLog.error(errMsg)
644
+ return errCode, f"{methodName} : {errMsg}"
645
+
646
+ # register new dataset/container
647
+ def registerNewDataset(self, datasetName, backEnd="rucio", location=None, lifetime=None, metaData=None, resurrect=False):
648
+ methodName = "registerNewDataset"
649
+ methodName += f" pid={self.pid}"
650
+ methodName += f" <datasetName={datasetName}>"
651
+ tmpLog = MsgWrapper(logger, methodName)
652
+ tmpLog.debug(f"start location={location} lifetime={lifetime}")
653
+ try:
654
+ # get rucio API
655
+ client = RucioClient()
656
+ # get scope and name
657
+ scope, dsn = self.extract_scope(datasetName)
658
+ # lifetime
659
+ if lifetime is not None:
660
+ lifetime = lifetime * 86400
661
+ # register
662
+ if not datasetName.endswith("/"):
663
+ # register dataset
664
+ name = dsn
665
+ client.add_dataset(scope, name, meta=metaData, lifetime=lifetime, rse=location)
666
+ else:
667
+ # register container
668
+ name = dsn
669
+ client.add_container(scope=scope, name=name)
670
+ except DataIdentifierAlreadyExists:
671
+ pass
672
+ except InvalidObject as e:
673
+ errMsg = f"{InvalidObject} : {str(e)}"
674
+ tmpLog.error(errMsg)
675
+ return self.SC_FATAL, f"{methodName} : {errMsg}"
676
+ except Exception as e:
677
+ errType = e
678
+ resurrected = False
679
+ # try to resurrect
680
+ if "DELETED_DIDS_PK violated" in str(errType) and resurrect:
681
+ try:
682
+ client.resurrect([{"scope": scope, "name": name}])
683
+ resurrected = True
684
+ except Exception:
685
+ pass
686
+ if not resurrected:
687
+ errCode, errMsg = self.checkError(errType)
688
+ tmpLog.error(errMsg)
689
+ return errCode, f"{methodName} : {errMsg}"
690
+ tmpLog.debug("done")
691
+ return self.SC_SUCCEEDED, True
692
+
693
+ # wrapper for list_content
694
+ def wp_list_content(self, client, scope, dsn):
695
+ if dsn.endswith("/"):
696
+ dsn = dsn[:-1]
697
+ retList = []
698
+ # get contents
699
+ for data in client.list_content(scope, dsn):
700
+ if data["type"] == "CONTAINER":
701
+ retList += self.wp_list_content(client, data["scope"], data["name"])
702
+ elif data["type"] == "DATASET":
703
+ retList.append(f"{data['scope']}:{data['name']}")
704
+ else:
705
+ pass
706
+ return retList
707
+
708
+ # list datasets in container
709
+ def listDatasetsInContainer(self, containerName):
710
+ methodName = "listDatasetsInContainer"
711
+ methodName += f" pid={self.pid}"
712
+ methodName += f" <containerName={containerName}>"
713
+ tmpLog = MsgWrapper(logger, methodName)
714
+ tmpLog.debug("start")
715
+ try:
716
+ # get rucio
717
+ client = RucioClient()
718
+ # get scope and name
719
+ scope, dsn = self.extract_scope(containerName)
720
+ # get contents
721
+ dsList = self.wp_list_content(client, scope, dsn)
722
+ tmpLog.debug("got " + str(dsList))
723
+ return self.SC_SUCCEEDED, dsList
724
+ except Exception as e:
725
+ errType = e
726
+ errCode, errMsg = self.checkError(errType)
727
+ tmpLog.error(errMsg)
728
+ return errCode, f"{methodName} : {errMsg}"
729
+
730
+ # expand Container
731
+ def expandContainer(self, containerName):
732
+ methodName = "expandContainer"
733
+ methodName += f" pid={self.pid}"
734
+ methodName += f" <contName={containerName}>"
735
+ tmpLog = MsgWrapper(logger, methodName)
736
+ tmpLog.debug("start")
737
+ try:
738
+ ds_size_map = {}
739
+ # get real names
740
+ tmpS, tmpRealNameList = self.listDatasets(containerName)
741
+ if tmpS != self.SC_SUCCEEDED:
742
+ tmpLog.error("failed to get real names")
743
+ return tmpS, tmpRealNameList
744
+ # loop over all names
745
+ for tmpRealName in tmpRealNameList:
746
+ # container
747
+ if tmpRealName.endswith("/"):
748
+ # get contents
749
+ tmpS, tmpO = self.listDatasetsInContainer(tmpRealName)
750
+ if tmpS != self.SC_SUCCEEDED:
751
+ tmpLog.error(f"failed to get datasets in {tmpRealName}")
752
+ return tmpS, tmpO
753
+ else:
754
+ tmpO = [tmpRealName]
755
+ # collect dataset names
756
+ non_empty_datasets = 0
757
+ for dataset_name in tmpO:
758
+ if dataset_name in ds_size_map:
759
+ continue
760
+ if non_empty_datasets > 10 or len(tmpO) == 1:
761
+ # skip metadata check as only one or enough datasets are available
762
+ n_files = 0
763
+ else:
764
+ # get number of files
765
+ tmp_status, tmp_output = self.getDatasetMetaData(dataset_name)
766
+ if tmp_status != self.SC_SUCCEEDED:
767
+ raise RuntimeError(f"failed to get metadata with {tmp_output}")
768
+ n_files = tmp_output["length"] if tmp_output["length"] else 0
769
+ if n_files > 0:
770
+ non_empty_datasets += 1
771
+ ds_size_map[dataset_name] = n_files
772
+ # sort by name
773
+ ds_size_map = dict(sorted(ds_size_map.items()))
774
+ # reverse sort by size to have larger datasets first
775
+ ds_list = [k for k in sorted(ds_size_map, key=ds_size_map.get, reverse=True)]
776
+ # return
777
+ tmpLog.debug(f"got {str(ds_list)}")
778
+ return self.SC_SUCCEEDED, ds_list
779
+ except Exception as e:
780
+ errType = e
781
+ errCode, errMsg = self.checkError(errType)
782
+ tmpLog.error(errMsg)
783
+ return errCode, f"{methodName} : {errMsg}"
784
+
785
+ # add dataset to container
786
+ def addDatasetsToContainer(self, containerName, datasetNames, backEnd="rucio"):
787
+ methodName = "addDatasetsToContainer"
788
+ methodName += f" pid={self.pid}"
789
+ methodName += f" <contName={containerName}>"
790
+ tmpLog = MsgWrapper(logger, methodName)
791
+ tmpLog.debug("start")
792
+ try:
793
+ # get Rucio API
794
+ client = RucioClient()
795
+ c_scope, c_name = self.extract_scope(containerName)
796
+ if c_name.endswith("/"):
797
+ c_name = c_name[:-1]
798
+ dsns = []
799
+ for ds in datasetNames:
800
+ ds_scope, ds_name = self.extract_scope(ds)
801
+ dsn = {"scope": ds_scope, "name": ds_name}
802
+ dsns.append(dsn)
803
+ try:
804
+ # add datasets
805
+ client.add_datasets_to_container(scope=c_scope, name=c_name, dsns=dsns)
806
+ except DuplicateContent:
807
+ # add datasets one by one
808
+ for ds in dsns:
809
+ try:
810
+ client.add_datasets_to_container(scope=c_scope, name=c_name, dsns=[ds])
811
+ except DuplicateContent:
812
+ pass
813
+ except Exception as e:
814
+ errType = e
815
+ errCode, errMsg = self.checkError(errType)
816
+ tmpLog.error(errMsg)
817
+ return errCode, f"{methodName} : {errMsg}"
818
+ tmpLog.debug("done")
819
+ return self.SC_SUCCEEDED, True
820
+
821
+ # get latest DBRelease
822
+ def getLatestDBRelease(self):
823
+ methodName = "getLatestDBRelease"
824
+ methodName += f" pid={self.pid}"
825
+ tmpLog = MsgWrapper(logger, methodName)
826
+ tmpLog.debug("trying to get the latest version number of DBR")
827
+ # get ddo datasets
828
+ tmpStat, ddoDatasets = self.listDatasets("ddo.*")
829
+ if tmpStat != self.SC_SUCCEEDED or ddoDatasets == {}:
830
+ tmpLog.error("failed to get a list of DBRelease datasets from DDM")
831
+ return self.SC_FAILED, None
832
+ # reverse sort to avoid redundant lookup
833
+ ddoDatasets.sort()
834
+ ddoDatasets.reverse()
835
+ # extract version number
836
+ latestVerMajor = 0
837
+ latestVerMinor = 0
838
+ latestVerBuild = 0
839
+ latestVerRev = 0
840
+ latestDBR = ""
841
+ for tmpName in ddoDatasets:
842
+ # ignore CDRelease
843
+ if ".CDRelease." in tmpName:
844
+ continue
845
+ # ignore user
846
+ if tmpName.startswith("ddo.user"):
847
+ continue
848
+ # use Atlas.Ideal
849
+ if ".Atlas.Ideal." not in tmpName:
850
+ continue
851
+ match = re.search("\.v(\d+)(_*[^\.]*)$", tmpName)
852
+ if match is None:
853
+ tmpLog.warning(f"cannot extract version number from {tmpName}")
854
+ continue
855
+ # ignore special DBRs
856
+ if match.group(2) != "":
857
+ continue
858
+ # get major,minor,build,revision numbers
859
+ tmpVerStr = match.group(1)
860
+ tmpVerMajor = 0
861
+ tmpVerMinor = 0
862
+ tmpVerBuild = 0
863
+ tmpVerRev = 0
864
+ try:
865
+ tmpVerMajor = int(tmpVerStr[0:2])
866
+ except Exception:
867
+ pass
868
+ try:
869
+ tmpVerMinor = int(tmpVerStr[2:4])
870
+ except Exception:
871
+ pass
872
+ try:
873
+ tmpVerBuild = int(tmpVerStr[4:6])
874
+ except Exception:
875
+ pass
876
+ try:
877
+ tmpVerRev = int(tmpVerStr[6:])
878
+ # use only three digit DBR
879
+ continue
880
+ except Exception:
881
+ pass
882
+ # compare
883
+ if latestVerMajor > tmpVerMajor:
884
+ continue
885
+ elif latestVerMajor == tmpVerMajor:
886
+ if latestVerMinor > tmpVerMinor:
887
+ continue
888
+ elif latestVerMinor == tmpVerMinor:
889
+ if latestVerBuild > tmpVerBuild:
890
+ continue
891
+ elif latestVerBuild == tmpVerBuild:
892
+ if latestVerRev > tmpVerRev:
893
+ continue
894
+ # check if well replicated
895
+ tmpStat, ddoReplicas = self.listDatasetReplicas(tmpName)
896
+ if ddoReplicas == []:
897
+ continue
898
+ # higher or equal version
899
+ latestVerMajor = tmpVerMajor
900
+ latestVerMinor = tmpVerMinor
901
+ latestVerBuild = tmpVerBuild
902
+ latestVerRev = tmpVerRev
903
+ latestDBR = tmpName
904
+ # failed
905
+ if latestDBR == "":
906
+ tmpLog.error("failed to get the latest version of DBRelease dataset from DDM")
907
+ return self.SC_FAILED, None
908
+ tmpLog.debug(f"use {latestDBR}")
909
+ return self.SC_SUCCEEDED, latestDBR
910
+
911
+ # freeze dataset
912
+ def freezeDataset(self, datasetName, ignoreUnknown=False):
913
+ methodName = "freezeDataset"
914
+ methodName += f" pid={self.pid}"
915
+ methodName = f"{methodName} datasetName={datasetName}"
916
+ tmpLog = MsgWrapper(logger, methodName)
917
+ tmpLog.debug("start")
918
+ isOK = True
919
+ try:
920
+ # get rucio API
921
+ client = RucioClient()
922
+ # get scope and name
923
+ scope, dsn = self.extract_scope(datasetName)
924
+ # check metadata to avoid a bug in rucio
925
+ if dsn.endswith("/"):
926
+ dsn = dsn[:-1]
927
+ tmpRet = client.get_metadata(scope, dsn)
928
+ # close
929
+ client.set_status(scope, dsn, open=False)
930
+ except UnsupportedOperation:
931
+ pass
932
+ except DataIdentifierNotFound as e:
933
+ errType = e
934
+ if ignoreUnknown:
935
+ pass
936
+ else:
937
+ isOK = False
938
+ except Exception as e:
939
+ errType = e
940
+ isOK = False
941
+ if isOK:
942
+ tmpLog.debug("done")
943
+ return self.SC_SUCCEEDED, True
944
+ else:
945
+ errCode, errMsg = self.checkError(errType)
946
+ tmpLog.error(errMsg)
947
+ return errCode, f"{methodName} : {errMsg}"
948
+
949
+ # finger
950
+ def finger(self, dn):
951
+ methodName = "finger"
952
+ methodName += f" pid={self.pid}"
953
+ methodName = f"{methodName} userName={dn}"
954
+ tmpLog = MsgWrapper(logger, methodName)
955
+ tmpLog.debug("start")
956
+ status, user_info = ddm.rucioAPI.finger(dn)
957
+ if status:
958
+ tmpLog.debug(f"done with {str(user_info)}")
959
+ return self.SC_SUCCEEDED, user_info
960
+ else:
961
+ err_msg = f"failed with {str(user_info)}"
962
+ tmpLog.error(err_msg)
963
+ return self.SC_FAILED, err_msg
964
+
965
+ # set dataset metadata
966
+ def setDatasetMetadata(self, datasetName, metadataName, metadaValue):
967
+ methodName = "setDatasetMetadata"
968
+ methodName += f" pid={self.pid}"
969
+ methodName = f"{methodName} datasetName={datasetName} metadataName={metadataName} metadaValue={metadaValue}"
970
+ tmpLog = MsgWrapper(logger, methodName)
971
+ tmpLog.debug("start")
972
+ try:
973
+ # get rucio API
974
+ client = RucioClient()
975
+ # get scope and name
976
+ scope, dsn = self.extract_scope(datasetName)
977
+ # set
978
+ client.set_metadata(scope, dsn, metadataName, metadaValue)
979
+ except (UnsupportedOperation, DataIdentifierNotFound):
980
+ pass
981
+ except Exception as e:
982
+ errType = e
983
+ errCode, errMsg = self.checkError(errType)
984
+ tmpLog.error(errMsg)
985
+ return errCode, f"{methodName} : {errMsg}"
986
+ tmpLog.debug("done")
987
+ return self.SC_SUCCEEDED, True
988
+
989
+ # register location
990
+ def registerDatasetLocation(
991
+ self, datasetName, location, lifetime=None, owner=None, backEnd="rucio", activity=None, grouping=None, weight=None, copies=1, ignore_availability=True
992
+ ):
993
+ methodName = "registerDatasetLocation"
994
+ methodName += f" pid={self.pid}"
995
+ methodName = f"{methodName} datasetName={datasetName} location={location}"
996
+ tmpLog = MsgWrapper(logger, methodName)
997
+ tmpLog.debug("start")
998
+ try:
999
+ # get rucio API
1000
+ client = RucioClient()
1001
+ # get scope and name
1002
+ scope, dsn = self.extract_scope(datasetName)
1003
+ # lifetime
1004
+ if lifetime is not None:
1005
+ lifetime = lifetime * 86400
1006
+ elif "SCRATCHDISK" in location:
1007
+ lifetime = 14 * 86400
1008
+ # get owner
1009
+ if owner is not None:
1010
+ tmpStat, userInfo = self.finger(owner)
1011
+ if tmpStat != self.SC_SUCCEEDED:
1012
+ raise RuntimeError(f"failed to get nickname for {owner}")
1013
+ owner = userInfo["nickname"]
1014
+ else:
1015
+ owner = client.account
1016
+ if grouping is None:
1017
+ grouping = "DATASET"
1018
+ # add rule
1019
+ dids = []
1020
+ did = {"scope": scope, "name": dsn}
1021
+ dids.append(did)
1022
+ locList = location.split(",")
1023
+ for tmpLoc in locList:
1024
+ client.add_replication_rule(
1025
+ dids=dids,
1026
+ copies=copies,
1027
+ rse_expression=tmpLoc,
1028
+ lifetime=lifetime,
1029
+ grouping=grouping,
1030
+ account=owner,
1031
+ locked=False,
1032
+ notify="N",
1033
+ ignore_availability=ignore_availability,
1034
+ activity=activity,
1035
+ weight=weight,
1036
+ )
1037
+ except DuplicateRule:
1038
+ pass
1039
+ except Exception as e:
1040
+ errType = e
1041
+ errCode, errMsg = self.checkError(errType)
1042
+ tmpLog.error(errMsg)
1043
+ return errCode, f"{methodName} : {errMsg}"
1044
+ tmpLog.debug(f"done for owner={owner}")
1045
+ return self.SC_SUCCEEDED, True
1046
+
1047
+ # delete dataset
1048
+ def deleteDataset(self, datasetName, emptyOnly, ignoreUnknown=False):
1049
+ methodName = "deleteDataset"
1050
+ methodName += f" pid={self.pid}"
1051
+ methodName = f"{methodName} datasetName={datasetName}"
1052
+ tmpLog = MsgWrapper(logger, methodName)
1053
+ tmpLog.debug("start")
1054
+ isOK = True
1055
+ retStr = ""
1056
+ nFiles = -1
1057
+ try:
1058
+ # get rucio API
1059
+ client = RucioClient()
1060
+ # get scope and name
1061
+ scope, dsn = self.extract_scope(datasetName)
1062
+ # get the number of files
1063
+ if emptyOnly:
1064
+ nFiles = 0
1065
+ for x in client.list_files(scope, dsn):
1066
+ nFiles += 1
1067
+ # erase
1068
+ if not emptyOnly or nFiles == 0:
1069
+ client.set_metadata(scope=scope, name=dsn, key="lifetime", value=0.0001)
1070
+ retStr = f"deleted {datasetName}"
1071
+ else:
1072
+ retStr = f"keep {datasetName} where {nFiles} files are available"
1073
+ except DataIdentifierNotFound as e:
1074
+ errType = e
1075
+ if ignoreUnknown:
1076
+ pass
1077
+ else:
1078
+ isOK = False
1079
+ except Exception as e:
1080
+ isOK = False
1081
+ errType = e
1082
+ if isOK:
1083
+ tmpLog.debug("done")
1084
+ return self.SC_SUCCEEDED, retStr
1085
+ else:
1086
+ errCode, errMsg = self.checkError(errType)
1087
+ tmpLog.error(errMsg)
1088
+ return errCode, f"{methodName} : {errMsg}"
1089
+
1090
+ # register subscription
1091
+ def registerDatasetSubscription(self, datasetName, location, activity, lifetime=None, asynchronous=False):
1092
+ methodName = "registerDatasetSubscription"
1093
+ methodName += f" pid={self.pid}"
1094
+ methodName = f"{methodName} datasetName={datasetName} location={location} activity={activity} asyn={asynchronous}"
1095
+ tmpLog = MsgWrapper(logger, methodName)
1096
+ tmpLog.debug("start")
1097
+ isOK = True
1098
+ try:
1099
+ if lifetime is not None:
1100
+ lifetime = lifetime * 24 * 60 * 60
1101
+ # get rucio API
1102
+ client = RucioClient()
1103
+ # get scope and name
1104
+ scope, dsn = self.extract_scope(datasetName)
1105
+ dids = [{"scope": scope, "name": dsn}]
1106
+ # check if a replication rule already exists
1107
+ for rule in client.list_did_rules(scope=scope, name=dsn):
1108
+ if (rule["rse_expression"] == location) and (rule["account"] == client.account):
1109
+ return True
1110
+ client.add_replication_rule(
1111
+ dids=dids,
1112
+ copies=1,
1113
+ rse_expression=location,
1114
+ weight=None,
1115
+ lifetime=lifetime,
1116
+ grouping="DATASET",
1117
+ account=client.account,
1118
+ locked=False,
1119
+ notify="N",
1120
+ ignore_availability=True,
1121
+ activity=activity,
1122
+ asynchronous=asynchronous,
1123
+ )
1124
+ except DuplicateRule:
1125
+ pass
1126
+ except DataIdentifierNotFound:
1127
+ pass
1128
+ except Exception as e:
1129
+ isOK = False
1130
+ errType = e
1131
+ if not isOK:
1132
+ errCode, errMsg = self.checkError(errType)
1133
+ tmpLog.error(errMsg)
1134
+ return errCode, f"{methodName} : {errMsg}"
1135
+ tmpLog.debug("done")
1136
+ return self.SC_SUCCEEDED, True
1137
+
1138
+ # convert output of listDatasetReplicas
1139
+ def convertOutListDatasetReplicas(self, datasetName, usefileLookup=False, use_vp=False, skip_incomplete_element=False):
1140
+ retMap = {}
1141
+ # get rucio API
1142
+ client = RucioClient()
1143
+ # get scope and name
1144
+ scope, dsn = self.extract_scope(datasetName)
1145
+ # get replicas
1146
+ itr = client.list_dataset_replicas(scope, dsn, deep=usefileLookup)
1147
+ items = []
1148
+ for item in itr:
1149
+ if "vp" not in item:
1150
+ item["vp"] = False
1151
+ items.append(item)
1152
+ # deep lookup if shallow gave nothing
1153
+ if items == [] and not usefileLookup:
1154
+ itr = client.list_dataset_replicas(scope, dsn, deep=True)
1155
+ for item in itr:
1156
+ if "vp" not in item:
1157
+ item["vp"] = False
1158
+ items.append(item)
1159
+ # VP
1160
+ if use_vp:
1161
+ itr = client.list_dataset_replicas_vp(scope, dsn)
1162
+ for item in itr:
1163
+ if item["vp"]:
1164
+ # add dummy
1165
+ if "length" not in item:
1166
+ item["length"] = 1
1167
+ if "available_length" not in item:
1168
+ item["available_length"] = 1
1169
+ if "bytes" not in item:
1170
+ item["bytes"] = 1
1171
+ if "available_bytes" not in item:
1172
+ item["available_bytes"] = 1
1173
+ if "site" in item and "rse" not in item:
1174
+ item["rse"] = item["site"]
1175
+ items.append(item)
1176
+ # loop over all RSEs
1177
+ for item in items:
1178
+ rse = item["rse"]
1179
+ if skip_incomplete_element and (not item["available_length"] or item["length"] != item["available_length"]):
1180
+ continue
1181
+ retMap[rse] = [
1182
+ {
1183
+ "total": item["length"],
1184
+ "found": item["available_length"],
1185
+ "tsize": item["bytes"],
1186
+ "asize": item["available_bytes"],
1187
+ "vp": item["vp"],
1188
+ "immutable": 1,
1189
+ }
1190
+ ]
1191
+ return retMap
1192
+
1193
+ # delete files from dataset
1194
+ def deleteFilesFromDataset(self, datasetName, filesToDelete):
1195
+ methodName = "deleteFilesFromDataset"
1196
+ methodName += f" pid={self.pid}"
1197
+ methodName += f" <datasetName={datasetName}>"
1198
+ tmpLog = MsgWrapper(logger, methodName)
1199
+ tmpLog.debug("start")
1200
+ isOK = True
1201
+ try:
1202
+ # get rucio API
1203
+ client = RucioClient()
1204
+ # get scope and name
1205
+ scope, dsn = self.extract_scope(datasetName)
1206
+ # open dataset
1207
+ try:
1208
+ client.set_status(scope, dsn, open=True)
1209
+ except UnsupportedOperation:
1210
+ pass
1211
+ # exec
1212
+ client.detach_dids(scope=scope, name=dsn, dids=filesToDelete)
1213
+ except Exception as e:
1214
+ isOK = False
1215
+ errType = e
1216
+ if not isOK:
1217
+ errCode, errMsg = self.checkError(errType)
1218
+ tmpLog.error(errMsg)
1219
+ return errCode, f"{methodName} : {errMsg}"
1220
+ tmpLog.debug("done")
1221
+ return self.SC_SUCCEEDED, True
1222
+
1223
+ # extract scope
1224
+ def extract_scope(self, dsn):
1225
+ if dsn.endswith("/"):
1226
+ dsn = re.sub("/$", "", dsn)
1227
+ if ":" in dsn:
1228
+ return dsn.split(":")[:2]
1229
+ scope = dsn.split(".")[0]
1230
+ if dsn.startswith("user") or dsn.startswith("group"):
1231
+ scope = ".".join(dsn.split(".")[0:2])
1232
+ return scope, dsn
1233
+
1234
+ # get DID string as scope:name
1235
+ def get_did_str(self, raw_name):
1236
+ scope, name = self.extract_scope(raw_name)
1237
+ return f"{scope}:{name}"
1238
+
1239
+ # open dataset
1240
+ def openDataset(self, datasetName):
1241
+ methodName = "openDataset"
1242
+ methodName += f" pid={self.pid}"
1243
+ methodName += f" <datasetName={datasetName}>"
1244
+ tmpLog = MsgWrapper(logger, methodName)
1245
+ tmpLog.debug("start")
1246
+ isOK = True
1247
+ try:
1248
+ # get rucio API
1249
+ client = RucioClient()
1250
+ # get scope and name
1251
+ scope, dsn = self.extract_scope(datasetName)
1252
+ # open dataset
1253
+ try:
1254
+ client.set_status(scope, dsn, open=True)
1255
+ except (UnsupportedOperation, DataIdentifierNotFound):
1256
+ pass
1257
+ except Exception as e:
1258
+ isOK = False
1259
+ errType = e
1260
+ if not isOK:
1261
+ errCode, errMsg = self.checkError(errType)
1262
+ tmpLog.error(errMsg)
1263
+ return errCode, f"{methodName} : {errMsg}"
1264
+ tmpLog.debug("done")
1265
+ return self.SC_SUCCEEDED, True
1266
+
1267
+ # update endpoint dict
1268
+ def updateEndPointDict(self):
1269
+ methodName = "updateEndPointDict"
1270
+ methodName += f" pid={self.pid}"
1271
+ tmpLog = MsgWrapper(logger, methodName)
1272
+ # check freshness
1273
+ timeNow = naive_utcnow()
1274
+ if self.lastUpdateEP is not None and timeNow - self.lastUpdateEP < self.timeIntervalEP:
1275
+ return
1276
+ self.lastUpdateEP = timeNow
1277
+ # get json
1278
+ try:
1279
+ tmpLog.debug("start")
1280
+ if hasattr(jedi_config.ddm, "endpoints_json_path"):
1281
+ tmp_path = jedi_config.ddm.endpoints_json_path
1282
+ else:
1283
+ tmp_path = "/cvmfs/atlas.cern.ch/repo/sw/local/etc/cric_ddmendpoints.json"
1284
+ if tmp_path.startswith("http"):
1285
+ ddd = requests.get(tmp_path, verify=False).json()
1286
+ else:
1287
+ with open(tmp_path) as f:
1288
+ ddd = json.load(f)
1289
+ self.endPointDict = {k: ddd[k] for k in ddd if ddd[k]["state"] == "ACTIVE"}
1290
+ tmpLog.debug(f"got {len(self.endPointDict)} endpoints ")
1291
+ except Exception as e:
1292
+ errStr = f"failed to update EP with {str(e)}"
1293
+ tmpLog.error(errStr)
1294
+ return
1295
+
1296
+ # check if the dataset is distributed
1297
+ def isDistributedDataset(self, dataset_name: str) -> tuple[Any, bool | str]:
1298
+ """
1299
+ Check if the dataset/container is distributed
1300
+ :param dataset_name: dataset name
1301
+ :return: status, is_distributed (True if distributed, error message if failed)
1302
+ """
1303
+ method_name = "isDistributedDataset"
1304
+ method_name += f" pid={self.pid}"
1305
+ method_name += f" <datasetName={dataset_name}>"
1306
+ tmp_log = MsgWrapper(logger, method_name)
1307
+ tmp_log.debug("start")
1308
+ is_distributed = True
1309
+ is_ok = True
1310
+ try:
1311
+ # get rucio API
1312
+ client = RucioClient()
1313
+ # get scope and name
1314
+ scope, dsn = self.extract_scope(dataset_name)
1315
+ # get metadata
1316
+ if dsn.endswith("/"):
1317
+ dsn = dsn[:-1]
1318
+ tmp_ret = client.get_metadata(scope, dsn)
1319
+ did_type = tmp_ret["did_type"]
1320
+ # get rules
1321
+ for rule in client.list_did_rules(scope, dsn):
1322
+ if rule["grouping"] == "NONE":
1323
+ continue
1324
+ elif rule["grouping"] == "DATASET" and did_type == "CONTAINER":
1325
+ continue
1326
+ else:
1327
+ is_distributed = False
1328
+ break
1329
+ except DataIdentifierNotFound:
1330
+ # ignore missing dataset
1331
+ pass
1332
+ except Exception as e:
1333
+ is_ok = False
1334
+ err_type = e
1335
+ if not is_ok:
1336
+ err_code, err_msg = self.checkError(err_type)
1337
+ tmp_log.error(err_msg)
1338
+ return err_code, f"{method_name} : {err_msg}"
1339
+ tmp_log.debug(f"done with {is_distributed}")
1340
+ return self.SC_SUCCEEDED, is_distributed
1341
+
1342
+ # update replication rules
1343
+ def updateReplicationRules(self, datasetName, dataMap):
1344
+ methodName = "updateReplicationRules"
1345
+ methodName += f" pid={self.pid}"
1346
+ methodName = f"{methodName} datasetName={datasetName}"
1347
+ tmpLog = MsgWrapper(logger, methodName)
1348
+ tmpLog.debug("start")
1349
+ isOK = True
1350
+ try:
1351
+ # get rucio API
1352
+ client = RucioClient()
1353
+ # get scope and name
1354
+ scope, dsn = self.extract_scope(datasetName)
1355
+ # get rules
1356
+ for rule in client.list_did_rules(scope=scope, name=dsn):
1357
+ for dataKey, data in dataMap.items():
1358
+ if rule["rse_expression"] == dataKey or re.search(dataKey, rule["rse_expression"]) is not None:
1359
+ tmpLog.debug(f"set data={str(data)} on {rule['rse_expression']}")
1360
+ client.update_replication_rule(rule["id"], data)
1361
+ break
1362
+ except DataIdentifierNotFound:
1363
+ pass
1364
+ except Exception as e:
1365
+ isOK = False
1366
+ errType = e
1367
+ if not isOK:
1368
+ errCode, errMsg = self.checkError(errType)
1369
+ tmpLog.error(errMsg)
1370
+ return errCode, f"{methodName} : {errMsg}"
1371
+ tmpLog.debug("done")
1372
+ return self.SC_SUCCEEDED, True
1373
+
1374
+ # get active staging rule
1375
+ def getActiveStagingRule(self, dataset_name):
1376
+ methodName = "getActiveStagingRule"
1377
+ methodName += f" datasetName={dataset_name}"
1378
+ tmpLog = MsgWrapper(logger, methodName)
1379
+ tmpLog.debug("start")
1380
+ ruleID = None
1381
+ try:
1382
+ # get rucio API
1383
+ client = RucioClient()
1384
+ # get scope and name
1385
+ scope, dsn = self.extract_scope(dataset_name)
1386
+ # get rules
1387
+ for rule in client.list_did_rules(scope=scope, name=dsn):
1388
+ if rule["activity"] == "Staging":
1389
+ ruleID = rule["id"]
1390
+ break
1391
+ except Exception as e:
1392
+ errType = e
1393
+ errCode, errMsg = self.checkError(errType)
1394
+ tmpLog.error(errMsg)
1395
+ return errCode, f"{methodName} : {errMsg}"
1396
+ tmpLog.debug(f"got ruleID={ruleID}")
1397
+ return self.SC_SUCCEEDED, ruleID
1398
+
1399
+ # check global quota
1400
+ def check_global_quota(self, user_name: str) -> tuple[StatusCode, tuple[bool, bool | None, str | None]]:
1401
+ """
1402
+ Check if the user has exceeded the global quota or is close to the limit.
1403
+
1404
+ :param user_name: Username to check the quota for.
1405
+ :return: A tuple of (StatusCode, (is_quota_ok, is_near_limit, error_message))
1406
+ """
1407
+ method_name = "check_global_quota"
1408
+ method_name += f" pid={self.pid}"
1409
+ method_name = f"{method_name} userName={user_name}"
1410
+ tmp_log = MsgWrapper(logger, method_name)
1411
+ tmp_log.debug("start")
1412
+ is_quota_ok = True
1413
+ is_near_limit = False
1414
+ err_msg = None
1415
+ try:
1416
+ # get rucio API
1417
+ client = RucioClient()
1418
+ tmp_stat, user_info = self.finger(user_name)
1419
+ if tmp_stat != self.SC_SUCCEEDED:
1420
+ is_quota_ok = False
1421
+ err_msg = "failed to get nickname"
1422
+ else:
1423
+ owner = user_info["nickname"]
1424
+ quota_info = client.get_global_account_usage(owner)
1425
+ for info in quota_info:
1426
+ usage_in_tb = math.ceil(info["bytes"] / (1000**4))
1427
+ limit_in_tb = int(info["bytes_limit"] / (1000**4))
1428
+ if info["bytes"] > info["bytes_limit"] > 0:
1429
+ is_quota_ok = False
1430
+ rse_expression = info["rse_expression"]
1431
+ err_msg = f"exceeded global quota on {rse_expression} (Usage:{usage_in_tb} > Limit:{limit_in_tb} in TB). see output from 'rucio list-account-usage {owner}'"
1432
+ break
1433
+ limit_value = 0.9
1434
+ if info["bytes"] > info["bytes_limit"] * limit_value > 0:
1435
+ is_near_limit = True
1436
+ rse_expression = info["rse_expression"]
1437
+ err_msg = f"close to global quota limit on {rse_expression} (Usage:{usage_in_tb} / Limit:{limit_in_tb} in TB > {int(limit_value*100)}%). see output from 'rucio list-account-usage {owner}'"
1438
+ break
1439
+ except Exception as e:
1440
+ err_msg = f"failed to get global quota info with {str(e)}"
1441
+ tmp_log.error(err_msg)
1442
+ is_quota_ok = False
1443
+ tmp_log.debug(f"done: is_quota_ok={is_quota_ok} is_near_limit={is_near_limit} err_msg={err_msg}")
1444
+ return self.SC_SUCCEEDED, (is_quota_ok, is_near_limit, err_msg)
1445
+
1446
+ # get endpoints over local quota for a user
1447
+ def get_endpoints_over_local_quota(self, user_name: str) -> tuple[StatusCode, tuple[bool, set]]:
1448
+ """
1449
+ Get a list of endpoints where the user exceeds local quota.
1450
+
1451
+ :param user_name: Username to check the quota for.
1452
+ :return: A tuple of (StatusCode, (is_ok, endpoints))
1453
+ """
1454
+ method_name = "get_endpoints_over_local_quota"
1455
+ method_name += f" pid={self.pid}"
1456
+ method_name = f"{method_name} userName={user_name}"
1457
+ tmp_log = MsgWrapper(logger, method_name)
1458
+ tmp_log.debug("start")
1459
+ is_ok = True
1460
+ full_endpoints = set()
1461
+ try:
1462
+ # get rucio API
1463
+ client = RucioClient()
1464
+ tmp_stat, user_info = self.finger(user_name)
1465
+ if tmp_stat != self.SC_SUCCEEDED:
1466
+ is_ok = False
1467
+ err_msg = "failed to get nickname"
1468
+ else:
1469
+ owner = user_info["nickname"]
1470
+ quota_info = client.get_local_account_usage(owner)
1471
+ for info in quota_info:
1472
+ if info["bytes"] >= info["bytes_limit"] > 0:
1473
+ full_endpoints.add(info["rse"])
1474
+ except Exception as e:
1475
+ err_msg = f"failed to get local quota info with {str(e)}"
1476
+ tmp_log.error(err_msg)
1477
+ is_ok = False
1478
+ tmp_log.debug(f"done: is_ok={is_ok} endpoints={','.join(full_endpoints)}")
1479
+ return self.SC_SUCCEEDED, (is_ok, full_endpoints)
1480
+
1481
+ # make staging rule
1482
+ def make_staging_rule(self, dataset_name, expression, activity, lifetime=None, weight=None, notify="N", source_replica_expression=None):
1483
+ methodName = "make_staging_rule"
1484
+ methodName += f" pid={self.pid}"
1485
+ methodName = f"{methodName} datasetName={dataset_name} expression={expression} activity={activity} lifetime={lifetime}"
1486
+ tmpLog = MsgWrapper(logger, methodName)
1487
+ tmpLog.debug("start")
1488
+ isOK = True
1489
+ ruleID = None
1490
+ try:
1491
+ if lifetime is not None:
1492
+ lifetime = lifetime * 24 * 60 * 60
1493
+ # get rucio API
1494
+ client = RucioClient()
1495
+ # get scope and name
1496
+ scope, dsn = self.extract_scope(dataset_name)
1497
+ # check if a replication rule already exists
1498
+ if ruleID is None:
1499
+ dids = [{"scope": scope, "name": dsn}]
1500
+ for rule in client.list_did_rules(scope=scope, name=dsn):
1501
+ if rule["rse_expression"] == expression and rule["account"] == client.account and rule["activity"] == activity:
1502
+ ruleID = rule["id"]
1503
+ tmpLog.debug(f"rule already exists: ID={ruleID}")
1504
+ break
1505
+ # make new rule
1506
+ if ruleID is None:
1507
+ rule = client.add_replication_rule(
1508
+ dids=dids,
1509
+ copies=1,
1510
+ rse_expression=expression,
1511
+ weight=weight,
1512
+ lifetime=lifetime,
1513
+ grouping="DATASET",
1514
+ account=client.account,
1515
+ locked=False,
1516
+ notify=notify,
1517
+ ignore_availability=False,
1518
+ activity=activity,
1519
+ asynchronous=False,
1520
+ source_replica_expression=source_replica_expression,
1521
+ )
1522
+ ruleID = rule["id"]
1523
+ tmpLog.debug(f"made new rule : ID={ruleID}")
1524
+ except Exception as e:
1525
+ errMsg = f"failed to make staging rule with {str(e)}"
1526
+ tmpLog.error(errMsg + traceback.format_exc())
1527
+ isOK = False
1528
+ errType = e
1529
+ if not isOK:
1530
+ errCode, errMsg = self.checkError(errType)
1531
+ tmpLog.error(errMsg)
1532
+ return errCode, f"{methodName} : {errMsg}"
1533
+ tmpLog.debug("done")
1534
+ return self.SC_SUCCEEDED, ruleID
1535
+
1536
+ # get state of all rules of a dataset
1537
+ def get_rules_state(self, dataset_name):
1538
+ methodName = "get_rules_state"
1539
+ methodName += f" datasetName={dataset_name}"
1540
+ tmpLog = MsgWrapper(logger, methodName)
1541
+ tmpLog.debug("start")
1542
+ res_dict = {}
1543
+ all_ok = False
1544
+ try:
1545
+ # get rucio API
1546
+ client = RucioClient()
1547
+ # get scope and name
1548
+ scope, dsn = self.extract_scope(dataset_name)
1549
+ # get rules and state
1550
+ for rule in client.list_did_rules(scope=scope, name=dsn):
1551
+ rule_id = rule["id"]
1552
+ res_dict[rule_id] = {"state": rule["state"], "rse_expression": rule["rse_expression"]}
1553
+ # if all ok
1554
+ if all((x == "OK" for x in res_dict.values())):
1555
+ all_ok = True
1556
+ except Exception as e:
1557
+ errType = e
1558
+ errCode, errMsg = self.checkError(errType)
1559
+ tmpLog.error(errMsg)
1560
+ return errCode, f"{methodName} : {errMsg}"
1561
+ tmpLog.debug(f"got {all_ok}, {res_dict}")
1562
+ return self.SC_SUCCEEDED, (all_ok, res_dict)
1563
+
1564
+ # list DID rules
1565
+ def list_did_rules(self, dataset_name, all_accounts=False):
1566
+ methodName = "list_did_rules"
1567
+ methodName += f" datasetName={dataset_name} all_accounts={all_accounts}"
1568
+ tmpLog = MsgWrapper(logger, methodName)
1569
+ tmpLog.debug("start")
1570
+ ret_list = []
1571
+ try:
1572
+ # get rucio API
1573
+ client = RucioClient()
1574
+ # get scope and name
1575
+ scope, dsn = self.extract_scope(dataset_name)
1576
+ # get rules
1577
+ for rule in client.list_did_rules(scope=scope, name=dsn):
1578
+ if rule["account"] == client.account or all_accounts:
1579
+ ret_list.append(rule)
1580
+ except Exception as e:
1581
+ errType = e
1582
+ errCode, errMsg = self.checkError(errType)
1583
+ tmpLog.error(errMsg)
1584
+ return errCode, f"{methodName} : {errMsg}"
1585
+ tmpLog.debug(f"got {len(ret_list)} rules")
1586
+ return self.SC_SUCCEEDED, ret_list
1587
+
1588
+ # update replication rule by rule ID
1589
+ def update_rule_by_id(self, rule_id, set_map):
1590
+ methodName = "update_rule_by_id"
1591
+ methodName += f" pid={self.pid}"
1592
+ methodName = f"{methodName} rule_id={rule_id}"
1593
+ tmpLog = MsgWrapper(logger, methodName)
1594
+ tmpLog.debug("start")
1595
+ isOK = True
1596
+ try:
1597
+ # get rucio API
1598
+ client = RucioClient()
1599
+ # update rule
1600
+ client.update_replication_rule(rule_id, set_map)
1601
+ except Exception as e:
1602
+ isOK = False
1603
+ errType = e
1604
+ if not isOK:
1605
+ errCode, errMsg = self.checkError(errType)
1606
+ tmpLog.error(errMsg)
1607
+ return errCode, f"{methodName} : {errMsg}"
1608
+ tmpLog.debug("done")
1609
+ return self.SC_SUCCEEDED, True
1610
+
1611
+ # get replication rule by rule ID
1612
+ def get_rule_by_id(self, rule_id, allow_missing=True):
1613
+ methodName = "get_rule_by_id"
1614
+ methodName += f" pid={self.pid}"
1615
+ methodName = f"{methodName} rule_id={rule_id}"
1616
+ tmpLog = MsgWrapper(logger, methodName)
1617
+ tmpLog.debug("start")
1618
+ try:
1619
+ # get rucio API
1620
+ client = RucioClient()
1621
+ # get rules
1622
+ rule = client.get_replication_rule(rule_id)
1623
+ except RuleNotFound as e:
1624
+ errType = e
1625
+ errCode, errMsg = self.checkError(errType)
1626
+ if allow_missing:
1627
+ tmpLog.debug(errMsg)
1628
+ return self.SC_SUCCEEDED, False
1629
+ except Exception as e:
1630
+ errType = e
1631
+ errCode, errMsg = self.checkError(errType)
1632
+ tmpLog.error(errMsg)
1633
+ return errCode, f"{methodName} : {errMsg}"
1634
+ tmpLog.debug(f"got rule")
1635
+ return self.SC_SUCCEEDED, rule
1636
+
1637
+ # list details of all replica locks for a rule by rule ID
1638
+ def list_replica_locks_by_id(self, rule_id):
1639
+ methodName = "list_replica_locks_by_id"
1640
+ methodName += f" pid={self.pid}"
1641
+ methodName = f"{methodName} rule_id={rule_id}"
1642
+ tmpLog = MsgWrapper(logger, methodName)
1643
+ tmpLog.debug("start")
1644
+ try:
1645
+ # get rucio API
1646
+ client = RucioClient()
1647
+ # get generator of replica locks
1648
+ res = client.list_replica_locks(rule_id)
1649
+ # turn into list
1650
+ ret = list(res)
1651
+ except Exception as e:
1652
+ errType = e
1653
+ errCode, errMsg = self.checkError(errType)
1654
+ tmpLog.error(errMsg)
1655
+ return errCode, f"{methodName} : {errMsg}"
1656
+ tmpLog.debug(f"got replica locks")
1657
+ return self.SC_SUCCEEDED, ret
1658
+
1659
+ # delete replication rule by rule ID
1660
+ def delete_replication_rule(self, rule_id, allow_missing=True):
1661
+ methodName = "delete_replication_rule"
1662
+ methodName += f" pid={self.pid}"
1663
+ methodName = f"{methodName} rule_id={rule_id}"
1664
+ tmpLog = MsgWrapper(logger, methodName)
1665
+ tmpLog.debug("start")
1666
+ try:
1667
+ # get rucio API
1668
+ client = RucioClient()
1669
+ # get rules
1670
+ ret = client.delete_replication_rule(rule_id)
1671
+ except RuleNotFound as e:
1672
+ errType = e
1673
+ errCode, errMsg = self.checkError(errType)
1674
+ if allow_missing:
1675
+ tmpLog.debug(errMsg)
1676
+ return self.SC_SUCCEEDED, False
1677
+ except Exception as e:
1678
+ errType = e
1679
+ errCode, errMsg = self.checkError(errType)
1680
+ tmpLog.error(errMsg)
1681
+ return errCode, f"{methodName} : {errMsg}"
1682
+ tmpLog.debug(f"deleted, return {ret}")
1683
+ return self.SC_SUCCEEDED, ret
1684
+
1685
+ # check endpoint
1686
+ def check_endpoint(self, rse):
1687
+ method_name = "check_endpoint"
1688
+ method_name = f"{method_name} rse={rse}"
1689
+ tmp_log = MsgWrapper(logger, method_name)
1690
+ tmp_log.debug("start")
1691
+ try:
1692
+ # get rucio API
1693
+ client = RucioClient()
1694
+ # get rules
1695
+ ret = client.get_rse(rse)
1696
+ # check availability for write
1697
+ if ret.get("availability_write") is False:
1698
+ result = False, f"{rse} unavailable for write"
1699
+ else:
1700
+ result = True, None
1701
+ except RSENotFound:
1702
+ # unknown endpoint
1703
+ result = False, f"unknown endpoint: {rse}"
1704
+ except Exception as e:
1705
+ # unknown error
1706
+ result = None, f"failed with {str(e)}"
1707
+ tmp_log.debug(f"done with {result}")
1708
+ return self.SC_SUCCEEDED, result