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