rucio 35.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

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