rucio 32.8.6__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.

Potentially problematic release.


This version of rucio might be problematic. Click here for more details.

Files changed (481) hide show
  1. rucio/__init__.py +18 -0
  2. rucio/alembicrevision.py +16 -0
  3. rucio/api/__init__.py +14 -0
  4. rucio/api/account.py +266 -0
  5. rucio/api/account_limit.py +287 -0
  6. rucio/api/authentication.py +302 -0
  7. rucio/api/config.py +218 -0
  8. rucio/api/credential.py +60 -0
  9. rucio/api/did.py +726 -0
  10. rucio/api/dirac.py +71 -0
  11. rucio/api/exporter.py +60 -0
  12. rucio/api/heartbeat.py +62 -0
  13. rucio/api/identity.py +160 -0
  14. rucio/api/importer.py +46 -0
  15. rucio/api/lifetime_exception.py +95 -0
  16. rucio/api/lock.py +131 -0
  17. rucio/api/meta.py +85 -0
  18. rucio/api/permission.py +72 -0
  19. rucio/api/quarantined_replica.py +69 -0
  20. rucio/api/replica.py +528 -0
  21. rucio/api/request.py +220 -0
  22. rucio/api/rse.py +601 -0
  23. rucio/api/rule.py +335 -0
  24. rucio/api/scope.py +89 -0
  25. rucio/api/subscription.py +255 -0
  26. rucio/api/temporary_did.py +49 -0
  27. rucio/api/vo.py +112 -0
  28. rucio/client/__init__.py +16 -0
  29. rucio/client/accountclient.py +413 -0
  30. rucio/client/accountlimitclient.py +155 -0
  31. rucio/client/baseclient.py +929 -0
  32. rucio/client/client.py +77 -0
  33. rucio/client/configclient.py +113 -0
  34. rucio/client/credentialclient.py +54 -0
  35. rucio/client/didclient.py +691 -0
  36. rucio/client/diracclient.py +48 -0
  37. rucio/client/downloadclient.py +1674 -0
  38. rucio/client/exportclient.py +44 -0
  39. rucio/client/fileclient.py +51 -0
  40. rucio/client/importclient.py +42 -0
  41. rucio/client/lifetimeclient.py +74 -0
  42. rucio/client/lockclient.py +99 -0
  43. rucio/client/metaclient.py +137 -0
  44. rucio/client/pingclient.py +45 -0
  45. rucio/client/replicaclient.py +444 -0
  46. rucio/client/requestclient.py +109 -0
  47. rucio/client/rseclient.py +664 -0
  48. rucio/client/ruleclient.py +287 -0
  49. rucio/client/scopeclient.py +88 -0
  50. rucio/client/subscriptionclient.py +161 -0
  51. rucio/client/touchclient.py +78 -0
  52. rucio/client/uploadclient.py +871 -0
  53. rucio/common/__init__.py +14 -0
  54. rucio/common/cache.py +74 -0
  55. rucio/common/config.py +796 -0
  56. rucio/common/constants.py +92 -0
  57. rucio/common/constraints.py +18 -0
  58. rucio/common/didtype.py +187 -0
  59. rucio/common/dumper/__init__.py +306 -0
  60. rucio/common/dumper/consistency.py +449 -0
  61. rucio/common/dumper/data_models.py +325 -0
  62. rucio/common/dumper/path_parsing.py +65 -0
  63. rucio/common/exception.py +1092 -0
  64. rucio/common/extra.py +37 -0
  65. rucio/common/logging.py +404 -0
  66. rucio/common/pcache.py +1387 -0
  67. rucio/common/policy.py +84 -0
  68. rucio/common/schema/__init__.py +143 -0
  69. rucio/common/schema/atlas.py +411 -0
  70. rucio/common/schema/belleii.py +406 -0
  71. rucio/common/schema/cms.py +478 -0
  72. rucio/common/schema/domatpc.py +399 -0
  73. rucio/common/schema/escape.py +424 -0
  74. rucio/common/schema/generic.py +431 -0
  75. rucio/common/schema/generic_multi_vo.py +410 -0
  76. rucio/common/schema/icecube.py +404 -0
  77. rucio/common/schema/lsst.py +423 -0
  78. rucio/common/stomp_utils.py +160 -0
  79. rucio/common/stopwatch.py +56 -0
  80. rucio/common/test_rucio_server.py +148 -0
  81. rucio/common/types.py +158 -0
  82. rucio/common/utils.py +1946 -0
  83. rucio/core/__init__.py +14 -0
  84. rucio/core/account.py +426 -0
  85. rucio/core/account_counter.py +171 -0
  86. rucio/core/account_limit.py +357 -0
  87. rucio/core/authentication.py +563 -0
  88. rucio/core/config.py +386 -0
  89. rucio/core/credential.py +218 -0
  90. rucio/core/did.py +3102 -0
  91. rucio/core/did_meta_plugins/__init__.py +250 -0
  92. rucio/core/did_meta_plugins/did_column_meta.py +326 -0
  93. rucio/core/did_meta_plugins/did_meta_plugin_interface.py +116 -0
  94. rucio/core/did_meta_plugins/filter_engine.py +573 -0
  95. rucio/core/did_meta_plugins/json_meta.py +215 -0
  96. rucio/core/did_meta_plugins/mongo_meta.py +199 -0
  97. rucio/core/did_meta_plugins/postgres_meta.py +317 -0
  98. rucio/core/dirac.py +208 -0
  99. rucio/core/distance.py +164 -0
  100. rucio/core/exporter.py +59 -0
  101. rucio/core/heartbeat.py +263 -0
  102. rucio/core/identity.py +290 -0
  103. rucio/core/importer.py +248 -0
  104. rucio/core/lifetime_exception.py +377 -0
  105. rucio/core/lock.py +474 -0
  106. rucio/core/message.py +241 -0
  107. rucio/core/meta.py +190 -0
  108. rucio/core/monitor.py +441 -0
  109. rucio/core/naming_convention.py +154 -0
  110. rucio/core/nongrid_trace.py +124 -0
  111. rucio/core/oidc.py +1339 -0
  112. rucio/core/permission/__init__.py +107 -0
  113. rucio/core/permission/atlas.py +1333 -0
  114. rucio/core/permission/belleii.py +1076 -0
  115. rucio/core/permission/cms.py +1166 -0
  116. rucio/core/permission/escape.py +1076 -0
  117. rucio/core/permission/generic.py +1128 -0
  118. rucio/core/permission/generic_multi_vo.py +1148 -0
  119. rucio/core/quarantined_replica.py +190 -0
  120. rucio/core/replica.py +3627 -0
  121. rucio/core/replica_sorter.py +368 -0
  122. rucio/core/request.py +2241 -0
  123. rucio/core/rse.py +1835 -0
  124. rucio/core/rse_counter.py +155 -0
  125. rucio/core/rse_expression_parser.py +460 -0
  126. rucio/core/rse_selector.py +277 -0
  127. rucio/core/rule.py +3419 -0
  128. rucio/core/rule_grouping.py +1473 -0
  129. rucio/core/scope.py +152 -0
  130. rucio/core/subscription.py +316 -0
  131. rucio/core/temporary_did.py +188 -0
  132. rucio/core/topology.py +448 -0
  133. rucio/core/trace.py +361 -0
  134. rucio/core/transfer.py +1233 -0
  135. rucio/core/vo.py +151 -0
  136. rucio/core/volatile_replica.py +123 -0
  137. rucio/daemons/__init__.py +14 -0
  138. rucio/daemons/abacus/__init__.py +14 -0
  139. rucio/daemons/abacus/account.py +106 -0
  140. rucio/daemons/abacus/collection_replica.py +113 -0
  141. rucio/daemons/abacus/rse.py +107 -0
  142. rucio/daemons/atropos/__init__.py +14 -0
  143. rucio/daemons/atropos/atropos.py +243 -0
  144. rucio/daemons/auditor/__init__.py +261 -0
  145. rucio/daemons/auditor/hdfs.py +86 -0
  146. rucio/daemons/auditor/srmdumps.py +284 -0
  147. rucio/daemons/automatix/__init__.py +14 -0
  148. rucio/daemons/automatix/automatix.py +281 -0
  149. rucio/daemons/badreplicas/__init__.py +14 -0
  150. rucio/daemons/badreplicas/minos.py +311 -0
  151. rucio/daemons/badreplicas/minos_temporary_expiration.py +173 -0
  152. rucio/daemons/badreplicas/necromancer.py +200 -0
  153. rucio/daemons/bb8/__init__.py +14 -0
  154. rucio/daemons/bb8/bb8.py +356 -0
  155. rucio/daemons/bb8/common.py +762 -0
  156. rucio/daemons/bb8/nuclei_background_rebalance.py +147 -0
  157. rucio/daemons/bb8/t2_background_rebalance.py +146 -0
  158. rucio/daemons/c3po/__init__.py +14 -0
  159. rucio/daemons/c3po/algorithms/__init__.py +14 -0
  160. rucio/daemons/c3po/algorithms/simple.py +131 -0
  161. rucio/daemons/c3po/algorithms/t2_free_space.py +125 -0
  162. rucio/daemons/c3po/algorithms/t2_free_space_only_pop.py +127 -0
  163. rucio/daemons/c3po/algorithms/t2_free_space_only_pop_with_network.py +279 -0
  164. rucio/daemons/c3po/c3po.py +342 -0
  165. rucio/daemons/c3po/collectors/__init__.py +14 -0
  166. rucio/daemons/c3po/collectors/agis.py +108 -0
  167. rucio/daemons/c3po/collectors/free_space.py +62 -0
  168. rucio/daemons/c3po/collectors/jedi_did.py +48 -0
  169. rucio/daemons/c3po/collectors/mock_did.py +46 -0
  170. rucio/daemons/c3po/collectors/network_metrics.py +63 -0
  171. rucio/daemons/c3po/collectors/workload.py +110 -0
  172. rucio/daemons/c3po/utils/__init__.py +14 -0
  173. rucio/daemons/c3po/utils/dataset_cache.py +40 -0
  174. rucio/daemons/c3po/utils/expiring_dataset_cache.py +45 -0
  175. rucio/daemons/c3po/utils/expiring_list.py +63 -0
  176. rucio/daemons/c3po/utils/popularity.py +82 -0
  177. rucio/daemons/c3po/utils/timeseries.py +76 -0
  178. rucio/daemons/cache/__init__.py +14 -0
  179. rucio/daemons/cache/consumer.py +191 -0
  180. rucio/daemons/common.py +391 -0
  181. rucio/daemons/conveyor/__init__.py +14 -0
  182. rucio/daemons/conveyor/common.py +530 -0
  183. rucio/daemons/conveyor/finisher.py +492 -0
  184. rucio/daemons/conveyor/poller.py +372 -0
  185. rucio/daemons/conveyor/preparer.py +198 -0
  186. rucio/daemons/conveyor/receiver.py +206 -0
  187. rucio/daemons/conveyor/stager.py +127 -0
  188. rucio/daemons/conveyor/submitter.py +379 -0
  189. rucio/daemons/conveyor/throttler.py +468 -0
  190. rucio/daemons/follower/__init__.py +14 -0
  191. rucio/daemons/follower/follower.py +97 -0
  192. rucio/daemons/hermes/__init__.py +14 -0
  193. rucio/daemons/hermes/hermes.py +738 -0
  194. rucio/daemons/judge/__init__.py +14 -0
  195. rucio/daemons/judge/cleaner.py +149 -0
  196. rucio/daemons/judge/evaluator.py +172 -0
  197. rucio/daemons/judge/injector.py +154 -0
  198. rucio/daemons/judge/repairer.py +144 -0
  199. rucio/daemons/oauthmanager/__init__.py +14 -0
  200. rucio/daemons/oauthmanager/oauthmanager.py +199 -0
  201. rucio/daemons/reaper/__init__.py +14 -0
  202. rucio/daemons/reaper/dark_reaper.py +272 -0
  203. rucio/daemons/reaper/light_reaper.py +255 -0
  204. rucio/daemons/reaper/reaper.py +701 -0
  205. rucio/daemons/replicarecoverer/__init__.py +14 -0
  206. rucio/daemons/replicarecoverer/suspicious_replica_recoverer.py +487 -0
  207. rucio/daemons/storage/__init__.py +14 -0
  208. rucio/daemons/storage/consistency/__init__.py +14 -0
  209. rucio/daemons/storage/consistency/actions.py +753 -0
  210. rucio/daemons/tracer/__init__.py +14 -0
  211. rucio/daemons/tracer/kronos.py +513 -0
  212. rucio/daemons/transmogrifier/__init__.py +14 -0
  213. rucio/daemons/transmogrifier/transmogrifier.py +753 -0
  214. rucio/daemons/undertaker/__init__.py +14 -0
  215. rucio/daemons/undertaker/undertaker.py +137 -0
  216. rucio/db/__init__.py +14 -0
  217. rucio/db/sqla/__init__.py +38 -0
  218. rucio/db/sqla/constants.py +192 -0
  219. rucio/db/sqla/migrate_repo/__init__.py +14 -0
  220. rucio/db/sqla/migrate_repo/env.py +111 -0
  221. rucio/db/sqla/migrate_repo/versions/01eaf73ab656_add_new_rule_notification_state_progress.py +71 -0
  222. rucio/db/sqla/migrate_repo/versions/0437a40dbfd1_add_eol_at_in_rules.py +50 -0
  223. rucio/db/sqla/migrate_repo/versions/0f1adb7a599a_create_transfer_hops_table.py +61 -0
  224. rucio/db/sqla/migrate_repo/versions/102efcf145f4_added_stuck_at_column_to_rules.py +46 -0
  225. rucio/db/sqla/migrate_repo/versions/13d4f70c66a9_introduce_transfer_limits.py +93 -0
  226. rucio/db/sqla/migrate_repo/versions/140fef722e91_cleanup_distances_table.py +78 -0
  227. rucio/db/sqla/migrate_repo/versions/14ec5aeb64cf_add_request_external_host.py +46 -0
  228. rucio/db/sqla/migrate_repo/versions/156fb5b5a14_add_request_type_to_requests_idx.py +53 -0
  229. rucio/db/sqla/migrate_repo/versions/1677d4d803c8_split_rse_availability_into_multiple.py +69 -0
  230. rucio/db/sqla/migrate_repo/versions/16a0aca82e12_create_index_on_table_replicas_path.py +42 -0
  231. rucio/db/sqla/migrate_repo/versions/1803333ac20f_adding_provenance_and_phys_group.py +46 -0
  232. rucio/db/sqla/migrate_repo/versions/1a29d6a9504c_add_didtype_chck_to_requests.py +61 -0
  233. rucio/db/sqla/migrate_repo/versions/1a80adff031a_create_index_on_rules_hist_recent.py +42 -0
  234. rucio/db/sqla/migrate_repo/versions/1c45d9730ca6_increase_identity_length.py +141 -0
  235. rucio/db/sqla/migrate_repo/versions/1d1215494e95_add_quarantined_replicas_table.py +75 -0
  236. rucio/db/sqla/migrate_repo/versions/1d96f484df21_asynchronous_rules_and_rule_approval.py +75 -0
  237. rucio/db/sqla/migrate_repo/versions/1f46c5f240ac_add_bytes_column_to_bad_replicas.py +46 -0
  238. rucio/db/sqla/migrate_repo/versions/1fc15ab60d43_add_message_history_table.py +51 -0
  239. rucio/db/sqla/migrate_repo/versions/2190e703eb6e_move_rse_settings_to_rse_attributes.py +135 -0
  240. rucio/db/sqla/migrate_repo/versions/21d6b9dc9961_add_mismatch_scheme_state_to_requests.py +65 -0
  241. rucio/db/sqla/migrate_repo/versions/22cf51430c78_add_availability_column_to_table_rses.py +42 -0
  242. rucio/db/sqla/migrate_repo/versions/22d887e4ec0a_create_sources_table.py +66 -0
  243. rucio/db/sqla/migrate_repo/versions/25821a8a45a3_remove_unique_constraint_on_requests.py +54 -0
  244. rucio/db/sqla/migrate_repo/versions/25fc855625cf_added_unique_constraint_to_rules.py +43 -0
  245. rucio/db/sqla/migrate_repo/versions/269fee20dee9_add_repair_cnt_to_locks.py +46 -0
  246. rucio/db/sqla/migrate_repo/versions/271a46ea6244_add_ignore_availability_column_to_rules.py +47 -0
  247. rucio/db/sqla/migrate_repo/versions/277b5fbb41d3_switch_heartbeats_executable.py +54 -0
  248. rucio/db/sqla/migrate_repo/versions/27e3a68927fb_remove_replicas_tombstone_and_replicas_.py +39 -0
  249. rucio/db/sqla/migrate_repo/versions/2854cd9e168_added_rule_id_column.py +48 -0
  250. rucio/db/sqla/migrate_repo/versions/295289b5a800_processed_by_and__at_in_requests.py +47 -0
  251. rucio/db/sqla/migrate_repo/versions/2962ece31cf4_add_nbaccesses_column_in_the_did_table.py +48 -0
  252. rucio/db/sqla/migrate_repo/versions/2af3291ec4c_added_replicas_history_table.py +59 -0
  253. rucio/db/sqla/migrate_repo/versions/2b69addda658_add_columns_for_third_party_copy_read_.py +47 -0
  254. rucio/db/sqla/migrate_repo/versions/2b8e7bcb4783_add_config_table.py +72 -0
  255. rucio/db/sqla/migrate_repo/versions/2ba5229cb54c_add_submitted_at_to_requests_table.py +46 -0
  256. rucio/db/sqla/migrate_repo/versions/2cbee484dcf9_added_column_volume_to_rse_transfer_.py +45 -0
  257. rucio/db/sqla/migrate_repo/versions/2edee4a83846_add_source_to_requests_and_requests_.py +48 -0
  258. rucio/db/sqla/migrate_repo/versions/2eef46be23d4_change_tokens_pk.py +48 -0
  259. rucio/db/sqla/migrate_repo/versions/2f648fc909f3_index_in_rule_history_on_scope_name.py +42 -0
  260. rucio/db/sqla/migrate_repo/versions/3082b8cef557_add_naming_convention_table_and_closed_.py +69 -0
  261. rucio/db/sqla/migrate_repo/versions/30fa38b6434e_add_index_on_service_column_in_the_message_table.py +46 -0
  262. rucio/db/sqla/migrate_repo/versions/3152492b110b_added_staging_area_column.py +78 -0
  263. rucio/db/sqla/migrate_repo/versions/32c7d2783f7e_create_bad_replicas_table.py +62 -0
  264. rucio/db/sqla/migrate_repo/versions/3345511706b8_replicas_table_pk_definition_is_in_.py +74 -0
  265. rucio/db/sqla/migrate_repo/versions/35ef10d1e11b_change_index_on_table_requests.py +44 -0
  266. rucio/db/sqla/migrate_repo/versions/379a19b5332d_create_rse_limits_table.py +67 -0
  267. rucio/db/sqla/migrate_repo/versions/384b96aa0f60_created_rule_history_tables.py +134 -0
  268. rucio/db/sqla/migrate_repo/versions/3ac1660a1a72_extend_distance_table.py +58 -0
  269. rucio/db/sqla/migrate_repo/versions/3ad36e2268b0_create_collection_replicas_updates_table.py +79 -0
  270. rucio/db/sqla/migrate_repo/versions/3c9df354071b_extend_waiting_request_state.py +61 -0
  271. rucio/db/sqla/migrate_repo/versions/3d9813fab443_add_a_new_state_lost_in_badfilesstatus.py +45 -0
  272. rucio/db/sqla/migrate_repo/versions/40ad39ce3160_add_transferred_at_to_requests_table.py +46 -0
  273. rucio/db/sqla/migrate_repo/versions/4207be2fd914_add_notification_column_to_rules.py +65 -0
  274. rucio/db/sqla/migrate_repo/versions/42db2617c364_create_index_on_requests_external_id.py +42 -0
  275. rucio/db/sqla/migrate_repo/versions/436827b13f82_added_column_activity_to_table_requests.py +46 -0
  276. rucio/db/sqla/migrate_repo/versions/44278720f774_update_requests_typ_sta_upd_idx_index.py +46 -0
  277. rucio/db/sqla/migrate_repo/versions/45378a1e76a8_create_collection_replica_table.py +80 -0
  278. rucio/db/sqla/migrate_repo/versions/469d262be19_removing_created_at_index.py +43 -0
  279. rucio/db/sqla/migrate_repo/versions/4783c1f49cb4_create_distance_table.py +61 -0
  280. rucio/db/sqla/migrate_repo/versions/49a21b4d4357_create_index_on_table_tokens.py +47 -0
  281. rucio/db/sqla/migrate_repo/versions/4a2cbedda8b9_add_source_replica_expression_column_to_.py +46 -0
  282. rucio/db/sqla/migrate_repo/versions/4a7182d9578b_added_bytes_length_accessed_at_columns.py +52 -0
  283. rucio/db/sqla/migrate_repo/versions/4bab9edd01fc_create_index_on_requests_rule_id.py +42 -0
  284. rucio/db/sqla/migrate_repo/versions/4c3a4acfe006_new_attr_account_table.py +65 -0
  285. rucio/db/sqla/migrate_repo/versions/4cf0a2e127d4_adding_transient_metadata.py +46 -0
  286. rucio/db/sqla/migrate_repo/versions/50280c53117c_add_qos_class_to_rse.py +47 -0
  287. rucio/db/sqla/migrate_repo/versions/52153819589c_add_rse_id_to_replicas_table.py +45 -0
  288. rucio/db/sqla/migrate_repo/versions/52fd9f4916fa_added_activity_to_rules.py +46 -0
  289. rucio/db/sqla/migrate_repo/versions/53b479c3cb0f_fix_did_meta_table_missing_updated_at_.py +48 -0
  290. rucio/db/sqla/migrate_repo/versions/5673b4b6e843_add_wfms_metadata_to_rule_tables.py +50 -0
  291. rucio/db/sqla/migrate_repo/versions/575767d9f89_added_source_history_table.py +59 -0
  292. rucio/db/sqla/migrate_repo/versions/58bff7008037_add_started_at_to_requests.py +48 -0
  293. rucio/db/sqla/migrate_repo/versions/58c8b78301ab_rename_callback_to_message.py +108 -0
  294. rucio/db/sqla/migrate_repo/versions/5f139f77382a_added_child_rule_id_column.py +57 -0
  295. rucio/db/sqla/migrate_repo/versions/688ef1840840_adding_did_meta_table.py +51 -0
  296. rucio/db/sqla/migrate_repo/versions/6e572a9bfbf3_add_new_split_container_column_to_rules.py +50 -0
  297. rucio/db/sqla/migrate_repo/versions/70587619328_add_comment_column_for_subscriptions.py +46 -0
  298. rucio/db/sqla/migrate_repo/versions/739064d31565_remove_history_table_pks.py +42 -0
  299. rucio/db/sqla/migrate_repo/versions/7541902bf173_add_didsfollowed_and_followevents_table.py +93 -0
  300. rucio/db/sqla/migrate_repo/versions/7ec22226cdbf_new_replica_state_for_temporary_.py +73 -0
  301. rucio/db/sqla/migrate_repo/versions/810a41685bc1_added_columns_rse_transfer_limits.py +52 -0
  302. rucio/db/sqla/migrate_repo/versions/83f991c63a93_correct_rse_expression_length.py +45 -0
  303. rucio/db/sqla/migrate_repo/versions/8523998e2e76_increase_size_of_extended_attributes_.py +46 -0
  304. rucio/db/sqla/migrate_repo/versions/8ea9122275b1_adding_missing_function_based_indices.py +54 -0
  305. rucio/db/sqla/migrate_repo/versions/90f47792bb76_add_clob_payload_to_messages.py +48 -0
  306. rucio/db/sqla/migrate_repo/versions/914b8f02df38_new_table_for_lifetime_model_exceptions.py +70 -0
  307. rucio/db/sqla/migrate_repo/versions/94a5961ddbf2_add_estimator_columns.py +48 -0
  308. rucio/db/sqla/migrate_repo/versions/9a1b149a2044_add_saml_identity_type.py +95 -0
  309. rucio/db/sqla/migrate_repo/versions/9a45bc4ea66d_add_vp_table.py +55 -0
  310. rucio/db/sqla/migrate_repo/versions/9eb936a81eb1_true_is_true.py +74 -0
  311. rucio/db/sqla/migrate_repo/versions/a118956323f8_added_vo_table_and_vo_col_to_rse.py +78 -0
  312. rucio/db/sqla/migrate_repo/versions/a193a275255c_add_status_column_in_messages.py +49 -0
  313. rucio/db/sqla/migrate_repo/versions/a5f6f6e928a7_1_7_0.py +124 -0
  314. rucio/db/sqla/migrate_repo/versions/a616581ee47_added_columns_to_table_requests.py +60 -0
  315. rucio/db/sqla/migrate_repo/versions/a6eb23955c28_state_idx_non_functional.py +53 -0
  316. rucio/db/sqla/migrate_repo/versions/a74275a1ad30_added_global_quota_table.py +56 -0
  317. rucio/db/sqla/migrate_repo/versions/a93e4e47bda_heartbeats.py +67 -0
  318. rucio/db/sqla/migrate_repo/versions/ae2a56fcc89_added_comment_column_to_rules.py +50 -0
  319. rucio/db/sqla/migrate_repo/versions/b4293a99f344_added_column_identity_to_table_tokens.py +46 -0
  320. rucio/db/sqla/migrate_repo/versions/b7d287de34fd_removal_of_replicastate_source.py +92 -0
  321. rucio/db/sqla/migrate_repo/versions/b818052fa670_add_index_to_quarantined_replicas.py +42 -0
  322. rucio/db/sqla/migrate_repo/versions/b8caac94d7f0_add_comments_column_for_subscriptions_.py +46 -0
  323. rucio/db/sqla/migrate_repo/versions/b96a1c7e1cc4_new_bad_pfns_table_and_bad_replicas_.py +147 -0
  324. rucio/db/sqla/migrate_repo/versions/bb695f45c04_extend_request_state.py +78 -0
  325. rucio/db/sqla/migrate_repo/versions/bc68e9946deb_add_staging_timestamps_to_request.py +53 -0
  326. rucio/db/sqla/migrate_repo/versions/bf3baa1c1474_correct_pk_and_idx_for_history_tables.py +74 -0
  327. rucio/db/sqla/migrate_repo/versions/c0937668555f_add_qos_policy_map_table.py +56 -0
  328. rucio/db/sqla/migrate_repo/versions/c129ccdb2d5_add_lumiblocknr_to_dids.py +46 -0
  329. rucio/db/sqla/migrate_repo/versions/ccdbcd48206e_add_did_type_column_index_on_did_meta_.py +68 -0
  330. rucio/db/sqla/migrate_repo/versions/cebad904c4dd_new_payload_column_for_heartbeats.py +48 -0
  331. rucio/db/sqla/migrate_repo/versions/d1189a09c6e0_oauth2_0_and_jwt_feature_support_adding_.py +149 -0
  332. rucio/db/sqla/migrate_repo/versions/d23453595260_extend_request_state_for_preparer.py +106 -0
  333. rucio/db/sqla/migrate_repo/versions/d6dceb1de2d_added_purge_column_to_rules.py +47 -0
  334. rucio/db/sqla/migrate_repo/versions/d6e2c3b2cf26_remove_third_party_copy_column_from_rse.py +45 -0
  335. rucio/db/sqla/migrate_repo/versions/d91002c5841_new_account_limits_table.py +105 -0
  336. rucio/db/sqla/migrate_repo/versions/e138c364ebd0_extending_columns_for_filter_and_.py +52 -0
  337. rucio/db/sqla/migrate_repo/versions/e59300c8b179_support_for_archive.py +106 -0
  338. rucio/db/sqla/migrate_repo/versions/f1b14a8c2ac1_postgres_use_check_constraints.py +30 -0
  339. rucio/db/sqla/migrate_repo/versions/f41ffe206f37_oracle_global_temporary_tables.py +75 -0
  340. rucio/db/sqla/migrate_repo/versions/f85a2962b021_adding_transfertool_column_to_requests_.py +49 -0
  341. rucio/db/sqla/migrate_repo/versions/fa7a7d78b602_increase_refresh_token_size.py +45 -0
  342. rucio/db/sqla/migrate_repo/versions/fb28a95fe288_add_replicas_rse_id_tombstone_idx.py +38 -0
  343. rucio/db/sqla/migrate_repo/versions/fe1a65b176c9_set_third_party_copy_read_and_write_.py +44 -0
  344. rucio/db/sqla/migrate_repo/versions/fe8ea2fa9788_added_third_party_copy_column_to_rse_.py +46 -0
  345. rucio/db/sqla/models.py +1834 -0
  346. rucio/db/sqla/sautils.py +48 -0
  347. rucio/db/sqla/session.py +470 -0
  348. rucio/db/sqla/types.py +207 -0
  349. rucio/db/sqla/util.py +521 -0
  350. rucio/rse/__init__.py +97 -0
  351. rucio/rse/protocols/__init__.py +14 -0
  352. rucio/rse/protocols/cache.py +123 -0
  353. rucio/rse/protocols/dummy.py +112 -0
  354. rucio/rse/protocols/gfal.py +701 -0
  355. rucio/rse/protocols/globus.py +243 -0
  356. rucio/rse/protocols/gsiftp.py +93 -0
  357. rucio/rse/protocols/http_cache.py +83 -0
  358. rucio/rse/protocols/mock.py +124 -0
  359. rucio/rse/protocols/ngarc.py +210 -0
  360. rucio/rse/protocols/posix.py +251 -0
  361. rucio/rse/protocols/protocol.py +530 -0
  362. rucio/rse/protocols/rclone.py +365 -0
  363. rucio/rse/protocols/rfio.py +137 -0
  364. rucio/rse/protocols/srm.py +339 -0
  365. rucio/rse/protocols/ssh.py +414 -0
  366. rucio/rse/protocols/storm.py +207 -0
  367. rucio/rse/protocols/webdav.py +547 -0
  368. rucio/rse/protocols/xrootd.py +295 -0
  369. rucio/rse/rsemanager.py +752 -0
  370. rucio/tests/__init__.py +14 -0
  371. rucio/tests/common.py +244 -0
  372. rucio/tests/common_server.py +132 -0
  373. rucio/transfertool/__init__.py +14 -0
  374. rucio/transfertool/fts3.py +1484 -0
  375. rucio/transfertool/globus.py +200 -0
  376. rucio/transfertool/globus_library.py +182 -0
  377. rucio/transfertool/mock.py +81 -0
  378. rucio/transfertool/transfertool.py +212 -0
  379. rucio/vcsversion.py +11 -0
  380. rucio/version.py +46 -0
  381. rucio/web/__init__.py +14 -0
  382. rucio/web/rest/__init__.py +14 -0
  383. rucio/web/rest/flaskapi/__init__.py +14 -0
  384. rucio/web/rest/flaskapi/authenticated_bp.py +28 -0
  385. rucio/web/rest/flaskapi/v1/__init__.py +14 -0
  386. rucio/web/rest/flaskapi/v1/accountlimits.py +234 -0
  387. rucio/web/rest/flaskapi/v1/accounts.py +1088 -0
  388. rucio/web/rest/flaskapi/v1/archives.py +100 -0
  389. rucio/web/rest/flaskapi/v1/auth.py +1642 -0
  390. rucio/web/rest/flaskapi/v1/common.py +385 -0
  391. rucio/web/rest/flaskapi/v1/config.py +305 -0
  392. rucio/web/rest/flaskapi/v1/credentials.py +213 -0
  393. rucio/web/rest/flaskapi/v1/dids.py +2204 -0
  394. rucio/web/rest/flaskapi/v1/dirac.py +116 -0
  395. rucio/web/rest/flaskapi/v1/export.py +77 -0
  396. rucio/web/rest/flaskapi/v1/heartbeats.py +129 -0
  397. rucio/web/rest/flaskapi/v1/identities.py +263 -0
  398. rucio/web/rest/flaskapi/v1/import.py +133 -0
  399. rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +315 -0
  400. rucio/web/rest/flaskapi/v1/locks.py +360 -0
  401. rucio/web/rest/flaskapi/v1/main.py +83 -0
  402. rucio/web/rest/flaskapi/v1/meta.py +226 -0
  403. rucio/web/rest/flaskapi/v1/metrics.py +37 -0
  404. rucio/web/rest/flaskapi/v1/nongrid_traces.py +97 -0
  405. rucio/web/rest/flaskapi/v1/ping.py +89 -0
  406. rucio/web/rest/flaskapi/v1/redirect.py +366 -0
  407. rucio/web/rest/flaskapi/v1/replicas.py +1866 -0
  408. rucio/web/rest/flaskapi/v1/requests.py +841 -0
  409. rucio/web/rest/flaskapi/v1/rses.py +2204 -0
  410. rucio/web/rest/flaskapi/v1/rules.py +824 -0
  411. rucio/web/rest/flaskapi/v1/scopes.py +161 -0
  412. rucio/web/rest/flaskapi/v1/subscriptions.py +646 -0
  413. rucio/web/rest/flaskapi/v1/templates/auth_crash.html +80 -0
  414. rucio/web/rest/flaskapi/v1/templates/auth_granted.html +82 -0
  415. rucio/web/rest/flaskapi/v1/tmp_dids.py +115 -0
  416. rucio/web/rest/flaskapi/v1/traces.py +100 -0
  417. rucio/web/rest/flaskapi/v1/vos.py +280 -0
  418. rucio/web/rest/main.py +19 -0
  419. rucio/web/rest/metrics.py +28 -0
  420. rucio-32.8.6.data/data/rucio/etc/alembic.ini.template +71 -0
  421. rucio-32.8.6.data/data/rucio/etc/alembic_offline.ini.template +74 -0
  422. rucio-32.8.6.data/data/rucio/etc/globus-config.yml.template +5 -0
  423. rucio-32.8.6.data/data/rucio/etc/ldap.cfg.template +30 -0
  424. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_approval_request.tmpl +38 -0
  425. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +4 -0
  426. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_approved_user.tmpl +17 -0
  427. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +6 -0
  428. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_denied_user.tmpl +17 -0
  429. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +19 -0
  430. rucio-32.8.6.data/data/rucio/etc/rse-accounts.cfg.template +25 -0
  431. rucio-32.8.6.data/data/rucio/etc/rucio.cfg.atlas.client.template +42 -0
  432. rucio-32.8.6.data/data/rucio/etc/rucio.cfg.template +257 -0
  433. rucio-32.8.6.data/data/rucio/etc/rucio_multi_vo.cfg.template +234 -0
  434. rucio-32.8.6.data/data/rucio/requirements.txt +55 -0
  435. rucio-32.8.6.data/data/rucio/tools/bootstrap.py +34 -0
  436. rucio-32.8.6.data/data/rucio/tools/merge_rucio_configs.py +147 -0
  437. rucio-32.8.6.data/data/rucio/tools/reset_database.py +40 -0
  438. rucio-32.8.6.data/scripts/rucio +2540 -0
  439. rucio-32.8.6.data/scripts/rucio-abacus-account +75 -0
  440. rucio-32.8.6.data/scripts/rucio-abacus-collection-replica +47 -0
  441. rucio-32.8.6.data/scripts/rucio-abacus-rse +79 -0
  442. rucio-32.8.6.data/scripts/rucio-admin +2434 -0
  443. rucio-32.8.6.data/scripts/rucio-atropos +61 -0
  444. rucio-32.8.6.data/scripts/rucio-auditor +199 -0
  445. rucio-32.8.6.data/scripts/rucio-automatix +51 -0
  446. rucio-32.8.6.data/scripts/rucio-bb8 +58 -0
  447. rucio-32.8.6.data/scripts/rucio-c3po +86 -0
  448. rucio-32.8.6.data/scripts/rucio-cache-client +135 -0
  449. rucio-32.8.6.data/scripts/rucio-cache-consumer +43 -0
  450. rucio-32.8.6.data/scripts/rucio-conveyor-finisher +59 -0
  451. rucio-32.8.6.data/scripts/rucio-conveyor-poller +67 -0
  452. rucio-32.8.6.data/scripts/rucio-conveyor-preparer +38 -0
  453. rucio-32.8.6.data/scripts/rucio-conveyor-receiver +44 -0
  454. rucio-32.8.6.data/scripts/rucio-conveyor-stager +77 -0
  455. rucio-32.8.6.data/scripts/rucio-conveyor-submitter +140 -0
  456. rucio-32.8.6.data/scripts/rucio-conveyor-throttler +105 -0
  457. rucio-32.8.6.data/scripts/rucio-dark-reaper +54 -0
  458. rucio-32.8.6.data/scripts/rucio-dumper +159 -0
  459. rucio-32.8.6.data/scripts/rucio-follower +45 -0
  460. rucio-32.8.6.data/scripts/rucio-hermes +55 -0
  461. rucio-32.8.6.data/scripts/rucio-judge-cleaner +90 -0
  462. rucio-32.8.6.data/scripts/rucio-judge-evaluator +138 -0
  463. rucio-32.8.6.data/scripts/rucio-judge-injector +45 -0
  464. rucio-32.8.6.data/scripts/rucio-judge-repairer +45 -0
  465. rucio-32.8.6.data/scripts/rucio-kronos +45 -0
  466. rucio-32.8.6.data/scripts/rucio-light-reaper +53 -0
  467. rucio-32.8.6.data/scripts/rucio-minos +54 -0
  468. rucio-32.8.6.data/scripts/rucio-minos-temporary-expiration +51 -0
  469. rucio-32.8.6.data/scripts/rucio-necromancer +121 -0
  470. rucio-32.8.6.data/scripts/rucio-oauth-manager +64 -0
  471. rucio-32.8.6.data/scripts/rucio-reaper +84 -0
  472. rucio-32.8.6.data/scripts/rucio-replica-recoverer +249 -0
  473. rucio-32.8.6.data/scripts/rucio-storage-consistency-actions +75 -0
  474. rucio-32.8.6.data/scripts/rucio-transmogrifier +78 -0
  475. rucio-32.8.6.data/scripts/rucio-undertaker +77 -0
  476. rucio-32.8.6.dist-info/METADATA +83 -0
  477. rucio-32.8.6.dist-info/RECORD +481 -0
  478. rucio-32.8.6.dist-info/WHEEL +5 -0
  479. rucio-32.8.6.dist-info/licenses/AUTHORS.rst +94 -0
  480. rucio-32.8.6.dist-info/licenses/LICENSE +201 -0
  481. rucio-32.8.6.dist-info/top_level.txt +1 -0
rucio/core/rse.py ADDED
@@ -0,0 +1,1835 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright European Organization for Nuclear Research (CERN) since 2012
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import json
17
+ from collections.abc import Iterator, Iterable
18
+ from datetime import datetime
19
+ from io import StringIO
20
+ from re import match
21
+ from typing import Any, Generic, Optional, TypeVar, Union, TYPE_CHECKING
22
+
23
+ import sqlalchemy
24
+ from dogpile.cache.api import NO_VALUE
25
+ from sqlalchemy.exc import DatabaseError, IntegrityError, OperationalError
26
+ from sqlalchemy.orm import aliased
27
+ from sqlalchemy.orm.exc import FlushError
28
+ from sqlalchemy.sql.expression import or_, and_, desc, true, false, func, select, delete
29
+
30
+ from rucio.common import exception, utils
31
+ from rucio.common.cache import make_region_memcached
32
+ from rucio.common.config import get_lfn2pfn_algorithm_default
33
+ from rucio.common import types
34
+ from rucio.common.utils import CHECKSUM_KEY, GLOBALLY_SUPPORTED_CHECKSUMS, Availability
35
+ from rucio.core.rse_counter import add_counter, get_counter
36
+ from rucio.db.sqla import models
37
+ from rucio.db.sqla.constants import RSEType, ReplicaState
38
+ from rucio.db.sqla.session import read_session, transactional_session, stream_session
39
+ from rucio.db.sqla.util import temp_table_mngr
40
+
41
+ if TYPE_CHECKING:
42
+ from sqlalchemy.orm import Session
43
+
44
+ T = TypeVar('T', bound="RseData")
45
+
46
+ RSE_SETTINGS = ["continent", "city", "region_code", "country_name", "time_zone", "ISP", "ASN"]
47
+ REGION = make_region_memcached(expiration_time=900)
48
+
49
+
50
+ class RseData:
51
+ """
52
+ Helper data class storing rse data grouped in one place.
53
+ """
54
+ def __init__(self, id_, name: "Optional[str]" = None, columns=None, attributes=None, info=None, usage=None, limits=None, transfer_limits=None):
55
+ self.id = id_
56
+ self._name = name
57
+ self._columns = columns
58
+ self._attributes = attributes
59
+ self._info = info
60
+ self._usage = usage
61
+ self._limits = limits
62
+ self._transfer_limits = transfer_limits
63
+
64
+ @property
65
+ def name(self) -> str:
66
+ if self._name is None:
67
+ raise ValueError(f'name not loaded for rse {self}')
68
+ return self._name
69
+
70
+ @name.setter
71
+ def name(self, name):
72
+ self._name = name
73
+
74
+ @property
75
+ def columns(self) -> dict[str, Any]:
76
+ if self._columns is None:
77
+ raise ValueError(f'columns not loaded for rse {self}')
78
+ return self._columns
79
+
80
+ @property
81
+ def attributes(self) -> dict[str, Any]:
82
+ if self._attributes is None:
83
+ raise ValueError(f'attributes not loaded for rse {self}')
84
+ return self._attributes
85
+
86
+ @property
87
+ def info(self) -> dict[str, Any]:
88
+ if self._info is None:
89
+ raise ValueError(f'info not loaded for rse {self}')
90
+ return self._info
91
+
92
+ @property
93
+ def usage(self) -> list[dict[str, Any]]:
94
+ if self._usage is None:
95
+ raise ValueError(f'usage not loaded for rse {self}')
96
+ return self._usage
97
+
98
+ @property
99
+ def limits(self) -> dict[str, Any]:
100
+ if self._limits is None:
101
+ raise ValueError(f'limits not loaded for rse {self}')
102
+ return self._limits
103
+
104
+ @property
105
+ def transfer_limits(self):
106
+ if self._transfer_limits is None:
107
+ raise ValueError(f'transfer_limits not loaded for rse {self}')
108
+ return self._transfer_limits
109
+
110
+ def __hash__(self):
111
+ return hash(self.id)
112
+
113
+ def __repr__(self):
114
+ if self._name is not None:
115
+ return self._name
116
+ return self.id
117
+
118
+ def __eq__(self, other):
119
+ if other is None:
120
+ return False
121
+ return self.id == other.id
122
+
123
+ def is_tape(self):
124
+ if self.info['rse_type'] == RSEType.TAPE or self.info['rse_type'] == 'TAPE':
125
+ return True
126
+ return False
127
+
128
+ def is_tape_or_staging_required(self):
129
+ if self.is_tape() or self.attributes.get('staging_required', False):
130
+ return True
131
+ return False
132
+
133
+ @read_session
134
+ def ensure_loaded(self, load_name=False, load_columns=False, load_attributes=False,
135
+ load_info=False, load_usage=False, load_limits=False, load_transfer_limits=False, *, session: "Session"):
136
+ if self._columns is None and load_columns:
137
+ self._columns = get_rse(rse_id=self.id, session=session)
138
+ self._name = self._columns['rse']
139
+ if self._attributes is None and load_attributes:
140
+ self._attributes = list_rse_attributes(self.id, use_cache=True, session=session)
141
+ if self._info is None and load_info:
142
+ self._info = get_rse_info(self.id, session=session)
143
+ self._name = self._info['rse']
144
+ if self._usage is None and load_usage:
145
+ self._usage = get_rse_usage(rse_id=self.id, session=session)
146
+ if self._limits is None and load_limits:
147
+ self._limits = get_rse_limits(rse_id=self.id, session=session)
148
+ if self._transfer_limits is None and load_transfer_limits:
149
+ self._transfer_limits = get_rse_transfer_limits(rse_id=self.id, session=session)
150
+ if self._name is None and load_name:
151
+ self._name = get_rse_name(rse_id=self.id, session=session)
152
+ return self
153
+
154
+ @staticmethod
155
+ @read_session
156
+ def bulk_load(rse_id_to_data: "dict[str, RseData]", load_name=False, load_columns=False, load_attributes=False,
157
+ load_info=False, load_usage=False, load_limits=False, *, session: "Session"):
158
+ """
159
+ Given a dict of RseData objects indexed by rse_id, ensure that the desired fields are initialised
160
+ in all objects from the input.
161
+ """
162
+ if len(rse_id_to_data) < 4: # 4 was selected without particular reason as "seems good enough"
163
+ for rse_data in rse_id_to_data.values():
164
+ rse_data.ensure_loaded(
165
+ load_name=load_name,
166
+ load_columns=load_columns,
167
+ load_attributes=load_attributes,
168
+ load_info=load_info,
169
+ load_usage=load_usage,
170
+ load_limits=load_limits,
171
+ session=session
172
+ )
173
+ return
174
+
175
+ rse_ids_to_load = set()
176
+ for rse_id, rse_data in rse_id_to_data.items():
177
+ anything_to_load = any((
178
+ load_name and rse_data._name is None,
179
+ load_columns and rse_data._columns is None,
180
+ load_attributes and rse_data._attributes is None,
181
+ load_info and rse_data._info is None,
182
+ load_usage and rse_data._usage is None,
183
+ load_limits and rse_data._limits is None,
184
+ ))
185
+ if anything_to_load:
186
+ rse_ids_to_load.add(rse_id)
187
+ if not rse_ids_to_load:
188
+ # all required fields are already present. Nothing to do
189
+ return
190
+
191
+ temp_table = temp_table_mngr(session).create_id_table()
192
+ session.bulk_insert_mappings(temp_table, ({'id': rse_id} for rse_id in rse_ids_to_load))
193
+
194
+ # We need to ensure that all rses exist and are not deleted. We could check this with a specialized
195
+ # query, but this seems wasteful under normal operation: the caller of the current function probably
196
+ # got the list of RSE IDs from list_rses (or another source which checks for deleted rses).
197
+ #
198
+ # Instead, directly fetch all RSEs, which allows to reduce the number (and complexity) of other queries bellow
199
+ stmt = select(
200
+ models.RSE
201
+ ).join_from(
202
+ temp_table,
203
+ models.RSE,
204
+ models.RSE.id == temp_table.id
205
+ ).where(
206
+ models.RSE.deleted == false()
207
+ )
208
+ db_rses_by_id = {str(db_rse.id): db_rse for db_rse in session.execute(stmt).scalars()}
209
+
210
+ if len(db_rses_by_id) != len(rse_ids_to_load):
211
+ failed_rse_ids = ', '.join(rse_ids_to_load.difference(db_rses_by_id))
212
+ raise exception.RSENotFound(f"RSE(s) with id(s) '{failed_rse_ids}' cannot be found")
213
+
214
+ if load_attributes:
215
+ for rse_id, attr in _fetch_many_rses_attributes(rse_id_temp_table=temp_table, session=session):
216
+ rse_id_to_data[rse_id]._attributes = attr
217
+
218
+ if load_columns:
219
+ settings_by_id = {}
220
+ if not load_attributes:
221
+ settings_by_id = dict(_fetch_many_rses_attributes(rse_id_temp_table=temp_table,
222
+ keys=RSE_SETTINGS,
223
+ session=session))
224
+ for rse_id, db_rse in db_rses_by_id.items():
225
+ rse_data = rse_id_to_data[rse_id]
226
+ settings = rse_data._attributes if rse_data._attributes is not None else settings_by_id.get(rse_id, {})
227
+ columns = _format_get_rse(db_rse=db_rse, rse_attributes=settings, session=session)
228
+ rse_data._columns = columns
229
+ rse_data._name = columns['rse']
230
+
231
+ if load_info:
232
+ stmt = select(
233
+ temp_table.id,
234
+ models.RSEProtocols
235
+ ).outerjoin_from(
236
+ temp_table,
237
+ models.RSEProtocols,
238
+ models.RSEProtocols.rse_id == temp_table.id
239
+ ).order_by(
240
+ temp_table.id,
241
+ )
242
+ for rse_id, db_protocols in _group_query_result_by_rse_id(stmt, session=session):
243
+ db_rse = db_rses_by_id[rse_id]
244
+ rse_data = rse_id_to_data[rse_id]
245
+ rse_attributes = rse_data._attributes
246
+ info = _format_get_rse_protocols(rse=db_rse, db_protocols=db_protocols,
247
+ rse_attributes=rse_attributes, session=session)
248
+ rse_data._info = info
249
+ rse_data._name = info['rse']
250
+
251
+ if load_limits:
252
+ stmt = select(
253
+ temp_table.id,
254
+ models.RSELimit
255
+ ).outerjoin_from(
256
+ temp_table,
257
+ models.RSELimit,
258
+ models.RSELimit.rse_id == temp_table.id
259
+ ).order_by(
260
+ temp_table.id,
261
+ )
262
+ for rse_id, limits_list in _group_query_result_by_rse_id(stmt, session=session):
263
+ rse_id_to_data[rse_id]._limits = {limit.name: limit.value for limit in limits_list}
264
+
265
+ if load_usage:
266
+ stmt = select(
267
+ temp_table.id,
268
+ models.RSEUsage
269
+ ).outerjoin_from(
270
+ temp_table,
271
+ models.RSEUsage,
272
+ models.RSEUsage.rse_id == temp_table.id
273
+ ).order_by(
274
+ temp_table.id,
275
+ )
276
+ for rse_id, db_usages in _group_query_result_by_rse_id(stmt, session=session):
277
+ usage = _format_get_rse_usage(rse_id=rse_id, db_usages=db_usages, per_account=False, session=session)
278
+ rse_id_to_data[rse_id]._usage = usage
279
+
280
+ if load_name:
281
+ # The name could have been loaded already (when loading columns or info). Skip loading if it's known.
282
+ if not load_columns and not load_info:
283
+ for rse_id in rse_ids_to_load:
284
+ rse_id_to_data[rse_id]._name = db_rses_by_id[rse_id].rse
285
+
286
+
287
+ class RseCollection(Generic[T]):
288
+ """
289
+ Container which keeps track of information loaded from the database for a group of RSEs.
290
+ """
291
+
292
+ def __init__(self, rse_ids: Optional[Iterable[str]] = None, rse_data_cls: type[T] = RseData):
293
+ self._rse_data_cls = rse_data_cls
294
+ self.rse_id_to_data_map: dict[str, T] = {}
295
+ if rse_ids is not None:
296
+ for rse_id in rse_ids:
297
+ self.rse_id_to_data_map[rse_id] = self._rse_data_cls(rse_id)
298
+
299
+ def __getitem__(self, item):
300
+ return self.get_or_create(item)
301
+
302
+ def __setitem__(self, key, value):
303
+ rse_id = key
304
+ rse_data = value
305
+ self.rse_id_to_data_map[rse_id] = rse_data
306
+
307
+ def __contains__(self, item):
308
+ if isinstance(item, RseData):
309
+ return item.id in self.rse_id_to_data_map
310
+ if isinstance(item, str):
311
+ return item in self.rse_id_to_data_map
312
+ return False
313
+
314
+ def get(self, rse_id: str) -> "Optional[T]":
315
+ return self.rse_id_to_data_map.get(rse_id)
316
+
317
+ def get_or_create(self, rse_id: str) -> "T":
318
+ rse_data = self.rse_id_to_data_map.get(rse_id)
319
+ if rse_data is None:
320
+ self.rse_id_to_data_map[rse_id] = rse_data = self._rse_data_cls(rse_id)
321
+ return rse_data
322
+
323
+ @transactional_session
324
+ def ensure_loaded(
325
+ self,
326
+ rse_ids: "Optional[Iterable[str]]" = None,
327
+ load_name: bool = False,
328
+ load_columns: bool = False,
329
+ load_attributes: bool = False,
330
+ load_info: bool = False,
331
+ load_usage: bool = False,
332
+ load_limits: bool = False,
333
+ *,
334
+ session: "Session",
335
+ ):
336
+ RseData.bulk_load(
337
+ rse_id_to_data={rse_id: self.get_or_create(rse_id) for rse_id in rse_ids} if rse_ids else self.rse_id_to_data_map,
338
+ load_name=load_name,
339
+ load_columns=load_columns,
340
+ load_attributes=load_attributes,
341
+ load_info=load_info,
342
+ load_usage=load_usage,
343
+ load_limits=load_limits,
344
+ session=session,
345
+ )
346
+
347
+
348
+ @stream_session
349
+ def _group_query_result_by_rse_id(stmt, *, session: "Session") -> Iterator[tuple[str, list[Any]]]:
350
+ """
351
+ Given a sqlalchemy query statement which fetches rows of two elements: (rse_id, object) ordered by rse_id.
352
+ Will execute the query and return objects grouped by rse_id: (rse_id, [object1, object2])
353
+ """
354
+
355
+ current_rse_id = None
356
+ objects = []
357
+ for rse_id, obj in session.execute(stmt):
358
+ if current_rse_id != rse_id:
359
+ if current_rse_id is not None:
360
+ yield str(current_rse_id), objects
361
+
362
+ current_rse_id = rse_id
363
+ objects = []
364
+
365
+ if obj is not None:
366
+ objects.append(obj)
367
+
368
+ if current_rse_id is not None:
369
+ yield str(current_rse_id), objects
370
+
371
+
372
+ @transactional_session
373
+ def add_rse(rse, vo='def', deterministic=True, volatile=False, city=None, region_code=None, country_name=None, continent=None, time_zone=None,
374
+ ISP=None, staging_area=False, rse_type=RSEType.DISK, longitude=None, latitude=None, ASN=None, availability_read: Optional[bool] = None,
375
+ availability_write: Optional[bool] = None, availability_delete: Optional[bool] = None, *, session: "Session"):
376
+ """
377
+ Add a rse with the given location name.
378
+
379
+ :param rse: the name of the new rse.
380
+ :param vo: the vo to add the RSE to.
381
+ :param deterministic: Boolean to know if the pfn is generated deterministically.
382
+ :param volatile: Boolean for RSE cache.
383
+ :param city: City for the RSE. Accessed by `locals()`.
384
+ :param region_code: The region code for the RSE. Accessed by `locals()`.
385
+ :param country_name: The country. Accessed by `locals()`.
386
+ :param continent: The continent. Accessed by `locals()`.
387
+ :param time_zone: Timezone. Accessed by `locals()`.
388
+ :param ISP: Internet service provider. Accessed by `locals()`.
389
+ :param staging_area: Staging area.
390
+ :param rse_type: RSE type.
391
+ :param latitude: Latitude coordinate of RSE.
392
+ :param longitude: Longitude coordinate of RSE.
393
+ :param ASN: Access service network. Accessed by `locals()`.
394
+ :param availability_read: If the RSE is readable.
395
+ :param availability_write: If the RSE is writable.
396
+ :param availability_delete: If the RSE is deletable.
397
+ :param session: The database session in use.
398
+ """
399
+ if isinstance(rse_type, str):
400
+ rse_type = RSEType(rse_type)
401
+
402
+ availability = Availability(availability_read, availability_write, availability_delete).integer
403
+ new_rse = models.RSE(rse=rse, vo=vo, deterministic=deterministic, volatile=volatile,
404
+ staging_area=staging_area, rse_type=rse_type, longitude=longitude,
405
+ latitude=latitude, availability=availability, availability_read=availability_read,
406
+ availability_write=availability_write, availability_delete=availability_delete,
407
+
408
+ # The following fields will be deprecated, they are RSE attributes now.
409
+ # (Still in the code for backwards compatibility)
410
+ city=city, region_code=region_code, country_name=country_name,
411
+ continent=continent, time_zone=time_zone, ISP=ISP, ASN=ASN)
412
+ try:
413
+ new_rse.save(session=session)
414
+ except IntegrityError:
415
+ raise exception.Duplicate(f"RSE '{rse}' already exists!")
416
+ except DatabaseError as error:
417
+ raise exception.RucioException(error.args)
418
+
419
+ # Add rse name as a RSE-Tag
420
+ add_rse_attribute(rse_id=new_rse.id, key=rse, value=True, session=session)
421
+
422
+ for setting in RSE_SETTINGS:
423
+ # The value accessed by locals is defined in the code and it can not be
424
+ # changed by a user request. This thus does not provide a scurity risk.
425
+ setting_value = locals().get(setting, None)
426
+ if setting_value:
427
+ add_rse_attribute(rse_id=new_rse.id, key=setting, value=setting_value, session=session)
428
+
429
+ # Add counter to monitor the space usage
430
+ add_counter(rse_id=new_rse.id, session=session)
431
+
432
+ return new_rse.id
433
+
434
+
435
+ @read_session
436
+ def rse_exists(rse, vo='def', include_deleted=False, *, session: "Session"):
437
+ """
438
+ Checks to see if RSE exists.
439
+
440
+ :param rse: Name of the rse.
441
+ :param vo: The VO for the RSE.
442
+ :param session: The database session in use.
443
+
444
+ :returns: True if found, otherwise false.
445
+ """
446
+ stmt = select(
447
+ models.RSE
448
+ ).where(
449
+ and_(
450
+ models.RSE.rse == rse,
451
+ models.RSE.vo == vo
452
+ )
453
+ )
454
+ if not include_deleted:
455
+ stmt = stmt.where(models.RSE.deleted == false())
456
+ return True if session.execute(stmt).scalar() else False
457
+
458
+
459
+ @transactional_session
460
+ def del_rse(rse_id, *, session: "Session"):
461
+ """
462
+ Disable a rse with the given rse id.
463
+
464
+ :param rse_id: the rse id.
465
+ :param session: The database session in use.
466
+ """
467
+
468
+ try:
469
+ stmt = select(
470
+ models.RSE
471
+ ).where(
472
+ models.RSE.id == rse_id,
473
+ models.RSE.deleted == false()
474
+ )
475
+ db_rse = session.execute(stmt).scalar_one()
476
+ rse_name = db_rse.rse
477
+ if not rse_is_empty(rse_id=rse_id, session=session):
478
+ raise exception.RSEOperationNotSupported('RSE \'%s\' is not empty' % rse_name)
479
+ except sqlalchemy.orm.exc.NoResultFound:
480
+ raise exception.RSENotFound('RSE with id \'%s\' cannot be found' % rse_id)
481
+ db_rse.delete(session=session)
482
+ try:
483
+ del_rse_attribute(rse_id=rse_id, key=rse_name, session=session)
484
+ except exception.RSEAttributeNotFound:
485
+ pass
486
+
487
+
488
+ @transactional_session
489
+ def restore_rse(rse_id, *, session: "Session"):
490
+ """
491
+ Restore a rse with the given rse id.
492
+
493
+ :param rse_id: the rse id.
494
+ :param session: The database session in use.
495
+ """
496
+
497
+ try:
498
+ stmt = select(
499
+ models.RSE
500
+ ).where(
501
+ models.RSE.id == rse_id,
502
+ models.RSE.deleted == true()
503
+ )
504
+ db_rse = session.execute(stmt).scalar_one()
505
+ except sqlalchemy.orm.exc.NoResultFound:
506
+ raise exception.RSENotFound('RSE with id \'%s\' cannot be found' % rse_id)
507
+ db_rse.deleted = False
508
+ db_rse.deleted_at = None
509
+ db_rse.save(session=session)
510
+ rse_name = db_rse.rse
511
+ add_rse_attribute(rse_id=rse_id, key=rse_name, value=True, session=session)
512
+
513
+
514
+ @read_session
515
+ def rse_is_empty(rse_id, *, session: "Session"):
516
+ """
517
+ Check if a RSE is empty.
518
+
519
+ :param rse_id: the rse id.
520
+ :param session: the database session in use.
521
+ """
522
+
523
+ is_empty = False
524
+ try:
525
+ is_empty = get_counter(rse_id, session=session)['bytes'] == 0
526
+ except exception.CounterNotFound:
527
+ is_empty = True
528
+ return is_empty
529
+
530
+
531
+ @read_session
532
+ def _format_get_rse(
533
+ db_rse: models.RSE,
534
+ rse_attributes: Optional[dict[str, Any]] = None,
535
+ *,
536
+ session: "Session"
537
+ ) -> dict[str, Any]:
538
+ """
539
+ Given a models.RSE object, return it formatted as expected by callers of get_rse
540
+ """
541
+ result = db_rse.to_dict()
542
+ result['type'] = db_rse.rse_type
543
+ if rse_attributes is not None:
544
+ rse_settings = {key: rse_attributes[key] for key in RSE_SETTINGS if key in rse_attributes}
545
+ else:
546
+ stmt = select(
547
+ models.RSEAttrAssociation
548
+ ).where(
549
+ and_(models.RSEAttrAssociation.rse_id == db_rse.id,
550
+ models.RSEAttrAssociation.key.in_(RSE_SETTINGS)),
551
+ )
552
+ rse_settings = {str(row.key): row.value for row in session.execute(stmt).scalars()}
553
+ result.update(rse_settings)
554
+ return result
555
+
556
+
557
+ @read_session
558
+ def get_rse(rse_id, *, session: "Session"):
559
+ """
560
+ Get a RSE or raise if it does not exist.
561
+
562
+ :param rse_id: The rse id.
563
+ :param session: The database session in use.
564
+
565
+ :raises RSENotFound: If referred RSE was not found in the database.
566
+ """
567
+
568
+ false_value = False # To make pep8 checker happy ...
569
+ try:
570
+ tmp = session.query(models.RSE).\
571
+ filter(sqlalchemy.and_(models.RSE.deleted == false_value,
572
+ models.RSE.id == rse_id))\
573
+ .one()
574
+ return _format_get_rse(tmp, session=session)
575
+ except sqlalchemy.orm.exc.NoResultFound:
576
+ raise exception.RSENotFound('RSE with id \'%s\' cannot be found' % rse_id)
577
+
578
+
579
+ @read_session
580
+ def get_rse_id(rse, vo='def', include_deleted=True, *, session: "Session"):
581
+ """
582
+ Get a RSE ID or raise if it does not exist.
583
+
584
+ :param rse: the rse name.
585
+ :param session: The database session in use.
586
+ :param include_deleted: Flag to toggle finding rse's marked as deleted.
587
+
588
+ :returns: The rse id.
589
+
590
+ :raises RSENotFound: If referred RSE was not found in the database.
591
+ """
592
+
593
+ if include_deleted:
594
+ if vo != 'def':
595
+ cache_key = 'rse-id_{}@{}'.format(rse, vo).replace(' ', '.')
596
+ else:
597
+ cache_key = 'rse-id_{}'.format(rse).replace(' ', '.')
598
+ result = REGION.get(cache_key)
599
+ if result != NO_VALUE:
600
+ return result
601
+
602
+ try:
603
+ stmt = select(
604
+ models.RSE.id
605
+ ).where(
606
+ models.RSE.rse == rse,
607
+ models.RSE.vo == vo
608
+ )
609
+ if not include_deleted:
610
+ stmt = stmt.where(models.RSE.deleted == false())
611
+ result = session.execute(stmt).scalar_one()
612
+ except sqlalchemy.orm.exc.NoResultFound:
613
+ raise exception.RSENotFound("RSE '%s' cannot be found in vo '%s'" % (rse, vo))
614
+
615
+ if include_deleted:
616
+ REGION.set(cache_key, result)
617
+ return result
618
+
619
+
620
+ @read_session
621
+ def _get_rse_db_column(rse_id: str, column, cache_prefix: str, include_deleted: bool = True, *, session: "Session"):
622
+ if include_deleted:
623
+ cache_key = '{}_{}'.format(cache_prefix, rse_id)
624
+ result = REGION.get(cache_key)
625
+ if result != NO_VALUE:
626
+ return result
627
+
628
+ try:
629
+ stmt = select(
630
+ column
631
+ ).where(
632
+ models.RSE.id == rse_id
633
+ )
634
+ if not include_deleted:
635
+ stmt = stmt.where(models.RSE.deleted == false())
636
+ result = session.execute(stmt).scalar_one()
637
+ except sqlalchemy.orm.exc.NoResultFound:
638
+ raise exception.RSENotFound('RSE with ID \'%s\' cannot be found' % rse_id)
639
+
640
+ if include_deleted:
641
+ REGION.set(cache_key, result)
642
+ return result
643
+
644
+
645
+ @read_session
646
+ def get_rse_name(rse_id: str, include_deleted: bool = True, *, session: "Session"):
647
+ """
648
+ Get a RSE name or raise if it does not exist.
649
+
650
+ :param rse_id: the rse uuid from the database.
651
+ :param session: The database session in use.
652
+ :param include_deleted: Flag to toggle finding rse's marked as deleted.
653
+
654
+ :returns: The rse name.
655
+
656
+ :raises RSENotFound: If referred RSE was not found in the database.
657
+ """
658
+ return _get_rse_db_column(
659
+ rse_id=rse_id,
660
+ column=models.RSE.rse,
661
+ cache_prefix='rse-name',
662
+ include_deleted=include_deleted,
663
+ session=session
664
+ )
665
+
666
+
667
+ @read_session
668
+ def get_rse_vo(rse_id: str, include_deleted: bool = True, *, session: "Session"):
669
+ """
670
+ Get the VO for a given RSE id.
671
+
672
+ :param rse_id: the rse uuid from the database.
673
+ :param session: the database session in use.
674
+ :param include_deleted: Flag to toggle finding rse's marked as deleted.
675
+
676
+ :returns The vo name.
677
+
678
+ :raises RSENotFound: If referred RSE was not found in database.
679
+ """
680
+ return _get_rse_db_column(
681
+ rse_id=rse_id,
682
+ column=models.RSE.vo,
683
+ cache_prefix='rse-vo',
684
+ include_deleted=include_deleted,
685
+ session=session
686
+ )
687
+
688
+
689
+ @read_session
690
+ def list_rses(filters={}, *, session: "Session"):
691
+ """
692
+ Returns a list of all RSEs.
693
+
694
+ :param filters: dictionary of attributes by which the results should be filtered.
695
+ :param session: The database session in use.
696
+
697
+ :returns: a list of dictionaries.
698
+ """
699
+
700
+ rse_list = []
701
+ filters = filters.copy() # Make a copy, so we can pop() without affecting the object `filters` outside this function
702
+
703
+ stmt = select(
704
+ models.RSE
705
+ ).where(
706
+ models.RSE.deleted == false()
707
+ )
708
+ if filters:
709
+ if 'availability' in filters and ('availability_read' in filters or 'availability_write' in filters or 'availability_delete' in filters):
710
+ raise exception.InvalidObject('Cannot use availability and read, write, delete filter at the same time.')
711
+
712
+ if 'availability' in filters:
713
+ availability = Availability.from_integer(filters['availability'])
714
+ filters['availability_read'] = availability.read
715
+ filters['availability_write'] = availability.write
716
+ filters['availability_delete'] = availability.delete
717
+ del filters['availability']
718
+
719
+ for (k, v) in filters.items():
720
+ if hasattr(models.RSE, k):
721
+ if k == 'rse_type':
722
+ stmt = stmt.where(getattr(models.RSE, k) == RSEType[v])
723
+ else:
724
+ stmt = stmt.where(getattr(models.RSE, k) == v)
725
+ else:
726
+ attr_assoc_alias = aliased(models.RSEAttrAssociation)
727
+ stmt = stmt.join(
728
+ attr_assoc_alias,
729
+ and_(
730
+ attr_assoc_alias.rse_id == models.RSE.id,
731
+ attr_assoc_alias.key == k,
732
+ attr_assoc_alias.value == v,
733
+ )
734
+ )
735
+ else:
736
+ stmt = stmt.order_by(
737
+ models.RSE.rse
738
+ )
739
+
740
+ for row in session.execute(stmt).scalars():
741
+ rse_list.append(row.to_dict())
742
+
743
+ return rse_list
744
+
745
+
746
+ @transactional_session
747
+ def add_rse_attribute(rse_id, key, value, *, session: "Session"):
748
+ """ Adds a RSE attribute.
749
+
750
+ :param rse_id: the rse id.
751
+ :param key: the key name.
752
+ :param value: the value name.
753
+ :param issuer: The issuer account.
754
+ :param session: The database session in use.
755
+
756
+ :returns: True is successful
757
+ """
758
+ try:
759
+ new_rse_attr = models.RSEAttrAssociation(rse_id=rse_id, key=key, value=value)
760
+ new_rse_attr = session.merge(new_rse_attr)
761
+ new_rse_attr.save(session=session)
762
+ except IntegrityError:
763
+ rse = get_rse_name(rse_id=rse_id, session=session)
764
+ raise exception.Duplicate(f"RSE attribute '{key}-{value}' for RSE '{rse}' already exists!")
765
+ return True
766
+
767
+
768
+ @transactional_session
769
+ def del_rse_attribute(rse_id, key, *, session: "Session"):
770
+ """
771
+ Delete a RSE attribute.
772
+
773
+ :param rse_id: the id of the rse.
774
+ :param key: the attribute key.
775
+ :param session: The database session in use.
776
+
777
+ :return: True if RSE attribute was deleted.
778
+ """
779
+ try:
780
+ stmt = select(
781
+ models.RSEAttrAssociation
782
+ ).where(
783
+ models.RSEAttrAssociation.rse_id == rse_id,
784
+ models.RSEAttrAssociation.key == key
785
+ )
786
+ rse_attr = session.execute(stmt).scalar_one()
787
+ except sqlalchemy.orm.exc.NoResultFound:
788
+ raise exception.RSEAttributeNotFound('RSE attribute \'%s\' cannot be found' % key)
789
+ rse_attr.delete(session=session)
790
+ return True
791
+
792
+
793
+ @read_session
794
+ def list_rse_attributes(rse_id: str, use_cache: bool = False, *, session: "Session"):
795
+ """
796
+ List RSE attributes for a RSE.
797
+
798
+ :param rse_id: The RSE id.
799
+ :param use_cache: decides if cache will be used or not
800
+ :param session: The database session in use.
801
+
802
+ :returns: A dictionary with RSE attributes for a RSE.
803
+ """
804
+ cache_key = 'rse_attributes_%s' % rse_id
805
+ if use_cache:
806
+ value = REGION.get(cache_key)
807
+
808
+ if value is not NO_VALUE:
809
+ return value
810
+
811
+ rse_attrs = {}
812
+
813
+ stmt = select(
814
+ models.RSEAttrAssociation
815
+ ).where(
816
+ models.RSEAttrAssociation.rse_id == rse_id
817
+ )
818
+ for attr in session.execute(stmt).scalars():
819
+ rse_attrs[attr.key] = attr.value
820
+
821
+ if use_cache:
822
+ REGION.set(cache_key, rse_attrs)
823
+
824
+ return rse_attrs
825
+
826
+
827
+ @stream_session
828
+ def _fetch_many_rses_attributes(
829
+ rse_id_temp_table,
830
+ keys: Optional[Iterable[str]] = None,
831
+ *,
832
+ session: "Session"
833
+ ) -> Iterator[tuple[str, dict[str, Any]]]:
834
+ """
835
+ Given a temporary table pre-filled with RSE IDs, fetch the attributes of these RSEs.
836
+ It's possible to only fetch a subset of attributes by setting the `keys` parameter.
837
+ """
838
+
839
+ stmt = select(
840
+ rse_id_temp_table.id,
841
+ models.RSEAttrAssociation,
842
+ ).outerjoin_from(
843
+ rse_id_temp_table,
844
+ models.RSEAttrAssociation,
845
+ models.RSEAttrAssociation.rse_id == rse_id_temp_table.id
846
+ ).order_by(
847
+ rse_id_temp_table.id,
848
+ )
849
+
850
+ if keys:
851
+ stmt = stmt.where(
852
+ models.RSEAttrAssociation.key.in_(keys)
853
+ )
854
+
855
+ for rse_id, attribute_list in _group_query_result_by_rse_id(stmt, session=session):
856
+ yield rse_id, {attr.key: attr.value for attr in attribute_list}
857
+
858
+
859
+ @read_session
860
+ def has_rse_attribute(rse_id, key, *, session: "Session"):
861
+ """
862
+ Indicates whether the named key is present for the RSE.
863
+
864
+ :param rse_id: The RSE id.
865
+ :param key: The key for the attribute.
866
+ :param session: The database session in use.
867
+
868
+ :returns: True or False
869
+ """
870
+ stmt = select(
871
+ models.RSEAttrAssociation.value
872
+ ).where(
873
+ models.RSEAttrAssociation.rse_id == rse_id,
874
+ models.RSEAttrAssociation.key == key
875
+ )
876
+ if session.execute(stmt).scalar():
877
+ return True
878
+ return False
879
+
880
+
881
+ @read_session
882
+ def get_rses_with_attribute(key, *, session: "Session"):
883
+ """
884
+ Return all RSEs with a certain attribute.
885
+
886
+ :param key: The key for the attribute.
887
+ :param session: The database session in use.
888
+
889
+ :returns: List of rse dictionaries
890
+ """
891
+ rse_list = []
892
+
893
+ stmt = select(
894
+ models.RSE
895
+ ).where(
896
+ models.RSE.deleted == false()
897
+ ).join(
898
+ models.RSEAttrAssociation,
899
+ and_(
900
+ models.RSEAttrAssociation.rse_id == models.RSE.id,
901
+ models.RSEAttrAssociation.key == key
902
+ )
903
+ )
904
+
905
+ for db_rse in session.execute(stmt).scalars():
906
+ rse_list.append(db_rse.to_dict())
907
+
908
+ return rse_list
909
+
910
+
911
+ @read_session
912
+ def get_rses_with_attribute_value(key, value, vo='def', *, session: "Session"):
913
+ """
914
+ Return all RSEs with a certain attribute.
915
+
916
+ :param key: The key for the attribute.
917
+ :param value: The value for the attribute.
918
+ :param session: The database session in use.
919
+
920
+ :returns: List of rse dictionaries with the rse_id and rse_name
921
+ """
922
+ if vo != 'def':
923
+ cache_key = 'av-%s-%s@%s' % (key, value, vo)
924
+ else:
925
+ cache_key = 'av-%s-%s' % (key, value)
926
+
927
+ result = REGION.get(cache_key)
928
+ if result is NO_VALUE:
929
+
930
+ rse_list = []
931
+
932
+ stmt = select(
933
+ models.RSE.id,
934
+ models.RSE.rse,
935
+ ).where(
936
+ models.RSE.deleted == false(),
937
+ models.RSE.vo == vo
938
+ ).join(
939
+ models.RSEAttrAssociation,
940
+ and_(
941
+ models.RSEAttrAssociation.rse_id == models.RSE.id,
942
+ models.RSEAttrAssociation.key == key,
943
+ models.RSEAttrAssociation.value == value
944
+ )
945
+ )
946
+
947
+ for row in session.execute(stmt):
948
+ rse_list.append({
949
+ 'rse_id': row.id,
950
+ 'rse_name': row.rse
951
+ })
952
+
953
+ REGION.set(cache_key, rse_list)
954
+ return rse_list
955
+
956
+ return result
957
+
958
+
959
+ @read_session
960
+ def get_rse_attribute(rse_id: str, key: str, use_cache: bool = True, *, session: "Session") -> Optional[Union[str, bool]]:
961
+ """
962
+ Retrieve RSE attribute value. If it is not cached, look it up in the
963
+ database. If the value exists and is not cached, it will be added to the
964
+ cache.
965
+
966
+ :param rse_id: The RSE id.
967
+ :param key: The key for the attribute.
968
+ :param session: The database session in use.
969
+
970
+ :returns: The value for the rse attribute, None if it does not exist.
971
+ """
972
+ cache_key = f'rse_attributes_{rse_id}_{key}'
973
+ if use_cache:
974
+ value = REGION.get(cache_key)
975
+
976
+ if value is not NO_VALUE:
977
+ return value
978
+
979
+ stmt = select(
980
+ models.RSEAttrAssociation.value
981
+ ).where(
982
+ models.RSEAttrAssociation.rse_id == rse_id,
983
+ models.RSEAttrAssociation.key == key
984
+ )
985
+ value = session.execute(stmt).scalar_one_or_none()
986
+
987
+ if use_cache:
988
+ REGION.set(cache_key, value)
989
+
990
+ return value
991
+
992
+
993
+ def get_rse_supported_checksums_from_attributes(rse_attributes: dict[str, Any]) -> list[str]:
994
+ """
995
+ Parse the RSE attribute defining the checksum supported by the RSE
996
+ :param rse_attributes: attributes retrieved using list_rse_attributes
997
+ :returns: A list of the names of supported checksums indicated by the specified attributes.
998
+ """
999
+ return parse_checksum_support_attribute(rse_attributes.get(CHECKSUM_KEY, ''))
1000
+
1001
+
1002
+ def parse_checksum_support_attribute(checksum_attribute: str) -> list[str]:
1003
+ """
1004
+ Parse the checksum support RSE attribute.
1005
+ :param checksum_attribute: The value of the RSE attribute storing the checksum value
1006
+
1007
+ :returns: The list of checksums supported by the selected RSE.
1008
+ If the list is empty (aka attribute is not set) it returns all the default checksums.
1009
+ Use 'none' to explicitly tell the RSE does not support any checksum algorithm.
1010
+ """
1011
+
1012
+ if not checksum_attribute:
1013
+ return GLOBALLY_SUPPORTED_CHECKSUMS
1014
+
1015
+ supported_checksum_list = [c.strip() for c in checksum_attribute.split(',') if c.strip()]
1016
+
1017
+ if 'none' in supported_checksum_list:
1018
+ return []
1019
+ else:
1020
+ return supported_checksum_list
1021
+
1022
+
1023
+ @transactional_session
1024
+ def set_rse_usage(rse_id, source, used, free, files=None, *, session: "Session"):
1025
+ """
1026
+ Set RSE usage information.
1027
+
1028
+ :param rse_id: the location id.
1029
+ :param source: The information source, e.g. srm.
1030
+ :param used: the used space in bytes.
1031
+ :param free: the free in bytes.
1032
+ :param files: the number of files
1033
+ :param session: The database session in use.
1034
+
1035
+ :returns: True if successful, otherwise false.
1036
+ """
1037
+ rse_usage = models.RSEUsage(rse_id=rse_id, source=source, used=used, free=free, files=files)
1038
+ # versioned_session(session)
1039
+ rse_usage = session.merge(rse_usage)
1040
+ rse_usage.save(session=session)
1041
+
1042
+ # rse_usage_history = models.RSEUsage.__history_mapper__.class_(rse_id=rse.id, source=source, used=used, free=free)
1043
+ # rse_usage_history.save(session=session)
1044
+
1045
+ return True
1046
+
1047
+
1048
+ @read_session
1049
+ def get_rse_usage(rse_id, source=None, per_account=False, *, session: "Session"):
1050
+ """
1051
+ get rse usage information.
1052
+
1053
+ :param rse_id: The RSE id.
1054
+ :param source: The information source, e.g. srm.
1055
+ :param session: The database session in use.
1056
+ :param per_account: Boolean whether the usage should be also calculated per account or not.
1057
+
1058
+ :returns: List of RSE usage data.
1059
+ """
1060
+
1061
+ stmt_rse_usage = select(
1062
+ models.RSEUsage
1063
+ ).where(
1064
+ models.RSEUsage.rse_id == rse_id
1065
+ )
1066
+
1067
+ if source:
1068
+ stmt_rse_usage = stmt_rse_usage.where(
1069
+ models.RSEUsage.source == source
1070
+ )
1071
+
1072
+ db_usages = session.execute(stmt_rse_usage).scalars()
1073
+ return _format_get_rse_usage(rse_id=rse_id, db_usages=db_usages, per_account=per_account, session=session)
1074
+
1075
+
1076
+ def _format_get_rse_usage(
1077
+ rse_id: str,
1078
+ db_usages: Iterable[models.RSEUsage],
1079
+ per_account: bool,
1080
+ *,
1081
+ session: "Session"
1082
+ ) -> list[dict[str, Any]]:
1083
+
1084
+ usage = list()
1085
+ for db_usage in db_usages:
1086
+ total = (db_usage.free or 0) + (db_usage.used or 0)
1087
+ rse_usage = {'rse_id': rse_id,
1088
+ 'source': db_usage.source,
1089
+ 'used': db_usage.used,
1090
+ 'free': db_usage.free,
1091
+ 'total': total,
1092
+ 'files': db_usage.files,
1093
+ 'updated_at': db_usage.updated_at}
1094
+ if per_account and db_usage.source == 'rucio':
1095
+ stmt_account_usage = select(
1096
+ models.AccountUsage
1097
+ ).where(
1098
+ models.AccountUsage.rse_id == rse_id
1099
+ )
1100
+ account_usages = []
1101
+ for row in session.execute(stmt_account_usage).scalars():
1102
+ if row.bytes != 0:
1103
+ percentage = round(float(row.bytes) / float(total) * 100, 2) if total else 0
1104
+ account_usages.append({'used': row.bytes, 'account': row.account, 'percentage': percentage})
1105
+ account_usages.sort(key=lambda x: x['used'], reverse=True)
1106
+ rse_usage['account_usages'] = account_usages
1107
+ usage.append(rse_usage)
1108
+ return usage
1109
+
1110
+
1111
+ @transactional_session
1112
+ def set_rse_limits(rse_id: str, name: str, value: int, *, session: 'Session') -> bool:
1113
+ """
1114
+ Set RSE limits.
1115
+
1116
+ :param rse_id: The RSE id.
1117
+ :param name: The name of the limit.
1118
+ :param value: The feature value.
1119
+ :param session: The database session in use.
1120
+
1121
+ :returns: True if successful, otherwise false.
1122
+ """
1123
+ rse_limit = models.RSELimit(rse_id=rse_id, name=name, value=value)
1124
+ rse_limit = session.merge(rse_limit)
1125
+ rse_limit.save(session=session)
1126
+ return True
1127
+
1128
+
1129
+ @read_session
1130
+ def get_rse_limits(rse_id: str, name: Optional[str] = None, *, session: 'Session') -> dict[str, int]:
1131
+ """
1132
+ Get RSE limits.
1133
+
1134
+ :param rse_id: The RSE id.
1135
+ :param name: A Limit name.
1136
+
1137
+ :returns: A dictionary with the limits {'limit.name': limit.value}.
1138
+ """
1139
+
1140
+ stmt = select(
1141
+ models.RSELimit
1142
+ ).where(
1143
+ models.RSELimit.rse_id == rse_id
1144
+ )
1145
+ if name:
1146
+ stmt = stmt.where(
1147
+ models.RSELimit.name == name
1148
+ )
1149
+ return {limit.name: limit.value for limit in session.execute(stmt).scalars()}
1150
+
1151
+
1152
+ @transactional_session
1153
+ def delete_rse_limits(rse_id: str, name: "Optional[str]" = None, *, session: 'Session') -> None:
1154
+ """
1155
+ Delete RSE limit.
1156
+
1157
+ :param rse_id: The RSE id.
1158
+ :param name: The name of the limit.
1159
+ """
1160
+ try:
1161
+ stmt = delete(
1162
+ models.RSELimit
1163
+ ).where(
1164
+ models.RSELimit.rse_id == rse_id,
1165
+ )
1166
+ if name is not None:
1167
+ stmt = stmt.where(
1168
+ models.RSELimit.name == name
1169
+ )
1170
+ session.execute(stmt)
1171
+ except IntegrityError as error:
1172
+ raise exception.RucioException(error.args)
1173
+
1174
+
1175
+ def _sanitize_rse_transfer_limit_dict(limit_dict):
1176
+ if limit_dict['activity'] == 'all_activities':
1177
+ limit_dict['activity'] = None
1178
+ return limit_dict
1179
+
1180
+
1181
+ @read_session
1182
+ def get_rse_transfer_limits(rse_id, activity=None, *, session: "Session"):
1183
+ """
1184
+ Get RSE transfer limits.
1185
+
1186
+ :param rse_id: The RSE id.
1187
+ :param activity: The activity.
1188
+
1189
+ :returns: A dictionary with the limits {'limit.direction': {'limit.activity': limit}}.
1190
+ """
1191
+ try:
1192
+ stmt = select(
1193
+ models.TransferLimit
1194
+ ).join_from(
1195
+ models.RSETransferLimit,
1196
+ models.TransferLimit,
1197
+ and_(models.RSETransferLimit.limit_id == models.TransferLimit.id,
1198
+ models.RSETransferLimit.rse_id == rse_id)
1199
+ )
1200
+ if activity:
1201
+ stmt = stmt.where(
1202
+ or_(
1203
+ models.TransferLimit.activity == activity,
1204
+ models.TransferLimit.activity == 'all_activities',
1205
+ )
1206
+ )
1207
+
1208
+ limits = {}
1209
+ for limit in session.execute(stmt).scalars():
1210
+ limit_dict = _sanitize_rse_transfer_limit_dict(limit.to_dict())
1211
+ limits.setdefault(limit_dict['direction'], {}).setdefault(limit_dict['activity'], limit_dict)
1212
+
1213
+ return limits
1214
+ except IntegrityError as error:
1215
+ raise exception.RucioException(error.args)
1216
+
1217
+
1218
+ @stream_session
1219
+ def list_rse_usage_history(rse_id, source=None, *, session: "Session"):
1220
+ """
1221
+ List RSE usage history information.
1222
+
1223
+ :param rse_id: The RSE id.
1224
+ :param source: The source of the usage information (srm, rucio).
1225
+ :param session: The database session in use.
1226
+
1227
+ :returns: A list of historic RSE usage.
1228
+ """
1229
+ stmt = select(
1230
+ models.RSEUsageHistory
1231
+ ).where(
1232
+ models.RSEUsageHistory.rse_id == rse_id
1233
+ ).order_by(
1234
+ desc(models.RSEUsageHistory.updated_at)
1235
+ )
1236
+ if source:
1237
+ stmt = stmt.where(
1238
+ models.RSEUsageHistory.source == source
1239
+ )
1240
+
1241
+ rse = get_rse_name(rse_id=rse_id, session=session)
1242
+ for usage in session.execute(stmt).yield_per(5).scalars():
1243
+ yield ({'rse_id': rse_id,
1244
+ 'rse': rse,
1245
+ 'source': usage.source,
1246
+ 'used': usage.used if usage.used else 0,
1247
+ 'total': usage.used if usage.used else 0 + usage.free if usage.free else 0,
1248
+ 'free': usage.free if usage.free else 0,
1249
+ 'updated_at': usage.updated_at})
1250
+
1251
+
1252
+ @transactional_session
1253
+ def add_protocol(
1254
+ rse_id: str,
1255
+ parameter: dict[str, Any],
1256
+ *,
1257
+ session: "Session"
1258
+ ) -> models.RSEProtocols:
1259
+ """
1260
+ Add a protocol to an existing RSE.
1261
+
1262
+ :param rse_id: the ID of the new RSE.
1263
+ :param parameter: parameters of the new protocol entry.
1264
+ :param session: The database session in use.
1265
+
1266
+ :raises RSENotFound: If RSE is not found.
1267
+ :raises RSEOperationNotSupported: If no scheme supported the requested operation for the given RSE.
1268
+ :raises RSEProtocolDomainNotSupported: If an undefined domain was provided.
1269
+ :raises RSEProtocolPriorityError: If the provided priority for the scheme is to big or below zero.
1270
+ :raises Duplicate: If scheme with identifier, hostname and port already exists
1271
+ for the given RSE.
1272
+ """
1273
+
1274
+ rse = ""
1275
+ try:
1276
+ rse = get_rse_name(rse_id=rse_id, session=session, include_deleted=False)
1277
+ except exception.RSENotFound:
1278
+ raise exception.RSENotFound('RSE id \'%s\' not found' % rse_id)
1279
+ # Insert new protocol entry
1280
+ parameter['rse_id'] = rse_id
1281
+
1282
+ # Default values
1283
+ parameter['port'] = parameter.get('port', 0)
1284
+ parameter['hostname'] = parameter.get('hostname', 'localhost')
1285
+
1286
+ # Transform nested domains to match DB schema e.g. [domains][lan][read] => [read_lan]
1287
+ if 'domains' in parameter:
1288
+ for domain in parameter['domains']:
1289
+ if domain not in utils.rse_supported_protocol_domains():
1290
+ raise exception.RSEProtocolDomainNotSupported(f"The protocol domain '{domain}' is not defined in the schema.")
1291
+ for op in parameter['domains'][domain]:
1292
+ if op not in utils.rse_supported_protocol_operations():
1293
+ raise exception.RSEOperationNotSupported(f"Operation '{op}' not defined in schema.")
1294
+ op_name = op if op.startswith('third_party_copy') else f'{op}_{domain}'.lower()
1295
+ priority = parameter['domains'][domain][op]
1296
+ if (type(priority) is not int or priority < 0) and priority is not None:
1297
+ raise exception.RSEProtocolPriorityError(f"The provided priority ({priority}) for operation '{op}' in domain '{domain}' is not supported.")
1298
+ parameter[op_name] = priority
1299
+ del parameter['domains']
1300
+
1301
+ if ('extended_attributes' in parameter) and parameter['extended_attributes']:
1302
+ try:
1303
+ parameter['extended_attributes'] = json.dumps(parameter['extended_attributes'], separators=(',', ':'))
1304
+ except ValueError:
1305
+ pass # String is not JSON
1306
+
1307
+ if parameter['scheme'] == 'srm':
1308
+ if ('extended_attributes' not in parameter) or ('web_service_path' not in parameter['extended_attributes']):
1309
+ raise exception.InvalidObject('Missing values! For SRM, extended_attributes and web_service_path must be specified')
1310
+
1311
+ try:
1312
+ new_protocol = models.RSEProtocols()
1313
+ new_protocol.update(parameter)
1314
+ new_protocol.save(session=session)
1315
+ except (IntegrityError, FlushError, OperationalError) as error:
1316
+ if ('UNIQUE constraint failed' in error.args[0]) or ('conflicts with persistent instance' in error.args[0]) \
1317
+ or match('.*IntegrityError.*ORA-00001: unique constraint.*RSE_PROTOCOLS_PK.*violated.*', error.args[0]) \
1318
+ or match('.*IntegrityError.*1062.*Duplicate entry.*for key.*', error.args[0]) \
1319
+ or match('.*IntegrityError.*duplicate key value violates unique constraint.*', error.args[0]) \
1320
+ or match('.*UniqueViolation.*duplicate key value violates unique constraint.*', error.args[0]) \
1321
+ or match('.*IntegrityError.*columns.*are not unique.*', error.args[0]):
1322
+ raise exception.Duplicate('Protocol \'%s\' on port %s already registered for \'%s\' with hostname \'%s\'.' % (parameter['scheme'], parameter['port'], rse, parameter['hostname']))
1323
+ elif 'may not be NULL' in error.args[0] \
1324
+ or match('.*IntegrityError.*ORA-01400: cannot insert NULL into.*RSE_PROTOCOLS.*IMPL.*', error.args[0]) \
1325
+ or match('.*IntegrityError.*Column.*cannot be null.*', error.args[0]) \
1326
+ or match('.*IntegrityError.*null value in column.*violates not-null constraint.*', error.args[0]) \
1327
+ or match('.*IntegrityError.*NOT NULL constraint failed.*', error.args[0]) \
1328
+ or match('.*NotNullViolation.*null value in column.*violates not-null constraint.*', error.args[0]) \
1329
+ or match('.*OperationalError.*cannot be null.*', error.args[0]):
1330
+ raise exception.InvalidObject('Missing values!')
1331
+
1332
+ raise exception.RucioException(error.args)
1333
+ return new_protocol
1334
+
1335
+
1336
+ @read_session
1337
+ def get_rse_protocols(rse_id, schemes=None, *, session: "Session") -> types.RSESettingsDict:
1338
+ """
1339
+ Returns protocol information. Parameter combinations are: (operation OR default) XOR scheme.
1340
+
1341
+ :param rse_id: The id of the rse.
1342
+ :param schemes: a list of schemes to filter by.
1343
+ :param session: The database session.
1344
+
1345
+ :returns: A dict with RSE information and supported protocols
1346
+
1347
+ :raises RSENotFound: If RSE is not found.
1348
+ """
1349
+
1350
+ _rse = get_rse(rse_id=rse_id, session=session)
1351
+ if not _rse:
1352
+ raise exception.RSENotFound('RSE with id \'%s\' not found' % rse_id)
1353
+
1354
+ terms = [models.RSEProtocols.rse_id == rse_id]
1355
+ if schemes:
1356
+ if not type(schemes) is list:
1357
+ schemes = [schemes]
1358
+ terms.extend([models.RSEProtocols.scheme.in_(schemes)])
1359
+
1360
+ stmt = select(
1361
+ models.RSEProtocols
1362
+ ).where(
1363
+ *terms
1364
+ )
1365
+
1366
+ _protocols = session.execute(stmt).scalars().all()
1367
+ return _format_get_rse_protocols(rse=_rse, db_protocols=_protocols, session=session)
1368
+
1369
+
1370
+ def _format_get_rse_protocols(
1371
+ rse: "models.RSE | dict[str, Any]",
1372
+ db_protocols: Iterable[models.RSEProtocols],
1373
+ rse_attributes: Optional[dict[str, Any]] = None,
1374
+ *,
1375
+ session: "Session"
1376
+ ) -> types.RSESettingsDict:
1377
+ _rse = rse
1378
+ if rse_attributes:
1379
+ lfn2pfn_algorithm = rse_attributes.get('lfn2pfn_algorithm')
1380
+ else:
1381
+ lfn2pfn_algorithm = get_rse_attribute(_rse['id'], 'lfn2pfn_algorithm', session=session)
1382
+ # Resolve LFN2PFN default algorithm as soon as possible. This way, we can send back the actual
1383
+ # algorithm name in response to REST queries.
1384
+ if not lfn2pfn_algorithm:
1385
+ lfn2pfn_algorithm = get_lfn2pfn_algorithm_default()
1386
+
1387
+ # Copy verify_checksum from the attributes, later: assume True if not specified
1388
+ if rse_attributes:
1389
+ verify_checksum = rse_attributes.get('verify_checksum')
1390
+ else:
1391
+ verify_checksum = get_rse_attribute(_rse['id'], 'verify_checksum', session=session)
1392
+
1393
+ # Copy sign_url from the attributes
1394
+ if rse_attributes:
1395
+ sign_url = rse_attributes.get('sign_url')
1396
+ else:
1397
+ sign_url = get_rse_attribute(_rse['id'], 'sign_url', session=session)
1398
+
1399
+ info = {'availability_delete': _rse['availability_delete'],
1400
+ 'availability_read': _rse['availability_read'],
1401
+ 'availability_write': _rse['availability_write'],
1402
+ 'credentials': None,
1403
+ 'deterministic': _rse['deterministic'],
1404
+ 'domain': utils.rse_supported_protocol_domains(),
1405
+ 'id': _rse['id'],
1406
+ 'lfn2pfn_algorithm': lfn2pfn_algorithm,
1407
+ 'protocols': list(),
1408
+ 'qos_class': _rse['qos_class'],
1409
+ 'rse': _rse['rse'],
1410
+ 'rse_type': _rse['rse_type'].name,
1411
+ 'sign_url': sign_url,
1412
+ 'staging_area': _rse['staging_area'],
1413
+ 'verify_checksum': verify_checksum if verify_checksum is not None else True,
1414
+ 'volatile': _rse['volatile']}
1415
+
1416
+ for op in utils.rse_supported_protocol_operations():
1417
+ info['%s_protocol' % op] = 1 # 1 indicates the default protocol
1418
+
1419
+ for row in db_protocols:
1420
+ p = {'hostname': row.hostname,
1421
+ 'scheme': row.scheme,
1422
+ 'port': row.port,
1423
+ 'prefix': row.prefix if row.prefix is not None else '',
1424
+ 'impl': row.impl,
1425
+ 'domains': {
1426
+ 'lan': {'read': row.read_lan,
1427
+ 'write': row.write_lan,
1428
+ 'delete': row.delete_lan},
1429
+ 'wan': {'read': row.read_wan,
1430
+ 'write': row.write_wan,
1431
+ 'delete': row.delete_wan,
1432
+ 'third_party_copy_read': row.third_party_copy_read,
1433
+ 'third_party_copy_write': row.third_party_copy_write}
1434
+ },
1435
+ 'extended_attributes': row.extended_attributes}
1436
+
1437
+ try:
1438
+ p['extended_attributes'] = json.load(StringIO(p['extended_attributes']))
1439
+ except ValueError:
1440
+ pass # If value is not a JSON string
1441
+
1442
+ info['protocols'].append(p)
1443
+ info['protocols'] = sorted(info['protocols'], key=lambda p: (p['hostname'], p['scheme'], p['port']))
1444
+ return info
1445
+
1446
+
1447
+ @read_session
1448
+ def get_rse_info(rse_id, *, session: "Session") -> types.RSESettingsDict:
1449
+ """
1450
+ For historical reasons, related to usage of rsemanager, "rse_info" is equivalent to
1451
+ a cached call to get_rse_protocols without any schemes set.
1452
+
1453
+ :param rse_id: The id of the rse.
1454
+ :param session: The database session.
1455
+ :returns: A dict with RSE information and supported protocols
1456
+ """
1457
+ key = 'rse_info_%s' % rse_id
1458
+ result = REGION.get(key)
1459
+ if result is NO_VALUE:
1460
+ result = get_rse_protocols(rse_id=rse_id, session=session)
1461
+ REGION.set(key, result)
1462
+ return result
1463
+
1464
+
1465
+ @transactional_session
1466
+ def update_protocols(
1467
+ rse_id: str,
1468
+ scheme: str,
1469
+ data: dict[str, Any],
1470
+ hostname: str,
1471
+ port: int,
1472
+ *,
1473
+ session: "Session"
1474
+ ) -> None:
1475
+ """
1476
+ Update an existing protocol entry for an RSE.
1477
+
1478
+ :param rse_id: the ID of the RSE.
1479
+ :param scheme: Protocol identifier.
1480
+ :param data: Dict with new values (keys must match column names in the database).
1481
+ :param hostname: Hostname defined for the scheme, used if more than one scheme
1482
+ is registered with the same identifier.
1483
+ :param port: The port registered for the hostname, used if more than one scheme
1484
+ is registered with the same identifier and hostname.
1485
+ :param session: The database session in use.
1486
+
1487
+ :raises RSENotFound: If RSE is not found.
1488
+ :raises RSEProtocolNotSupported: If no matching protocol was found for the given RSE.
1489
+ :raises RSEOperationNotSupported: If no protocol supported the requested operation for the given RSE.
1490
+ :raises RSEProtocolDomainNotSupported: If an undefined domain was provided.
1491
+ :raises RSEProtocolPriorityError: If the provided priority for the protocol is too big or below zero.
1492
+ :raises KeyNotFound: Invalid data for update provided.
1493
+ :raises Duplicate: If protocol with identifier, hostname and port already exists
1494
+ for the given RSE.
1495
+ """
1496
+
1497
+ # Transform nested domains to match DB schema e.g. [domains][lan][read] => [read_lan]
1498
+ if 'domains' in data:
1499
+ for domain in data['domains']:
1500
+ if domain not in utils.rse_supported_protocol_domains():
1501
+ raise exception.RSEProtocolDomainNotSupported(f"The protocol domain '{domain}' is not defined in the schema.")
1502
+ for op in data['domains'][domain]:
1503
+ if op not in utils.rse_supported_protocol_operations():
1504
+ raise exception.RSEOperationNotSupported(f"Operation '{op}' not defined in schema.")
1505
+ op_name = op if op.startswith('third_party_copy') else f'{op}_{domain}'.lower()
1506
+ priority = data['domains'][domain][op]
1507
+ if (type(priority) is not int or priority < 0) and priority is not None:
1508
+ raise exception.RSEProtocolPriorityError(f"The provided priority ({priority}) for operation '{op}' in domain '{domain}' is not supported.")
1509
+ data[op_name] = priority
1510
+ del data['domains']
1511
+
1512
+ if 'extended_attributes' in data:
1513
+ try:
1514
+ data['extended_attributes'] = json.dumps(data['extended_attributes'], separators=(',', ':'))
1515
+ except ValueError:
1516
+ pass # String is not JSON
1517
+
1518
+ try:
1519
+ rse = get_rse_name(rse_id=rse_id, session=session, include_deleted=False)
1520
+ except exception.RSENotFound:
1521
+ raise exception.RSENotFound('RSE with id \'%s\' not found' % rse_id)
1522
+
1523
+ terms = [models.RSEProtocols.rse_id == rse_id,
1524
+ models.RSEProtocols.scheme == scheme,
1525
+ models.RSEProtocols.hostname == hostname,
1526
+ models.RSEProtocols.port == port]
1527
+
1528
+ try:
1529
+ stmt = select(
1530
+ models.RSEProtocols
1531
+ ).where(
1532
+ *terms
1533
+ )
1534
+ up = session.execute(stmt).scalar()
1535
+ if up is None:
1536
+ msg = 'RSE \'%s\' does not support protocol \'%s\' for hostname \'%s\' on port \'%s\'' % (rse, scheme, hostname, port)
1537
+ raise exception.RSEProtocolNotSupported(msg)
1538
+ up.update(data, flush=True, session=session)
1539
+ except (IntegrityError, OperationalError) as error:
1540
+ if 'UNIQUE'.lower() in error.args[0].lower() or 'Duplicate' in error.args[0]: # Covers SQLite, Oracle and MySQL error
1541
+ raise exception.Duplicate('Protocol \'%s\' on port %s already registered for \'%s\' with hostname \'%s\'.' % (scheme, port, rse, hostname))
1542
+ elif 'may not be NULL' in error.args[0] or "cannot be null" in error.args[0]:
1543
+ raise exception.InvalidObject('Missing values: %s' % error.args[0])
1544
+ raise error
1545
+ except DatabaseError as error:
1546
+ if match('.*DatabaseError.*ORA-01407: cannot update .*RSE_PROTOCOLS.*IMPL.*to NULL.*', error.args[0]):
1547
+ raise exception.InvalidObject('Invalid values !')
1548
+ raise error
1549
+
1550
+
1551
+ @transactional_session
1552
+ def del_protocols(
1553
+ rse_id: str,
1554
+ scheme: str,
1555
+ hostname: Optional[str] = None,
1556
+ port: Optional[int] = None,
1557
+ *,
1558
+ session: "Session"
1559
+ ) -> None:
1560
+ """
1561
+ Delete one or more existing protocol entries for an RSE.
1562
+
1563
+ :param rse_id: the ID of the RSE.
1564
+ :param scheme: Protocol identifier.
1565
+ :param hostname: Hostname defined for the scheme, used if more than one scheme
1566
+ is registered with the same identifier.
1567
+ :param port: The port registered for the hostname, used if more than one scheme
1568
+ is registered with the same identifier and hostname.
1569
+ :param session: The database session in use.
1570
+
1571
+ :raises RSENotFound: If RSE is not found.
1572
+ :raises RSEProtocolNotSupported: If no matching scheme was found for the given RSE.
1573
+ """
1574
+ try:
1575
+ rse_name = get_rse_name(rse_id=rse_id, session=session, include_deleted=False)
1576
+ except exception.RSENotFound:
1577
+ raise exception.RSENotFound('RSE \'%s\' not found' % rse_id)
1578
+ terms = [models.RSEProtocols.rse_id == rse_id, models.RSEProtocols.scheme == scheme]
1579
+ if hostname is not None:
1580
+ terms.append(models.RSEProtocols.hostname == hostname)
1581
+ if port is not None:
1582
+ terms.append(models.RSEProtocols.port == port)
1583
+ stmt = select(
1584
+ models.RSEProtocols
1585
+ ).where(
1586
+ *terms
1587
+ )
1588
+ p = session.execute(stmt).scalars().all()
1589
+
1590
+ if not p:
1591
+ msg = 'RSE \'%s\' does not support protocol \'%s\'' % (rse_name, scheme)
1592
+ msg += ' for hostname \'%s\'' % hostname if hostname else ''
1593
+ msg += ' on port \'%s\'' % port if port else ''
1594
+ raise exception.RSEProtocolNotSupported(msg)
1595
+
1596
+ for row in p:
1597
+ row.delete(session=session)
1598
+
1599
+
1600
+ MUTABLE_RSE_PROPERTIES = {
1601
+ 'name',
1602
+ 'availability_read',
1603
+ 'availability_write',
1604
+ 'availability_delete',
1605
+ 'latitude',
1606
+ 'longitude',
1607
+ 'time_zone',
1608
+ 'rse_type',
1609
+ 'volatile',
1610
+ 'deterministic',
1611
+ 'region_code',
1612
+ 'country_name',
1613
+ 'city',
1614
+ 'staging_area',
1615
+ 'qos_class',
1616
+ 'continent',
1617
+ 'availability'
1618
+ }
1619
+
1620
+
1621
+ @transactional_session
1622
+ def update_rse(rse_id: str, parameters: dict[str, Any], *, session: "Session"):
1623
+ """
1624
+ Update RSE properties like availability or name.
1625
+
1626
+ :param rse_id: the id of the new rse.
1627
+ :param parameters: A dictionary with property (name, read, write, delete as keys).
1628
+ :param session: The database session in use.
1629
+
1630
+ :raises RSENotFound: If RSE is not found.
1631
+ :raises InputValidationError: If a parameter does not exist. Nothing will be added then.
1632
+ """
1633
+ for key in parameters.keys():
1634
+ if key not in MUTABLE_RSE_PROPERTIES:
1635
+ raise exception.InputValidationError(f"The key '{key}' does not exist for RSE properties.")
1636
+
1637
+ try:
1638
+ stmt = select(
1639
+ models.RSE
1640
+ ).where(
1641
+ models.RSE.id == rse_id
1642
+ )
1643
+ db_rse = session.execute(stmt).scalar_one()
1644
+ except sqlalchemy.orm.exc.NoResultFound:
1645
+ raise exception.RSENotFound('RSE with ID \'%s\' cannot be found' % rse_id)
1646
+ old_rse_name = db_rse.rse
1647
+
1648
+ param = {}
1649
+
1650
+ if 'availability' in parameters:
1651
+ availability = Availability.from_integer(parameters['availability'])
1652
+ param['availability_read'] = availability.read
1653
+ param['availability_write'] = availability.write
1654
+ param['availability_delete'] = availability.delete
1655
+
1656
+ for key in parameters:
1657
+ if key == 'name' and parameters['name'] != old_rse_name: # Needed due to wrongly setting name in pre1.22.7 clients
1658
+ param['rse'] = parameters['name']
1659
+ elif key in MUTABLE_RSE_PROPERTIES - {'name'}:
1660
+ param[key] = parameters[key]
1661
+
1662
+ # handle null-able keys
1663
+ for key in parameters:
1664
+ if key in ['qos_class']:
1665
+ if param[key] and param[key].lower() in ['', 'none', 'null']:
1666
+ param[key] = None
1667
+
1668
+ # handle rse settings
1669
+ for setting in set(param.keys()).intersection(RSE_SETTINGS):
1670
+ if has_rse_attribute(rse_id, setting, session=session):
1671
+ del_rse_attribute(rse_id, setting, session=session)
1672
+ add_rse_attribute(rse_id, setting, param[setting], session=session)
1673
+
1674
+ db_rse.update(param, session=session)
1675
+ if 'rse' in param:
1676
+ add_rse_attribute(rse_id=rse_id, key=parameters['name'], value=True, session=session)
1677
+ del_rse_attribute(rse_id=rse_id, key=old_rse_name, session=session)
1678
+
1679
+
1680
+ @read_session
1681
+ def export_rse(rse_id, *, session: "Session"):
1682
+ """
1683
+ Get the internal representation of an RSE.
1684
+
1685
+ :param rse_id: The RSE id.
1686
+
1687
+ :returns: A dictionary with the internal representation of an RSE.
1688
+ """
1689
+
1690
+ stmt = select(
1691
+ models.RSE
1692
+ ).where(
1693
+ models.RSE.id == rse_id
1694
+ )
1695
+
1696
+ rse_data = {}
1697
+ for _rse in session.execute(stmt).scalars():
1698
+ for k, v in _rse:
1699
+ rse_data[k] = v
1700
+
1701
+ rse_data.pop('continent')
1702
+ rse_data.pop('ASN')
1703
+ rse_data.pop('ISP')
1704
+ rse_data.pop('deleted')
1705
+ rse_data.pop('deleted_at')
1706
+
1707
+ # get RSE attributes
1708
+ rse_data['attributes'] = list_rse_attributes(rse_id=rse_id, session=session)
1709
+
1710
+ protocols = get_rse_protocols(rse_id=rse_id, session=session)
1711
+ rse_data['lfn2pfn_algorithm'] = protocols.get('lfn2pfn_algorithm')
1712
+ rse_data['verify_checksum'] = protocols.get('verify_checksum')
1713
+ rse_data['credentials'] = protocols.get('credentials')
1714
+ rse_data['availability_delete'] = protocols.get('availability_delete')
1715
+ rse_data['availability_write'] = protocols.get('availability_write')
1716
+ rse_data['availability_read'] = protocols.get('availability_read')
1717
+ rse_data['protocols'] = protocols.get('protocols')
1718
+
1719
+ # get RSE limits
1720
+ limits = get_rse_limits(rse_id=rse_id, session=session)
1721
+ rse_data['MinFreeSpace'] = limits.get('MinFreeSpace')
1722
+
1723
+ return rse_data
1724
+
1725
+
1726
+ @transactional_session
1727
+ def add_qos_policy(rse_id, qos_policy, *, session: "Session"):
1728
+ """
1729
+ Add a QoS policy from an RSE.
1730
+
1731
+ :param rse_id: The id of the RSE.
1732
+ :param qos_policy: The QoS policy to add.
1733
+ :param session: The database session in use.
1734
+
1735
+ :raises Duplicate: If the QoS policy already exists.
1736
+ :returns: True if successful, except otherwise.
1737
+ """
1738
+
1739
+ try:
1740
+ new_qos_policy = models.RSEQoSAssociation()
1741
+ new_qos_policy.update({'rse_id': rse_id,
1742
+ 'qos_policy': qos_policy})
1743
+ new_qos_policy.save(session=session)
1744
+ except (IntegrityError, FlushError, OperationalError) as error:
1745
+ if ('UNIQUE constraint failed' in error.args[0]) or ('conflicts with persistent instance' in error.args[0]) \
1746
+ or match('.*IntegrityError.*ORA-00001: unique constraint.*RSE_PROTOCOLS_PK.*violated.*', error.args[0]) \
1747
+ or match('.*IntegrityError.*1062.*Duplicate entry.*for key.*', error.args[0]) \
1748
+ or match('.*IntegrityError.*duplicate key value violates unique constraint.*', error.args[0])\
1749
+ or match('.*UniqueViolation.*duplicate key value violates unique constraint.*', error.args[0])\
1750
+ or match('.*IntegrityError.*columns.*are not unique.*', error.args[0]):
1751
+ raise exception.Duplicate('QoS policy %s already exists!' % qos_policy)
1752
+ except DatabaseError as error:
1753
+ raise exception.RucioException(error.args)
1754
+
1755
+ return True
1756
+
1757
+
1758
+ @transactional_session
1759
+ def delete_qos_policy(rse_id, qos_policy, *, session: "Session"):
1760
+ """
1761
+ Delete a QoS policy from an RSE.
1762
+
1763
+ :param rse_id: The id of the RSE.
1764
+ :param qos_policy: The QoS policy to delete.
1765
+ :param session: The database session in use.
1766
+
1767
+ :returns: True if successful, silent failure if QoS policy does not exist.
1768
+ """
1769
+
1770
+ try:
1771
+ stmt = delete(
1772
+ models.RSEQoSAssociation
1773
+ ).where(
1774
+ models.RSEQoSAssociation.rse_id == rse_id,
1775
+ models.RSEQoSAssociation.qos_policy == qos_policy
1776
+ )
1777
+ session.execute(stmt)
1778
+ except DatabaseError as error:
1779
+ raise exception.RucioException(error.args)
1780
+
1781
+ return True
1782
+
1783
+
1784
+ @read_session
1785
+ def list_qos_policies(rse_id, *, session: "Session"):
1786
+ """
1787
+ List all QoS policies of an RSE.
1788
+
1789
+ :param rse_id: The id of the RSE.
1790
+ :param session: The database session in use.
1791
+
1792
+ :returns: List containing all QoS policies.
1793
+ """
1794
+
1795
+ qos_policies = []
1796
+ try:
1797
+ stmt = select(
1798
+ models.RSEQoSAssociation.qos_policy
1799
+ ).where(
1800
+ models.RSEQoSAssociation.rse_id == rse_id
1801
+ )
1802
+ for qos_policy in session.execute(stmt).scalars():
1803
+ qos_policies.append(qos_policy)
1804
+ except DatabaseError as error:
1805
+ raise exception.RucioException(error.args)
1806
+
1807
+ return qos_policies
1808
+
1809
+
1810
+ @transactional_session
1811
+ def fill_rse_expired(rse_id, *, session: "Session"):
1812
+ """
1813
+ Fill the rse_usage for source expired
1814
+
1815
+ :param rse_id: The RSE id.
1816
+
1817
+ :returns: True if successful, except otherwise.
1818
+ """
1819
+ stmt = select(
1820
+ func.sum(models.RSEFileAssociation.bytes).label("bytes"),
1821
+ func.count().label("length")
1822
+ ).with_hint(
1823
+ models.RSEFileAssociation, "INDEX(REPLICAS REPLICAS_RSE_ID_TOMBSTONE_IDX)", 'oracle'
1824
+ ).where(
1825
+ models.RSEFileAssociation.tombstone < datetime.utcnow(),
1826
+ models.RSEFileAssociation.lock_cnt == 0,
1827
+ models.RSEFileAssociation.rse_id == rse_id,
1828
+ models.RSEFileAssociation.state.in_((ReplicaState.AVAILABLE, ReplicaState.UNAVAILABLE, ReplicaState.BAD))
1829
+ )
1830
+
1831
+ sum_bytes, sum_files = session.execute(stmt).one()
1832
+ models.RSEUsage(rse_id=rse_id,
1833
+ used=sum_bytes,
1834
+ files=sum_files,
1835
+ source='expired').save(session=session)