rucio 32.8.6__py3-none-any.whl → 35.8.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (502) hide show
  1. rucio/__init__.py +0 -1
  2. rucio/alembicrevision.py +1 -2
  3. rucio/client/__init__.py +0 -1
  4. rucio/client/accountclient.py +45 -25
  5. rucio/client/accountlimitclient.py +37 -9
  6. rucio/client/baseclient.py +199 -154
  7. rucio/client/client.py +2 -3
  8. rucio/client/configclient.py +19 -6
  9. rucio/client/credentialclient.py +9 -4
  10. rucio/client/didclient.py +238 -63
  11. rucio/client/diracclient.py +13 -5
  12. rucio/client/downloadclient.py +162 -51
  13. rucio/client/exportclient.py +4 -4
  14. rucio/client/fileclient.py +3 -4
  15. rucio/client/importclient.py +4 -4
  16. rucio/client/lifetimeclient.py +21 -5
  17. rucio/client/lockclient.py +18 -8
  18. rucio/client/{metaclient.py → metaconventionsclient.py} +18 -15
  19. rucio/client/pingclient.py +0 -1
  20. rucio/client/replicaclient.py +15 -5
  21. rucio/client/requestclient.py +35 -19
  22. rucio/client/rseclient.py +133 -51
  23. rucio/client/ruleclient.py +29 -22
  24. rucio/client/scopeclient.py +8 -6
  25. rucio/client/subscriptionclient.py +47 -35
  26. rucio/client/touchclient.py +8 -4
  27. rucio/client/uploadclient.py +166 -82
  28. rucio/common/__init__.py +0 -1
  29. rucio/common/cache.py +4 -4
  30. rucio/common/config.py +52 -47
  31. rucio/common/constants.py +69 -2
  32. rucio/common/constraints.py +0 -1
  33. rucio/common/didtype.py +24 -22
  34. rucio/common/dumper/__init__.py +70 -41
  35. rucio/common/dumper/consistency.py +26 -22
  36. rucio/common/dumper/data_models.py +16 -23
  37. rucio/common/dumper/path_parsing.py +0 -1
  38. rucio/common/exception.py +281 -222
  39. rucio/common/extra.py +0 -1
  40. rucio/common/logging.py +54 -38
  41. rucio/common/pcache.py +122 -101
  42. rucio/common/plugins.py +153 -0
  43. rucio/common/policy.py +4 -4
  44. rucio/common/schema/__init__.py +17 -10
  45. rucio/common/schema/atlas.py +7 -5
  46. rucio/common/schema/belleii.py +7 -5
  47. rucio/common/schema/domatpc.py +7 -5
  48. rucio/common/schema/escape.py +7 -5
  49. rucio/common/schema/generic.py +8 -6
  50. rucio/common/schema/generic_multi_vo.py +7 -5
  51. rucio/common/schema/icecube.py +7 -5
  52. rucio/common/stomp_utils.py +0 -1
  53. rucio/common/stopwatch.py +0 -1
  54. rucio/common/test_rucio_server.py +2 -2
  55. rucio/common/types.py +262 -17
  56. rucio/common/utils.py +743 -451
  57. rucio/core/__init__.py +0 -1
  58. rucio/core/account.py +99 -29
  59. rucio/core/account_counter.py +89 -24
  60. rucio/core/account_limit.py +90 -24
  61. rucio/core/authentication.py +86 -29
  62. rucio/core/config.py +108 -38
  63. rucio/core/credential.py +14 -7
  64. rucio/core/did.py +680 -782
  65. rucio/core/did_meta_plugins/__init__.py +8 -6
  66. rucio/core/did_meta_plugins/did_column_meta.py +17 -12
  67. rucio/core/did_meta_plugins/did_meta_plugin_interface.py +60 -11
  68. rucio/core/did_meta_plugins/filter_engine.py +90 -50
  69. rucio/core/did_meta_plugins/json_meta.py +41 -16
  70. rucio/core/did_meta_plugins/mongo_meta.py +25 -8
  71. rucio/core/did_meta_plugins/postgres_meta.py +3 -4
  72. rucio/core/dirac.py +46 -17
  73. rucio/core/distance.py +66 -43
  74. rucio/core/exporter.py +5 -5
  75. rucio/core/heartbeat.py +181 -81
  76. rucio/core/identity.py +22 -12
  77. rucio/core/importer.py +23 -12
  78. rucio/core/lifetime_exception.py +32 -32
  79. rucio/core/lock.py +244 -142
  80. rucio/core/message.py +79 -38
  81. rucio/core/{meta.py → meta_conventions.py} +57 -44
  82. rucio/core/monitor.py +19 -13
  83. rucio/core/naming_convention.py +68 -27
  84. rucio/core/nongrid_trace.py +17 -5
  85. rucio/core/oidc.py +151 -29
  86. rucio/core/permission/__init__.py +18 -6
  87. rucio/core/permission/atlas.py +50 -35
  88. rucio/core/permission/belleii.py +6 -5
  89. rucio/core/permission/escape.py +8 -6
  90. rucio/core/permission/generic.py +82 -80
  91. rucio/core/permission/generic_multi_vo.py +9 -7
  92. rucio/core/quarantined_replica.py +91 -58
  93. rucio/core/replica.py +1303 -772
  94. rucio/core/replica_sorter.py +10 -12
  95. rucio/core/request.py +1133 -285
  96. rucio/core/rse.py +142 -102
  97. rucio/core/rse_counter.py +49 -18
  98. rucio/core/rse_expression_parser.py +6 -7
  99. rucio/core/rse_selector.py +41 -16
  100. rucio/core/rule.py +1538 -474
  101. rucio/core/rule_grouping.py +213 -68
  102. rucio/core/scope.py +50 -22
  103. rucio/core/subscription.py +92 -44
  104. rucio/core/topology.py +66 -24
  105. rucio/core/trace.py +42 -28
  106. rucio/core/transfer.py +543 -259
  107. rucio/core/vo.py +36 -18
  108. rucio/core/volatile_replica.py +59 -32
  109. rucio/daemons/__init__.py +0 -1
  110. rucio/daemons/abacus/__init__.py +0 -1
  111. rucio/daemons/abacus/account.py +29 -19
  112. rucio/daemons/abacus/collection_replica.py +21 -10
  113. rucio/daemons/abacus/rse.py +22 -12
  114. rucio/daemons/atropos/__init__.py +0 -1
  115. rucio/daemons/atropos/atropos.py +1 -2
  116. rucio/daemons/auditor/__init__.py +56 -28
  117. rucio/daemons/auditor/hdfs.py +17 -6
  118. rucio/daemons/auditor/srmdumps.py +116 -45
  119. rucio/daemons/automatix/__init__.py +0 -1
  120. rucio/daemons/automatix/automatix.py +30 -18
  121. rucio/daemons/badreplicas/__init__.py +0 -1
  122. rucio/daemons/badreplicas/minos.py +29 -18
  123. rucio/daemons/badreplicas/minos_temporary_expiration.py +5 -7
  124. rucio/daemons/badreplicas/necromancer.py +9 -13
  125. rucio/daemons/bb8/__init__.py +0 -1
  126. rucio/daemons/bb8/bb8.py +10 -13
  127. rucio/daemons/bb8/common.py +151 -154
  128. rucio/daemons/bb8/nuclei_background_rebalance.py +15 -9
  129. rucio/daemons/bb8/t2_background_rebalance.py +15 -8
  130. rucio/daemons/c3po/__init__.py +0 -1
  131. rucio/daemons/c3po/algorithms/__init__.py +0 -1
  132. rucio/daemons/c3po/algorithms/simple.py +8 -5
  133. rucio/daemons/c3po/algorithms/t2_free_space.py +10 -7
  134. rucio/daemons/c3po/algorithms/t2_free_space_only_pop.py +10 -7
  135. rucio/daemons/c3po/algorithms/t2_free_space_only_pop_with_network.py +30 -15
  136. rucio/daemons/c3po/c3po.py +81 -52
  137. rucio/daemons/c3po/collectors/__init__.py +0 -1
  138. rucio/daemons/c3po/collectors/agis.py +17 -17
  139. rucio/daemons/c3po/collectors/free_space.py +32 -13
  140. rucio/daemons/c3po/collectors/jedi_did.py +14 -5
  141. rucio/daemons/c3po/collectors/mock_did.py +11 -6
  142. rucio/daemons/c3po/collectors/network_metrics.py +12 -4
  143. rucio/daemons/c3po/collectors/workload.py +21 -19
  144. rucio/daemons/c3po/utils/__init__.py +0 -1
  145. rucio/daemons/c3po/utils/dataset_cache.py +15 -5
  146. rucio/daemons/c3po/utils/expiring_dataset_cache.py +16 -5
  147. rucio/daemons/c3po/utils/expiring_list.py +6 -7
  148. rucio/daemons/c3po/utils/popularity.py +5 -2
  149. rucio/daemons/c3po/utils/timeseries.py +25 -12
  150. rucio/daemons/cache/__init__.py +0 -1
  151. rucio/daemons/cache/consumer.py +21 -15
  152. rucio/daemons/common.py +42 -18
  153. rucio/daemons/conveyor/__init__.py +0 -1
  154. rucio/daemons/conveyor/common.py +69 -37
  155. rucio/daemons/conveyor/finisher.py +83 -46
  156. rucio/daemons/conveyor/poller.py +101 -69
  157. rucio/daemons/conveyor/preparer.py +35 -28
  158. rucio/daemons/conveyor/receiver.py +64 -21
  159. rucio/daemons/conveyor/stager.py +33 -28
  160. rucio/daemons/conveyor/submitter.py +71 -47
  161. rucio/daemons/conveyor/throttler.py +99 -35
  162. rucio/daemons/follower/__init__.py +0 -1
  163. rucio/daemons/follower/follower.py +12 -8
  164. rucio/daemons/hermes/__init__.py +0 -1
  165. rucio/daemons/hermes/hermes.py +57 -21
  166. rucio/daemons/judge/__init__.py +0 -1
  167. rucio/daemons/judge/cleaner.py +27 -17
  168. rucio/daemons/judge/evaluator.py +31 -18
  169. rucio/daemons/judge/injector.py +31 -23
  170. rucio/daemons/judge/repairer.py +28 -18
  171. rucio/daemons/oauthmanager/__init__.py +0 -1
  172. rucio/daemons/oauthmanager/oauthmanager.py +7 -8
  173. rucio/daemons/reaper/__init__.py +0 -1
  174. rucio/daemons/reaper/dark_reaper.py +15 -9
  175. rucio/daemons/reaper/reaper.py +109 -67
  176. rucio/daemons/replicarecoverer/__init__.py +0 -1
  177. rucio/daemons/replicarecoverer/suspicious_replica_recoverer.py +255 -116
  178. rucio/{api → daemons/rsedecommissioner}/__init__.py +0 -1
  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 +0 -1
  186. rucio/daemons/storage/consistency/__init__.py +0 -1
  187. rucio/daemons/storage/consistency/actions.py +152 -59
  188. rucio/daemons/tracer/__init__.py +0 -1
  189. rucio/daemons/tracer/kronos.py +47 -24
  190. rucio/daemons/transmogrifier/__init__.py +0 -1
  191. rucio/daemons/transmogrifier/transmogrifier.py +35 -26
  192. rucio/daemons/undertaker/__init__.py +0 -1
  193. rucio/daemons/undertaker/undertaker.py +10 -10
  194. rucio/db/__init__.py +0 -1
  195. rucio/db/sqla/__init__.py +16 -2
  196. rucio/db/sqla/constants.py +10 -1
  197. rucio/db/sqla/migrate_repo/__init__.py +0 -1
  198. rucio/db/sqla/migrate_repo/env.py +0 -1
  199. rucio/db/sqla/migrate_repo/versions/01eaf73ab656_add_new_rule_notification_state_progress.py +0 -1
  200. rucio/db/sqla/migrate_repo/versions/0437a40dbfd1_add_eol_at_in_rules.py +0 -3
  201. rucio/db/sqla/migrate_repo/versions/0f1adb7a599a_create_transfer_hops_table.py +1 -3
  202. rucio/db/sqla/migrate_repo/versions/102efcf145f4_added_stuck_at_column_to_rules.py +0 -3
  203. rucio/db/sqla/migrate_repo/versions/13d4f70c66a9_introduce_transfer_limits.py +1 -3
  204. rucio/db/sqla/migrate_repo/versions/140fef722e91_cleanup_distances_table.py +1 -3
  205. rucio/db/sqla/migrate_repo/versions/14ec5aeb64cf_add_request_external_host.py +0 -3
  206. rucio/db/sqla/migrate_repo/versions/156fb5b5a14_add_request_type_to_requests_idx.py +1 -4
  207. rucio/db/sqla/migrate_repo/versions/1677d4d803c8_split_rse_availability_into_multiple.py +0 -1
  208. rucio/db/sqla/migrate_repo/versions/16a0aca82e12_create_index_on_table_replicas_path.py +0 -2
  209. rucio/db/sqla/migrate_repo/versions/1803333ac20f_adding_provenance_and_phys_group.py +0 -1
  210. rucio/db/sqla/migrate_repo/versions/1a29d6a9504c_add_didtype_chck_to_requests.py +0 -1
  211. rucio/db/sqla/migrate_repo/versions/1a80adff031a_create_index_on_rules_hist_recent.py +0 -2
  212. rucio/db/sqla/migrate_repo/versions/1c45d9730ca6_increase_identity_length.py +0 -1
  213. rucio/db/sqla/migrate_repo/versions/1d1215494e95_add_quarantined_replicas_table.py +1 -3
  214. rucio/db/sqla/migrate_repo/versions/1d96f484df21_asynchronous_rules_and_rule_approval.py +0 -1
  215. rucio/db/sqla/migrate_repo/versions/1f46c5f240ac_add_bytes_column_to_bad_replicas.py +0 -3
  216. rucio/db/sqla/migrate_repo/versions/1fc15ab60d43_add_message_history_table.py +0 -1
  217. rucio/db/sqla/migrate_repo/versions/2190e703eb6e_move_rse_settings_to_rse_attributes.py +1 -2
  218. rucio/db/sqla/migrate_repo/versions/21d6b9dc9961_add_mismatch_scheme_state_to_requests.py +0 -1
  219. rucio/db/sqla/migrate_repo/versions/22cf51430c78_add_availability_column_to_table_rses.py +0 -3
  220. rucio/db/sqla/migrate_repo/versions/22d887e4ec0a_create_sources_table.py +1 -3
  221. rucio/db/sqla/migrate_repo/versions/25821a8a45a3_remove_unique_constraint_on_requests.py +1 -4
  222. rucio/db/sqla/migrate_repo/versions/25fc855625cf_added_unique_constraint_to_rules.py +0 -2
  223. rucio/db/sqla/migrate_repo/versions/269fee20dee9_add_repair_cnt_to_locks.py +0 -3
  224. rucio/db/sqla/migrate_repo/versions/271a46ea6244_add_ignore_availability_column_to_rules.py +0 -3
  225. rucio/db/sqla/migrate_repo/versions/277b5fbb41d3_switch_heartbeats_executable.py +1 -2
  226. rucio/db/sqla/migrate_repo/versions/27e3a68927fb_remove_replicas_tombstone_and_replicas_.py +0 -1
  227. rucio/db/sqla/migrate_repo/versions/2854cd9e168_added_rule_id_column.py +0 -1
  228. rucio/db/sqla/migrate_repo/versions/295289b5a800_processed_by_and__at_in_requests.py +0 -2
  229. rucio/db/sqla/migrate_repo/versions/2962ece31cf4_add_nbaccesses_column_in_the_did_table.py +0 -3
  230. rucio/db/sqla/migrate_repo/versions/2af3291ec4c_added_replicas_history_table.py +1 -3
  231. rucio/db/sqla/migrate_repo/versions/2b69addda658_add_columns_for_third_party_copy_read_.py +0 -2
  232. rucio/db/sqla/migrate_repo/versions/2b8e7bcb4783_add_config_table.py +1 -4
  233. rucio/db/sqla/migrate_repo/versions/2ba5229cb54c_add_submitted_at_to_requests_table.py +0 -3
  234. rucio/db/sqla/migrate_repo/versions/2cbee484dcf9_added_column_volume_to_rse_transfer_.py +1 -4
  235. rucio/db/sqla/migrate_repo/versions/2edee4a83846_add_source_to_requests_and_requests_.py +0 -1
  236. rucio/db/sqla/migrate_repo/versions/2eef46be23d4_change_tokens_pk.py +1 -3
  237. rucio/db/sqla/migrate_repo/versions/2f648fc909f3_index_in_rule_history_on_scope_name.py +0 -2
  238. rucio/db/sqla/migrate_repo/versions/3082b8cef557_add_naming_convention_table_and_closed_.py +1 -3
  239. rucio/db/sqla/migrate_repo/versions/30fa38b6434e_add_index_on_service_column_in_the_message_table.py +1 -3
  240. rucio/db/sqla/migrate_repo/versions/3152492b110b_added_staging_area_column.py +1 -2
  241. rucio/db/sqla/migrate_repo/versions/32c7d2783f7e_create_bad_replicas_table.py +1 -3
  242. rucio/db/sqla/migrate_repo/versions/3345511706b8_replicas_table_pk_definition_is_in_.py +1 -3
  243. rucio/db/sqla/migrate_repo/versions/35ef10d1e11b_change_index_on_table_requests.py +0 -2
  244. rucio/db/sqla/migrate_repo/versions/379a19b5332d_create_rse_limits_table.py +1 -3
  245. rucio/db/sqla/migrate_repo/versions/384b96aa0f60_created_rule_history_tables.py +2 -3
  246. rucio/db/sqla/migrate_repo/versions/3ac1660a1a72_extend_distance_table.py +0 -3
  247. rucio/db/sqla/migrate_repo/versions/3ad36e2268b0_create_collection_replicas_updates_table.py +1 -4
  248. rucio/db/sqla/migrate_repo/versions/3c9df354071b_extend_waiting_request_state.py +0 -1
  249. rucio/db/sqla/migrate_repo/versions/3d9813fab443_add_a_new_state_lost_in_badfilesstatus.py +0 -1
  250. rucio/db/sqla/migrate_repo/versions/40ad39ce3160_add_transferred_at_to_requests_table.py +0 -3
  251. rucio/db/sqla/migrate_repo/versions/4207be2fd914_add_notification_column_to_rules.py +0 -1
  252. rucio/db/sqla/migrate_repo/versions/42db2617c364_create_index_on_requests_external_id.py +0 -2
  253. rucio/db/sqla/migrate_repo/versions/436827b13f82_added_column_activity_to_table_requests.py +0 -3
  254. rucio/db/sqla/migrate_repo/versions/44278720f774_update_requests_typ_sta_upd_idx_index.py +0 -2
  255. rucio/db/sqla/migrate_repo/versions/45378a1e76a8_create_collection_replica_table.py +2 -4
  256. rucio/db/sqla/migrate_repo/versions/469d262be19_removing_created_at_index.py +0 -2
  257. rucio/db/sqla/migrate_repo/versions/4783c1f49cb4_create_distance_table.py +1 -3
  258. rucio/db/sqla/migrate_repo/versions/49a21b4d4357_create_index_on_table_tokens.py +1 -4
  259. rucio/db/sqla/migrate_repo/versions/4a2cbedda8b9_add_source_replica_expression_column_to_.py +0 -3
  260. rucio/db/sqla/migrate_repo/versions/4a7182d9578b_added_bytes_length_accessed_at_columns.py +0 -3
  261. rucio/db/sqla/migrate_repo/versions/4bab9edd01fc_create_index_on_requests_rule_id.py +0 -2
  262. rucio/db/sqla/migrate_repo/versions/4c3a4acfe006_new_attr_account_table.py +1 -3
  263. rucio/db/sqla/migrate_repo/versions/4cf0a2e127d4_adding_transient_metadata.py +0 -3
  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 +0 -2
  266. rucio/db/sqla/migrate_repo/versions/52153819589c_add_rse_id_to_replicas_table.py +0 -2
  267. rucio/db/sqla/migrate_repo/versions/52fd9f4916fa_added_activity_to_rules.py +0 -3
  268. rucio/db/sqla/migrate_repo/versions/53b479c3cb0f_fix_did_meta_table_missing_updated_at_.py +0 -3
  269. rucio/db/sqla/migrate_repo/versions/5673b4b6e843_add_wfms_metadata_to_rule_tables.py +0 -3
  270. rucio/db/sqla/migrate_repo/versions/575767d9f89_added_source_history_table.py +1 -2
  271. rucio/db/sqla/migrate_repo/versions/58bff7008037_add_started_at_to_requests.py +0 -3
  272. rucio/db/sqla/migrate_repo/versions/58c8b78301ab_rename_callback_to_message.py +1 -3
  273. rucio/db/sqla/migrate_repo/versions/5f139f77382a_added_child_rule_id_column.py +1 -3
  274. rucio/db/sqla/migrate_repo/versions/688ef1840840_adding_did_meta_table.py +1 -2
  275. rucio/db/sqla/migrate_repo/versions/6e572a9bfbf3_add_new_split_container_column_to_rules.py +0 -3
  276. rucio/db/sqla/migrate_repo/versions/70587619328_add_comment_column_for_subscriptions.py +0 -3
  277. rucio/db/sqla/migrate_repo/versions/739064d31565_remove_history_table_pks.py +1 -2
  278. rucio/db/sqla/migrate_repo/versions/7541902bf173_add_didsfollowed_and_followevents_table.py +2 -4
  279. rucio/db/sqla/migrate_repo/versions/7ec22226cdbf_new_replica_state_for_temporary_.py +0 -1
  280. rucio/db/sqla/migrate_repo/versions/810a41685bc1_added_columns_rse_transfer_limits.py +1 -4
  281. rucio/db/sqla/migrate_repo/versions/83f991c63a93_correct_rse_expression_length.py +0 -2
  282. rucio/db/sqla/migrate_repo/versions/8523998e2e76_increase_size_of_extended_attributes_.py +0 -3
  283. rucio/db/sqla/migrate_repo/versions/8ea9122275b1_adding_missing_function_based_indices.py +1 -2
  284. rucio/db/sqla/migrate_repo/versions/90f47792bb76_add_clob_payload_to_messages.py +0 -3
  285. rucio/db/sqla/migrate_repo/versions/914b8f02df38_new_table_for_lifetime_model_exceptions.py +1 -3
  286. rucio/db/sqla/migrate_repo/versions/94a5961ddbf2_add_estimator_columns.py +0 -3
  287. rucio/db/sqla/migrate_repo/versions/9a1b149a2044_add_saml_identity_type.py +0 -1
  288. rucio/db/sqla/migrate_repo/versions/9a45bc4ea66d_add_vp_table.py +1 -2
  289. rucio/db/sqla/migrate_repo/versions/9eb936a81eb1_true_is_true.py +0 -2
  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 +1 -3
  292. rucio/db/sqla/migrate_repo/versions/a193a275255c_add_status_column_in_messages.py +0 -2
  293. rucio/db/sqla/migrate_repo/versions/a5f6f6e928a7_1_7_0.py +1 -4
  294. rucio/db/sqla/migrate_repo/versions/a616581ee47_added_columns_to_table_requests.py +0 -1
  295. rucio/db/sqla/migrate_repo/versions/a6eb23955c28_state_idx_non_functional.py +0 -1
  296. rucio/db/sqla/migrate_repo/versions/a74275a1ad30_added_global_quota_table.py +1 -3
  297. rucio/db/sqla/migrate_repo/versions/a93e4e47bda_heartbeats.py +1 -4
  298. rucio/db/sqla/migrate_repo/versions/ae2a56fcc89_added_comment_column_to_rules.py +0 -1
  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 +0 -3
  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 +1 -2
  303. rucio/db/sqla/migrate_repo/versions/b818052fa670_add_index_to_quarantined_replicas.py +1 -3
  304. rucio/db/sqla/migrate_repo/versions/b8caac94d7f0_add_comments_column_for_subscriptions_.py +0 -3
  305. rucio/db/sqla/migrate_repo/versions/b96a1c7e1cc4_new_bad_pfns_table_and_bad_replicas_.py +1 -5
  306. rucio/db/sqla/migrate_repo/versions/bb695f45c04_extend_request_state.py +1 -3
  307. rucio/db/sqla/migrate_repo/versions/bc68e9946deb_add_staging_timestamps_to_request.py +0 -3
  308. rucio/db/sqla/migrate_repo/versions/bf3baa1c1474_correct_pk_and_idx_for_history_tables.py +1 -3
  309. rucio/db/sqla/migrate_repo/versions/c0937668555f_add_qos_policy_map_table.py +1 -2
  310. rucio/db/sqla/migrate_repo/versions/c129ccdb2d5_add_lumiblocknr_to_dids.py +0 -3
  311. rucio/db/sqla/migrate_repo/versions/ccdbcd48206e_add_did_type_column_index_on_did_meta_.py +1 -4
  312. rucio/db/sqla/migrate_repo/versions/cebad904c4dd_new_payload_column_for_heartbeats.py +1 -2
  313. rucio/db/sqla/migrate_repo/versions/d1189a09c6e0_oauth2_0_and_jwt_feature_support_adding_.py +1 -4
  314. rucio/db/sqla/migrate_repo/versions/d23453595260_extend_request_state_for_preparer.py +1 -3
  315. rucio/db/sqla/migrate_repo/versions/d6dceb1de2d_added_purge_column_to_rules.py +1 -4
  316. rucio/db/sqla/migrate_repo/versions/d6e2c3b2cf26_remove_third_party_copy_column_from_rse.py +0 -2
  317. rucio/db/sqla/migrate_repo/versions/d91002c5841_new_account_limits_table.py +1 -3
  318. rucio/db/sqla/migrate_repo/versions/e138c364ebd0_extending_columns_for_filter_and_.py +0 -3
  319. rucio/db/sqla/migrate_repo/versions/e59300c8b179_support_for_archive.py +1 -3
  320. rucio/db/sqla/migrate_repo/versions/f1b14a8c2ac1_postgres_use_check_constraints.py +0 -1
  321. rucio/db/sqla/migrate_repo/versions/f41ffe206f37_oracle_global_temporary_tables.py +1 -2
  322. rucio/db/sqla/migrate_repo/versions/f85a2962b021_adding_transfertool_column_to_requests_.py +1 -3
  323. rucio/db/sqla/migrate_repo/versions/fa7a7d78b602_increase_refresh_token_size.py +0 -2
  324. rucio/db/sqla/migrate_repo/versions/fb28a95fe288_add_replicas_rse_id_tombstone_idx.py +0 -1
  325. rucio/db/sqla/migrate_repo/versions/fe1a65b176c9_set_third_party_copy_read_and_write_.py +1 -2
  326. rucio/db/sqla/migrate_repo/versions/fe8ea2fa9788_added_third_party_copy_column_to_rse_.py +0 -3
  327. rucio/db/sqla/models.py +122 -216
  328. rucio/db/sqla/sautils.py +12 -5
  329. rucio/db/sqla/session.py +71 -43
  330. rucio/db/sqla/types.py +3 -4
  331. rucio/db/sqla/util.py +91 -69
  332. rucio/gateway/__init__.py +13 -0
  333. rucio/{api → gateway}/account.py +119 -46
  334. rucio/{api → gateway}/account_limit.py +12 -13
  335. rucio/{api → gateway}/authentication.py +106 -33
  336. rucio/{api → gateway}/config.py +12 -13
  337. rucio/{api → gateway}/credential.py +15 -4
  338. rucio/{api → gateway}/did.py +384 -140
  339. rucio/{api → gateway}/dirac.py +16 -6
  340. rucio/{api → gateway}/exporter.py +3 -4
  341. rucio/{api → gateway}/heartbeat.py +17 -5
  342. rucio/{api → gateway}/identity.py +63 -19
  343. rucio/{api → gateway}/importer.py +3 -4
  344. rucio/{api → gateway}/lifetime_exception.py +35 -10
  345. rucio/{api → gateway}/lock.py +34 -12
  346. rucio/{api/meta.py → gateway/meta_conventions.py} +18 -16
  347. rucio/{api → gateway}/permission.py +4 -5
  348. rucio/{api → gateway}/quarantined_replica.py +13 -4
  349. rucio/{api → gateway}/replica.py +12 -11
  350. rucio/{api → gateway}/request.py +129 -28
  351. rucio/{api → gateway}/rse.py +11 -12
  352. rucio/{api → gateway}/rule.py +117 -35
  353. rucio/{api → gateway}/scope.py +24 -14
  354. rucio/{api → gateway}/subscription.py +65 -43
  355. rucio/{api → gateway}/vo.py +17 -7
  356. rucio/rse/__init__.py +3 -4
  357. rucio/rse/protocols/__init__.py +0 -1
  358. rucio/rse/protocols/bittorrent.py +184 -0
  359. rucio/rse/protocols/cache.py +1 -2
  360. rucio/rse/protocols/dummy.py +1 -2
  361. rucio/rse/protocols/gfal.py +12 -10
  362. rucio/rse/protocols/globus.py +7 -7
  363. rucio/rse/protocols/gsiftp.py +2 -3
  364. rucio/rse/protocols/http_cache.py +1 -2
  365. rucio/rse/protocols/mock.py +1 -2
  366. rucio/rse/protocols/ngarc.py +1 -2
  367. rucio/rse/protocols/posix.py +12 -13
  368. rucio/rse/protocols/protocol.py +116 -52
  369. rucio/rse/protocols/rclone.py +6 -7
  370. rucio/rse/protocols/rfio.py +4 -5
  371. rucio/rse/protocols/srm.py +9 -10
  372. rucio/rse/protocols/ssh.py +8 -9
  373. rucio/rse/protocols/storm.py +2 -3
  374. rucio/rse/protocols/webdav.py +17 -14
  375. rucio/rse/protocols/xrootd.py +23 -17
  376. rucio/rse/rsemanager.py +19 -7
  377. rucio/tests/__init__.py +0 -1
  378. rucio/tests/common.py +43 -17
  379. rucio/tests/common_server.py +3 -3
  380. rucio/transfertool/__init__.py +0 -1
  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 +250 -138
  385. rucio/transfertool/fts3_plugins.py +152 -0
  386. rucio/transfertool/globus.py +9 -8
  387. rucio/transfertool/globus_library.py +1 -2
  388. rucio/transfertool/mock.py +21 -12
  389. rucio/transfertool/transfertool.py +33 -24
  390. rucio/vcsversion.py +4 -4
  391. rucio/version.py +5 -13
  392. rucio/web/__init__.py +0 -1
  393. rucio/web/rest/__init__.py +0 -1
  394. rucio/web/rest/flaskapi/__init__.py +0 -1
  395. rucio/web/rest/flaskapi/authenticated_bp.py +0 -1
  396. rucio/web/rest/flaskapi/v1/__init__.py +0 -1
  397. rucio/web/rest/flaskapi/v1/accountlimits.py +15 -13
  398. rucio/web/rest/flaskapi/v1/accounts.py +49 -48
  399. rucio/web/rest/flaskapi/v1/archives.py +12 -10
  400. rucio/web/rest/flaskapi/v1/auth.py +146 -144
  401. rucio/web/rest/flaskapi/v1/common.py +82 -41
  402. rucio/web/rest/flaskapi/v1/config.py +5 -6
  403. rucio/web/rest/flaskapi/v1/credentials.py +7 -8
  404. rucio/web/rest/flaskapi/v1/dids.py +158 -28
  405. rucio/web/rest/flaskapi/v1/dirac.py +8 -8
  406. rucio/web/rest/flaskapi/v1/export.py +3 -5
  407. rucio/web/rest/flaskapi/v1/heartbeats.py +3 -5
  408. rucio/web/rest/flaskapi/v1/identities.py +3 -5
  409. rucio/web/rest/flaskapi/v1/import.py +3 -4
  410. rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +6 -9
  411. rucio/web/rest/flaskapi/v1/locks.py +2 -4
  412. rucio/web/rest/flaskapi/v1/main.py +10 -2
  413. rucio/web/rest/flaskapi/v1/{meta.py → meta_conventions.py} +26 -11
  414. rucio/web/rest/flaskapi/v1/metrics.py +1 -2
  415. rucio/web/rest/flaskapi/v1/nongrid_traces.py +4 -4
  416. rucio/web/rest/flaskapi/v1/ping.py +6 -7
  417. rucio/web/rest/flaskapi/v1/redirect.py +8 -9
  418. rucio/web/rest/flaskapi/v1/replicas.py +43 -19
  419. rucio/web/rest/flaskapi/v1/requests.py +178 -21
  420. rucio/web/rest/flaskapi/v1/rses.py +61 -26
  421. rucio/web/rest/flaskapi/v1/rules.py +48 -18
  422. rucio/web/rest/flaskapi/v1/scopes.py +3 -5
  423. rucio/web/rest/flaskapi/v1/subscriptions.py +22 -18
  424. rucio/web/rest/flaskapi/v1/traces.py +4 -4
  425. rucio/web/rest/flaskapi/v1/types.py +20 -0
  426. rucio/web/rest/flaskapi/v1/vos.py +3 -5
  427. rucio/web/rest/main.py +0 -1
  428. rucio/web/rest/metrics.py +0 -1
  429. rucio/web/rest/ping.py +27 -0
  430. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/ldap.cfg.template +1 -1
  431. rucio-35.8.0.data/data/rucio/requirements.server.txt +268 -0
  432. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/tools/bootstrap.py +3 -3
  433. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/tools/merge_rucio_configs.py +2 -5
  434. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/tools/reset_database.py +3 -3
  435. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio +87 -85
  436. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-abacus-account +0 -1
  437. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-abacus-collection-replica +0 -1
  438. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-abacus-rse +0 -1
  439. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-admin +45 -32
  440. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-atropos +0 -1
  441. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-auditor +13 -7
  442. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-automatix +1 -2
  443. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-bb8 +0 -1
  444. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-c3po +0 -1
  445. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-cache-client +2 -3
  446. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-cache-consumer +0 -1
  447. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-finisher +1 -2
  448. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-poller +0 -1
  449. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-preparer +0 -1
  450. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-receiver +0 -1
  451. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-stager +0 -1
  452. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-submitter +2 -3
  453. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-throttler +0 -1
  454. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-dark-reaper +0 -1
  455. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-dumper +11 -10
  456. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-follower +0 -1
  457. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-hermes +0 -1
  458. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-judge-cleaner +0 -1
  459. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-judge-evaluator +2 -3
  460. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-judge-injector +0 -1
  461. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-judge-repairer +0 -1
  462. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-kronos +1 -3
  463. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-minos +0 -1
  464. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-minos-temporary-expiration +0 -1
  465. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-necromancer +1 -2
  466. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-oauth-manager +2 -3
  467. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-reaper +0 -1
  468. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-replica-recoverer +6 -7
  469. rucio-35.8.0.data/scripts/rucio-rse-decommissioner +66 -0
  470. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-storage-consistency-actions +0 -1
  471. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-transmogrifier +0 -1
  472. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-undertaker +1 -2
  473. rucio-35.8.0.dist-info/METADATA +72 -0
  474. rucio-35.8.0.dist-info/RECORD +493 -0
  475. {rucio-32.8.6.dist-info → rucio-35.8.0.dist-info}/WHEEL +1 -1
  476. {rucio-32.8.6.dist-info → rucio-35.8.0.dist-info}/licenses/AUTHORS.rst +3 -0
  477. rucio/api/temporary_did.py +0 -49
  478. rucio/common/schema/cms.py +0 -478
  479. rucio/common/schema/lsst.py +0 -423
  480. rucio/core/permission/cms.py +0 -1166
  481. rucio/core/temporary_did.py +0 -188
  482. rucio/daemons/reaper/light_reaper.py +0 -255
  483. rucio/web/rest/flaskapi/v1/tmp_dids.py +0 -115
  484. rucio-32.8.6.data/data/rucio/requirements.txt +0 -55
  485. rucio-32.8.6.data/scripts/rucio-light-reaper +0 -53
  486. rucio-32.8.6.dist-info/METADATA +0 -83
  487. rucio-32.8.6.dist-info/RECORD +0 -481
  488. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/alembic.ini.template +0 -0
  489. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/alembic_offline.ini.template +0 -0
  490. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/globus-config.yml.template +0 -0
  491. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_approval_request.tmpl +0 -0
  492. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +0 -0
  493. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_approved_user.tmpl +0 -0
  494. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +0 -0
  495. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_denied_user.tmpl +0 -0
  496. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +0 -0
  497. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/rse-accounts.cfg.template +0 -0
  498. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/rucio.cfg.atlas.client.template +0 -0
  499. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/rucio.cfg.template +0 -0
  500. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/rucio_multi_vo.cfg.template +0 -0
  501. {rucio-32.8.6.dist-info → rucio-35.8.0.dist-info}/licenses/LICENSE +0 -0
  502. {rucio-32.8.6.dist-info → rucio-35.8.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,3 @@
1
- # -*- coding: utf-8 -*-
2
1
  # Copyright European Organization for Nuclear Research (CERN) since 2012
3
2
  #
4
3
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,10 +18,9 @@ import logging
19
18
  import pathlib
20
19
  import traceback
21
20
  import uuid
22
- from collections.abc import Callable
23
21
  from configparser import NoOptionError, NoSectionError
24
22
  from json import loads
25
- from typing import Any, Optional, TYPE_CHECKING
23
+ from typing import TYPE_CHECKING, Any, Optional, Union
26
24
  from urllib.parse import urlparse
27
25
 
28
26
  import requests
@@ -31,20 +29,26 @@ from requests.adapters import ReadTimeout
31
29
  from requests.packages.urllib3 import disable_warnings # pylint: disable=import-error
32
30
 
33
31
  from rucio.common.cache import make_region_memcached
34
- from rucio.common.config import config_get, config_get_bool, config_get_int
35
- from rucio.common.constants import FTS_JOB_TYPE, FTS_STATE, FTS_COMPLETE_STATE
36
- from rucio.common.exception import TransferToolTimeout, TransferToolWrongAnswer, DuplicateFileTransferSubmission
32
+ from rucio.common.config import config_get, config_get_bool, config_get_int, config_get_list
33
+ from rucio.common.constants import FTS_COMPLETE_STATE, FTS_JOB_TYPE, FTS_STATE, RseAttr
34
+ from rucio.common.exception import DuplicateFileTransferSubmission, TransferToolTimeout, TransferToolWrongAnswer
37
35
  from rucio.common.stopwatch import Stopwatch
38
- from rucio.common.utils import APIEncoder, chunks, PREFERRED_CHECKSUM
36
+ from rucio.common.utils import PREFERRED_CHECKSUM, APIEncoder, chunks, deep_merge_dict
39
37
  from rucio.core.monitor import MetricManager
40
- from rucio.core.oidc import get_token_for_account_operation
38
+ from rucio.core.oidc import request_token
41
39
  from rucio.core.request import get_source_rse, get_transfer_error
42
- from rucio.core.rse import get_rse_supported_checksums_from_attributes
40
+ from rucio.core.rse import determine_audience_for_rse, determine_scope_for_rse, get_rse_supported_checksums_from_attributes
43
41
  from rucio.db.sqla.constants import RequestState
44
- from rucio.transfertool.transfertool import Transfertool, TransferToolBuilder, TransferStatusReport
42
+ from rucio.transfertool.fts3_plugins import FTS3TapeMetadataPlugin
43
+ from rucio.transfertool.transfertool import TransferStatusReport, Transfertool, TransferToolBuilder
45
44
 
46
45
  if TYPE_CHECKING:
47
- from rucio.core.transfer import DirectTransferDefinition
46
+ from collections.abc import Iterable, Sequence
47
+
48
+ from sqlalchemy.orm import Session
49
+
50
+ from rucio.common.types import LoggerFunction
51
+ from rucio.core.request import DirectTransfer
48
52
  from rucio.core.rse import RseData
49
53
 
50
54
  logging.getLogger("requests").setLevel(logging.CRITICAL)
@@ -70,9 +74,6 @@ BULK_QUERY_COUNTER = METRICS.counter(name='{host}.bulk_query.{state}',
70
74
  QUERY_DETAILS_COUNTER = METRICS.counter(name='{host}.query_details.{state}',
71
75
  documentation='Number of detailed status queries', labelnames=('state', 'host'))
72
76
 
73
- ALLOW_USER_OIDC_TOKENS = config_get_bool('conveyor', 'allow_user_oidc_tokens', False, False)
74
- REQUEST_OIDC_SCOPE = config_get('conveyor', 'request_oidc_scope', False, 'fts:submit-transfer')
75
- REQUEST_OIDC_AUDIENCE = config_get('conveyor', 'request_oidc_audience', False, 'fts:example')
76
77
  REWRITE_HTTPS_TO_DAVS = config_get_bool('transfers', 'rewrite_https_to_davs', default=False)
77
78
  VO_CERTS_PATH = config_get('conveyor', 'vo_certs_path', False, None)
78
79
 
@@ -109,7 +110,7 @@ _SCITAGS_EXP_ID = None
109
110
  _SCITAGS_ACTIVITY_IDS = {}
110
111
 
111
112
 
112
- def _scitags_ids(logger: Callable[..., Any] = logging.log) -> "tuple[int | None, dict[str, int]]":
113
+ def _scitags_ids(logger: "LoggerFunction" = logging.log) -> "tuple[int | None, dict[str, int]]":
113
114
  """
114
115
  Re-fetch if needed and return the scitags ids
115
116
  """
@@ -124,7 +125,7 @@ def _scitags_ids(logger: Callable[..., Any] = logging.log) -> "tuple[int | None,
124
125
  if _SCITAGS_NEXT_REFRESH < now:
125
126
  exp_name = config_get('packet-marking', 'exp_name', default='')
126
127
  fetch_url = config_get('packet-marking', 'fetch_url', default='https://www.scitags.org/api.json')
127
- fetch_interval = config_get_int('packet-marking', 'fetch_interval', default=datetime.timedelta(hours=48).seconds)
128
+ fetch_interval = config_get_int('packet-marking', 'fetch_interval', default=int(datetime.timedelta(hours=48).total_seconds()))
128
129
  fetch_timeout = config_get_int('packet-marking', 'fetch_timeout', default=5)
129
130
 
130
131
  _SCITAGS_NEXT_REFRESH = now + datetime.timedelta(seconds=fetch_interval)
@@ -160,7 +161,7 @@ def _scitags_ids(logger: Callable[..., Any] = logging.log) -> "tuple[int | None,
160
161
  return _SCITAGS_EXP_ID, _SCITAGS_ACTIVITY_IDS
161
162
 
162
163
 
163
- def _pick_cert_file(vo: "Optional[str]") -> "Optional[str]":
164
+ def _pick_cert_file(vo: Optional[str]) -> Optional[str]:
164
165
  cert = None
165
166
  if vo:
166
167
  vo_cert = config_get('vo_certs', vo, False, None)
@@ -177,7 +178,7 @@ def _pick_cert_file(vo: "Optional[str]") -> "Optional[str]":
177
178
  return cert
178
179
 
179
180
 
180
- def _configured_source_strategy(activity: str, logger: Callable[..., Any]) -> str:
181
+ def _configured_source_strategy(activity: str, logger: "LoggerFunction") -> str:
181
182
  """
182
183
  Retrieve from the configuration the source selection strategy for the given activity
183
184
  """
@@ -198,36 +199,20 @@ def _configured_source_strategy(activity: str, logger: Callable[..., Any]) -> st
198
199
  return activity_source_strategy.get(str(activity), default_source_strategy)
199
200
 
200
201
 
201
- def oidc_supported(transfer_hop) -> bool:
202
- """
203
- checking OIDC AuthN/Z support per destination and source RSEs;
204
-
205
- for oidc_support to be activated, all sources and the destination must explicitly support it
206
- """
207
- # assumes use of boolean 'oidc_support' RSE attribute
208
- if not transfer_hop.dst.rse.attributes.get('oidc_support', False):
209
- return False
210
-
211
- for source in transfer_hop.sources:
212
- if not source.rse.attributes.get('oidc_support', False):
213
- return False
214
- return True
215
-
216
-
217
202
  def _available_checksums(
218
- transfer: "DirectTransferDefinition",
203
+ transfer: "DirectTransfer",
219
204
  ) -> tuple[set[str], set[str]]:
220
205
  """
221
206
  Get checksums which can be used for file validation on the source and the destination RSE
222
207
  """
223
208
  src_attributes = transfer.src.rse.attributes
224
- if src_attributes.get('verify_checksum', True):
209
+ if src_attributes.get(RseAttr.VERIFY_CHECKSUM, True):
225
210
  src_checksums = set(get_rse_supported_checksums_from_attributes(src_attributes))
226
211
  else:
227
212
  src_checksums = set()
228
213
 
229
214
  dst_attributes = transfer.dst.rse.attributes
230
- if dst_attributes.get('verify_checksum', True):
215
+ if dst_attributes.get(RseAttr.VERIFY_CHECKSUM, True):
231
216
  dst_checksums = set(get_rse_supported_checksums_from_attributes(dst_attributes))
232
217
  else:
233
218
  dst_checksums = set()
@@ -236,8 +221,8 @@ def _available_checksums(
236
221
 
237
222
 
238
223
  def _hop_checksum_validation_strategy(
239
- transfer: "DirectTransferDefinition",
240
- logger: Callable[..., Any],
224
+ transfer: "DirectTransfer",
225
+ logger: "LoggerFunction",
241
226
  ) -> tuple[str, set[str]]:
242
227
  """
243
228
  Compute the checksum validation strategy (none, source, destination or both) depending
@@ -262,8 +247,8 @@ def _hop_checksum_validation_strategy(
262
247
 
263
248
 
264
249
  def _path_checksum_validation_strategy(
265
- transfer_path: "list[DirectTransferDefinition]",
266
- logger: Callable[..., Any],
250
+ transfer_path: "list[DirectTransfer]",
251
+ logger: "LoggerFunction",
267
252
  ) -> str:
268
253
  """
269
254
  Compute the checksum validation strategy for the whole transfer path.
@@ -279,7 +264,7 @@ def _path_checksum_validation_strategy(
279
264
 
280
265
 
281
266
  def _pick_fts_checksum(
282
- transfer: "DirectTransferDefinition",
267
+ transfer: "DirectTransfer",
283
268
  path_strategy: "str",
284
269
  ) -> Optional[str]:
285
270
  """
@@ -313,9 +298,31 @@ def _pick_fts_checksum(
313
298
  return checksum_to_use
314
299
 
315
300
 
316
- def build_job_params(transfer_path, bring_online, default_lifetime, archive_timeout_override, max_time_in_queue, logger):
301
+ def _use_tokens(transfer_hop: "DirectTransfer"):
302
+ """Whether a transfer can be performed with tokens.
303
+
304
+ In order to be so, all the involved RSEs must have it explicitly enabled
305
+ and the protocol being used must be WebDAV.
306
+ """
307
+ for endpoint in [*transfer_hop.sources, transfer_hop.dst]:
308
+ if (endpoint.rse.attributes.get(RseAttr.OIDC_SUPPORT) is not True
309
+ or endpoint.scheme != 'davs'):
310
+ return False
311
+ return True
312
+
313
+
314
+ def build_job_params(
315
+ transfer_path: list["DirectTransfer"],
316
+ bring_online: Optional[int] = None,
317
+ default_lifetime: Optional[int] = None,
318
+ archive_timeout_override: Optional[int] = None,
319
+ max_time_in_queue: Optional[dict] = None,
320
+ logger: "LoggerFunction" = logging.log
321
+ ) -> dict[str, Any]:
317
322
  """
318
323
  Prepare the job parameters which will be passed to FTS transfertool
324
+ Please refer to https://fts3-docs.web.cern.ch/fts3-docs/fts-rest/docs/bulk.html#parameters
325
+ for the list of parameters.
319
326
  """
320
327
 
321
328
  # The last hop is the main request (the one which triggered the whole transfer),
@@ -323,13 +330,46 @@ def build_job_params(transfer_path, bring_online, default_lifetime, archive_time
323
330
  last_hop = transfer_path[-1]
324
331
  first_hop = transfer_path[0]
325
332
 
326
- overwrite, bring_online_local = True, None
333
+ # Overwriting by default is set to True for non TAPE RSEs.
334
+ # Tape RSEs can force overwrite by setting the "overwrite" attribute to True.
335
+ # There is yet another configuration option: transfers->overwrite_corrupted_files that when is set to True
336
+ # it will retry failed requests with overwrite flag set to True
337
+ overwrite, overwrite_when_only_on_disk, bring_online_local = True, False, None
338
+
327
339
  if first_hop.src.rse.is_tape_or_staging_required():
328
340
  # Activate bring_online if it was requested by first hop
329
341
  # We don't allow multihop via a tape, so bring_online should not be set on any other hop
330
342
  bring_online_local = bring_online
343
+
331
344
  if last_hop.dst.rse.is_tape():
332
- overwrite = False
345
+ # FTS v3.12.12 introduced a new boolean parameter "overwrite_when_only_on_disk" that controls if the file can be overwritten
346
+ # in TAPE enabled RSEs ONLY IF the file is on the disk buffer and not yet committed to tape media.
347
+ # This functionality should reduce the number of stuck files in the disk buffer that are not migrated to tape media (for whatever reason).
348
+ # Please be aware that FTS does not guarantee an atomic operation from the time it checks for existence of the file on disk and tape and
349
+ # the moment the file is overwritten, so there is a race condition that could overwrite the file on the tape media
350
+
351
+ # Setting both flags is incompatible, so we opt in for the safest approach: "overwrite_when_only_on_disk"
352
+ # this is aligned with FTS implementation: see (internal access only): https://its.cern.ch/jira/browse/FTS-2007
353
+
354
+ overwrite_when_only_on_disk = last_hop.dst.rse.attributes.get('overwrite_when_only_on_disk', False)
355
+ overwrite = False if overwrite_when_only_on_disk else last_hop.dst.rse.attributes.get('overwrite', False)
356
+
357
+ # We still need to check for the
358
+ # "transfers -> overwrite_corrupted_files setting. The logic behind this flag is that
359
+ # it will update the rws (RequestWithSources) with the "overwrite" attribute set to True
360
+ # after finding an 'Destination file exists and overwrite is not enabled' error message
361
+ overwrite_corrupted_files = last_hop.rws.attributes.get('overwrite', False)
362
+ if overwrite_corrupted_files:
363
+ overwrite = True # both for DISK and TAPE
364
+
365
+ logger(logging.DEBUG, 'RSE:%s Is it Tape? %s overwrite_when_only_on_disk:%s overwrite:%s overwrite_corrupted_files=%s' % (
366
+ last_hop.dst.rse.name,
367
+ last_hop.dst.rse.is_tape(),
368
+ overwrite_when_only_on_disk,
369
+ overwrite,
370
+ overwrite_corrupted_files))
371
+
372
+ logger(logging.DEBUG, 'RSE attributes are: %s' % (last_hop.dst.rse.attributes))
333
373
 
334
374
  # Get dest space token
335
375
  dest_protocol = last_hop.protocol_factory.protocol(last_hop.dst.rse, last_hop.dst.scheme, last_hop.operation_dest)
@@ -338,8 +378,8 @@ def build_job_params(transfer_path, bring_online, default_lifetime, archive_time
338
378
  dest_protocol.attributes['extended_attributes'] and 'space_token' in dest_protocol.attributes['extended_attributes']:
339
379
  dest_spacetoken = dest_protocol.attributes['extended_attributes']['space_token']
340
380
 
341
- strict_copy = last_hop.dst.rse.attributes.get('strict_copy', False)
342
- archive_timeout = last_hop.dst.rse.attributes.get('archive_timeout', None)
381
+ strict_copy = last_hop.dst.rse.attributes.get(RseAttr.STRICT_COPY, False)
382
+ archive_timeout = last_hop.dst.rse.attributes.get(RseAttr.ARCHIVE_TIMEOUT, None)
343
383
 
344
384
  job_params = {'account': last_hop.rws.account,
345
385
  'verify_checksum': _path_checksum_validation_strategy(transfer_path, logger=logger),
@@ -348,29 +388,32 @@ def build_job_params(transfer_path, bring_online, default_lifetime, archive_time
348
388
  'job_metadata': {
349
389
  'issuer': 'rucio',
350
390
  'multi_sources': False,
391
+ 'overwrite_when_only_on_disk': overwrite_when_only_on_disk,
351
392
  },
352
- 'overwrite': last_hop.rws.attributes.get('overwrite', overwrite),
393
+ 'overwrite': overwrite,
394
+ 'overwrite_when_only_on_disk': overwrite_when_only_on_disk,
353
395
  'priority': last_hop.rws.priority}
354
396
 
355
397
  if len(transfer_path) > 1:
356
398
  job_params['multihop'] = True
357
399
  job_params['job_metadata']['multihop'] = True
358
- elif len(last_hop.legacy_sources) > 1:
400
+ elif len(last_hop.sources) > 1:
359
401
  job_params['job_metadata']['multi_sources'] = True
360
402
  if strict_copy:
361
403
  job_params['strict_copy'] = strict_copy
362
404
  if dest_spacetoken:
363
405
  job_params['spacetoken'] = dest_spacetoken
364
- if last_hop.use_ipv4:
406
+ if (last_hop.dst.rse.attributes.get(RseAttr.USE_IPV4, False)
407
+ or any(src.rse.attributes.get(RseAttr.USE_IPV4, False) for src in last_hop.sources)):
365
408
  job_params['ipv4'] = True
366
409
  job_params['ipv6'] = False
367
410
 
368
411
  # assume s3alternate True (path-style URL S3 RSEs)
369
412
  job_params['s3alternate'] = True
370
- src_rse_s3_url_style = first_hop.src.rse.attributes.get('s3_url_style', None)
413
+ src_rse_s3_url_style = first_hop.src.rse.attributes.get(RseAttr.S3_URL_STYLE, None)
371
414
  if src_rse_s3_url_style == "host":
372
415
  job_params['s3alternate'] = False
373
- dst_rse_s3_url_style = last_hop.dst.rse.attributes.get('s3_url_style', None)
416
+ dst_rse_s3_url_style = last_hop.dst.rse.attributes.get(RseAttr.S3_URL_STYLE, None)
374
417
  if dst_rse_s3_url_style == "host":
375
418
  job_params['s3alternate'] = False
376
419
 
@@ -393,23 +436,42 @@ def build_job_params(transfer_path, bring_online, default_lifetime, archive_time
393
436
  elif 'default' in max_time_in_queue:
394
437
  job_params['max_time_in_queue'] = max_time_in_queue['default']
395
438
 
396
- overwrite_hop = True
397
- for transfer_hop in transfer_path[:-1]:
398
- # Only allow overwrite if all hops in multihop allow it
399
- h_overwrite = transfer_hop.rws.attributes.get('overwrite', True)
400
- job_params['overwrite'] = h_overwrite and job_params['overwrite']
401
- # Allow overwrite_hop if all intermediate hops allow it (ignoring the last hop)
402
- overwrite_hop = h_overwrite and overwrite_hop
439
+ # Refer to https://its.cern.ch/jira/browse/FTS-1749 for full details (login needed), extract below:
440
+ # Why the overwrite_hop parameter is needed?
441
+ # Rucio decides that a multihop transfer is needed DISK1 --> DISK2 --> TAPE1 in order to put the file on tape. For some reason, the file already exists on DISK2.
442
+ # Rucio doesn't know about this file on DISK2. It could be either a correct or corrupted file. This can be due to a previous issue on Rucio side, FTS side, network side, etc (many possible reasons).
443
+ # Normally, Rucio allows overwrite towards any disk destination, but denies overwrite towards a tape destination. However, in this case, because the destination of the multihop is a tape, DISK2 cannot be overwritten.
444
+ # Proposed solution
445
+ # Provide an --overwrite-hop submission option, which instructs FTS to overwrite all transfers except for the destination within a multihop submission.
446
+
447
+ # direct transfers, is not multihop
448
+ if len(transfer_path) == 1:
449
+ overwrite_hop = False
450
+
451
+ else:
452
+ # Set `overwrite_hop` to `True` only if all hops allow it
453
+ overwrite_hop = all(transfer_hop.rws.attributes.get('overwrite', True) for transfer_hop in transfer_path[:-1])
454
+ job_params['overwrite'] = overwrite_hop and job_params['overwrite']
455
+
403
456
  if not job_params['overwrite'] and overwrite_hop:
404
457
  job_params['overwrite_hop'] = overwrite_hop
405
458
 
459
+ logger(logging.DEBUG, 'Job parameters are : %s' % (job_params))
406
460
  return job_params
407
461
 
408
462
 
409
- def bulk_group_transfers(transfer_paths, policy='rule', group_bulk=200, source_strategy=None, max_time_in_queue=None,
410
- logger=logging.log, archive_timeout_override=None, bring_online=None, default_lifetime=None):
463
+ def bulk_group_transfers(
464
+ transfer_paths: "Iterable[list[DirectTransfer]]",
465
+ policy: str = 'rule',
466
+ group_bulk: int = 200,
467
+ source_strategy: Optional[str] = None,
468
+ max_time_in_queue: Optional[dict] = None,
469
+ logger: "LoggerFunction" = logging.log,
470
+ archive_timeout_override: Optional[int] = None,
471
+ bring_online: Optional[int] = None,
472
+ default_lifetime: Optional[int] = None) -> list[dict[str, Any]]:
411
473
  """
412
- Group transfers in bulk based on certain criterias
474
+ Group transfers in bulk based on certain criteria
413
475
 
414
476
  :param transfer_paths: List of transfer paths to group. Each path is a list of single-hop transfers.
415
477
  :param policy: Policy to use to group.
@@ -433,6 +495,7 @@ def bulk_group_transfers(transfer_paths, policy='rule', group_bulk=200, source_s
433
495
  max_time_in_queue=max_time_in_queue,
434
496
  logger=logger
435
497
  )
498
+ logger(logging.DEBUG, 'bulk_group_transfers: Job parameters are: %s' % (job_params))
436
499
  if job_params['job_metadata'].get('multi_sources') or job_params['job_metadata'].get('multihop'):
437
500
  # for multi-hop and multi-source transfers, no bulk submission.
438
501
  fts_jobs.append({'transfers': transfer_path[0:group_bulk], 'job_params': job_params})
@@ -478,6 +541,7 @@ def bulk_group_transfers(transfer_paths, policy='rule', group_bulk=200, source_s
478
541
  # split transfer groups to have at most group_bulk elements in each one
479
542
  for group in grouped_transfers.values():
480
543
  job_params = group['job_params']
544
+ logger(logging.DEBUG, 'bulk_group_transfers: grouped_transfers.values(): Job parameters are: %s' % (job_params))
481
545
  for transfer_paths in chunks(group['transfers'], group_bulk):
482
546
  fts_jobs.append({'transfers': transfer_paths, 'job_params': job_params})
483
547
 
@@ -498,7 +562,7 @@ class Fts3TransferStatusReport(TransferStatusReport):
498
562
  'attributes',
499
563
  ]
500
564
 
501
- def __init__(self, external_host, request_id, request=None):
565
+ def __init__(self, external_host: str, request_id: str, request: Optional[dict] = None):
502
566
  super().__init__(request_id, request=request)
503
567
  self.external_host = external_host
504
568
 
@@ -512,7 +576,7 @@ class Fts3TransferStatusReport(TransferStatusReport):
512
576
  self._reason = None
513
577
  self._src_rse = None
514
578
  self._fts_address = self.external_host
515
- # Supported db fields bellow:
579
+ # Supported db fields below:
516
580
  self.state = None
517
581
  self.external_id = None
518
582
  self.started_at = None
@@ -527,10 +591,10 @@ class Fts3TransferStatusReport(TransferStatusReport):
527
591
  return f'Transfer {self._transfer_id} of {self._file_metadata["scope"]}:{self._file_metadata["name"]} ' \
528
592
  f'{self._file_metadata["src_rse"]} --({self._file_metadata["request_id"]})-> {self._file_metadata["dst_rse"]}'
529
593
 
530
- def initialize(self, session, logger=logging.log):
594
+ def initialize(self, session: "Session", logger: "LoggerFunction" = logging.log) -> None:
531
595
  raise NotImplementedError(f"{self.__class__.__name__} is abstract and shouldn't be used directly")
532
596
 
533
- def get_monitor_msg_fields(self, session, logger=logging.log):
597
+ def get_monitor_msg_fields(self, session: "Session", logger: "LoggerFunction" = logging.log) -> dict[str, Any]:
534
598
  self.ensure_initialized(session, logger)
535
599
  fields = {
536
600
  'transfer_link': self._transfer_link(),
@@ -546,10 +610,10 @@ class Fts3TransferStatusReport(TransferStatusReport):
546
610
  }
547
611
  return fields
548
612
 
549
- def _transfer_link(self):
613
+ def _transfer_link(self) -> str:
550
614
  return '%s/fts3/ftsmon/#/job/%s' % (self._fts_address.replace('8446', '8449'), self._transfer_id)
551
615
 
552
- def _find_attribute_updates(self, request, new_state, reason, overwrite_corrupted_files):
616
+ def _find_attribute_updates(self, request: dict, new_state: RequestState, reason: str, overwrite_corrupted_files: Optional[bool] = None) -> Optional[dict[str, Any]]:
553
617
  attributes = None
554
618
  if new_state == RequestState.FAILED and 'Destination file exists and overwrite is not enabled' in (reason or ''):
555
619
  dst_file = self._file_metadata.get('dst_file', {})
@@ -559,7 +623,7 @@ class Fts3TransferStatusReport(TransferStatusReport):
559
623
  attributes['overwrite'] = True
560
624
  return attributes
561
625
 
562
- def _find_used_source_rse(self, session, logger):
626
+ def _find_used_source_rse(self, session: "Session", logger: "LoggerFunction") -> tuple[Optional[str], Optional[str]]:
563
627
  """
564
628
  For multi-source transfers, FTS has a choice between multiple sources.
565
629
  Find which of the possible sources FTS actually used for the transfer.
@@ -577,7 +641,7 @@ class Fts3TransferStatusReport(TransferStatusReport):
577
641
  return meta_rse_name, meta_rse_id
578
642
 
579
643
  @staticmethod
580
- def _dst_file_set_and_file_corrupted(request, dst_file):
644
+ def _dst_file_set_and_file_corrupted(request: dict, dst_file: dict) -> bool:
581
645
  """
582
646
  Returns True if the `dst_file` dict returned by fts was filled and its content allows to
583
647
  affirm that the file is corrupted.
@@ -590,7 +654,7 @@ class Fts3TransferStatusReport(TransferStatusReport):
590
654
  return False
591
655
 
592
656
  @staticmethod
593
- def _dst_file_set_and_file_correct(request, dst_file):
657
+ def _dst_file_set_and_file_correct(request: dict, dst_file: dict) -> bool:
594
658
  """
595
659
  Returns True if the `dst_file` dict returned by fts was filled and its content allows to
596
660
  affirm that the file is correct.
@@ -635,19 +699,21 @@ class FTS3CompletionMessageTransferStatusReport(Fts3TransferStatusReport):
635
699
  """
636
700
  Parses FTS Completion messages received via the message queue
637
701
  """
638
- def __init__(self, external_host, request_id, fts_message):
702
+ def __init__(self, external_host: str, request_id: str, fts_message: dict[str, Any]):
639
703
  super().__init__(external_host=external_host, request_id=request_id)
640
704
 
641
705
  self.fts_message = fts_message
642
706
 
643
- self._transfer_id = fts_message.get('tr_id').split("__")[-1]
707
+ transfer_id = fts_message.get('tr_id')
708
+ if transfer_id is not None:
709
+ self._transfer_id = transfer_id.split("__")[-1]
644
710
 
645
711
  self._file_metadata = fts_message['file_metadata']
646
- self._multi_sources = str(fts_message.get('job_metadata', {}).get('multi_sources', '')).lower() == str('true')
712
+ self._multi_sources = str(fts_message.get('job_metadata', {}).get('multi_sources', '')).lower() == 'true'
647
713
  self._src_url = fts_message.get('src_url', None)
648
714
  self._dst_url = fts_message.get('dst_url', None)
649
715
 
650
- def initialize(self, session, logger=logging.log):
716
+ def initialize(self, session: "Session", logger: "LoggerFunction" = logging.log) -> None:
651
717
 
652
718
  fts_message = self.fts_message
653
719
  request_id = self.request_id
@@ -659,10 +725,11 @@ class FTS3CompletionMessageTransferStatusReport(Fts3TransferStatusReport):
659
725
  new_state = RequestState.DONE
660
726
  elif str(fts_message['t_final_transfer_state']) == FTS_COMPLETE_STATE.ERROR:
661
727
  request = self.request(session)
662
- if self._is_recoverable_fts_overwrite_error(request, reason, self._file_metadata): # pylint:disable=no-member
663
- new_state = RequestState.DONE
664
- else:
665
- new_state = RequestState.FAILED
728
+ if request is not None:
729
+ if self._is_recoverable_fts_overwrite_error(request, reason, self._file_metadata): # pylint:disable=no-member
730
+ new_state = RequestState.DONE
731
+ else:
732
+ new_state = RequestState.FAILED
666
733
 
667
734
  transfer_id = self._transfer_id
668
735
  if new_state:
@@ -705,7 +772,7 @@ class FTS3ApiTransferStatusReport(Fts3TransferStatusReport):
705
772
  """
706
773
  Parses FTS api response
707
774
  """
708
- def __init__(self, external_host, request_id, job_response, file_response, request=None):
775
+ def __init__(self, external_host: str, request_id: str, job_response: dict[str, Any], file_response: dict[str, Any], request: Optional[dict[str, Any]] = None):
709
776
  super().__init__(external_host=external_host, request_id=request_id, request=request)
710
777
 
711
778
  self.job_response = job_response
@@ -714,12 +781,12 @@ class FTS3ApiTransferStatusReport(Fts3TransferStatusReport):
714
781
  self._transfer_id = job_response.get('job_id')
715
782
 
716
783
  self._file_metadata = file_response['file_metadata']
717
- self._multi_sources = str(job_response['job_metadata'].get('multi_sources', '')).lower() == str('true')
784
+ self._multi_sources = str(job_response['job_metadata'].get('multi_sources', '')).lower() == 'true'
718
785
  self._src_url = file_response.get('source_surl', None)
719
786
  self._dst_url = file_response.get('dest_surl', None)
720
787
  self.logger = logging.log
721
788
 
722
- def initialize(self, session, logger=logging.log):
789
+ def initialize(self, session: "Session", logger=logging.log) -> None:
723
790
 
724
791
  self.logger = logger
725
792
  job_response = self.job_response
@@ -739,10 +806,12 @@ class FTS3ApiTransferStatusReport(Fts3TransferStatusReport):
739
806
  new_state = RequestState.DONE
740
807
  elif file_state == FTS_STATE.FAILED and job_state_is_final or \
741
808
  file_state == FTS_STATE.FAILED and not self._multi_sources: # for multi-source transfers we must wait for the job to be in a final state
742
- if self._is_recoverable_fts_overwrite_error(self.request(session), reason, self._file_metadata):
743
- new_state = RequestState.DONE
744
- else:
745
- new_state = RequestState.FAILED
809
+ request = self.request(session)
810
+ if request is not None:
811
+ if self._is_recoverable_fts_overwrite_error(request, reason, self._file_metadata):
812
+ new_state = RequestState.DONE
813
+ else:
814
+ new_state = RequestState.FAILED
746
815
  elif job_state_is_final and file_state == FTS_STATE.CANCELED:
747
816
  new_state = RequestState.FAILED
748
817
  elif job_state_is_final and file_state == FTS_STATE.NOT_USED:
@@ -795,16 +864,27 @@ class FTS3Transfertool(Transfertool):
795
864
  """
796
865
 
797
866
  external_name = 'fts3'
798
- required_rse_attrs = ('fts', )
799
-
800
- def __init__(self, external_host, oidc_account=None, vo=None, group_bulk=1, group_policy='rule', source_strategy=None,
801
- max_time_in_queue=None, bring_online=43200, default_lifetime=172800, archive_timeout_override=None,
802
- logger=logging.log):
867
+ required_rse_attrs = (RseAttr.FTS, )
868
+ supported_schemes = Transfertool.supported_schemes.union(('mock', ))
869
+
870
+ def __init__(self,
871
+ external_host: str,
872
+ oidc_account: Optional[str] = None,
873
+ oidc_support: bool = False,
874
+ vo: Optional[str] = None,
875
+ group_bulk: int = 1,
876
+ group_policy: str = 'rule',
877
+ source_strategy: Optional[str] = None,
878
+ max_time_in_queue: Optional[dict[str, Any]] = None,
879
+ bring_online: Optional[int] = 43200,
880
+ default_lifetime: Optional[int] = 172800,
881
+ archive_timeout_override: Optional[int] = None,
882
+ logger: "LoggerFunction" = logging.log
883
+ ):
803
884
  """
804
885
  Initializes the transfertool
805
886
 
806
887
  :param external_host: The external host where the transfertool API is running
807
- :param oidc_account: optional oidc account to use for submission
808
888
  """
809
889
  super().__init__(external_host, logger)
810
890
 
@@ -816,20 +896,23 @@ class FTS3Transfertool(Transfertool):
816
896
  self.default_lifetime = default_lifetime
817
897
  self.archive_timeout_override = archive_timeout_override
818
898
 
819
- # token for OAuth 2.0 OIDC authorization scheme (working only with dCache + davs/https protocols as of Sep 2019)
899
+ try:
900
+ tape_plugins = config_get_list("transfers", "fts3tape_metadata_plugins", raise_exception=True)
901
+ self.tape_metadata_plugins = [FTS3TapeMetadataPlugin(plugin.strip(" ")) for plugin in tape_plugins]
902
+ except (NoOptionError, NoSectionError, ValueError) as e:
903
+ self.logger(logging.DEBUG, f"Failed to set up any fts3 archive-metadata plugins: {e}")
904
+ self.tape_metadata_plugins = []
905
+
820
906
  self.token = None
821
- if oidc_account:
822
- getadmintoken = False
823
- if ALLOW_USER_OIDC_TOKENS is False:
824
- getadmintoken = True
825
- self.logger(logging.DEBUG, 'Attempting to get a token for account %s. Admin token option set to %s' % (oidc_account, getadmintoken))
826
- # find the appropriate OIDC token and exchange it (for user accounts) if necessary
827
- token_dict = get_token_for_account_operation(oidc_account, req_audience=REQUEST_OIDC_AUDIENCE, req_scope=REQUEST_OIDC_SCOPE, admin=getadmintoken)
828
- if token_dict is not None:
829
- self.logger(logging.DEBUG, 'Access token has been granted.')
830
- if 'token' in token_dict:
831
- self.logger(logging.DEBUG, 'Access token used as transfer token.')
832
- self.token = token_dict['token']
907
+ if oidc_support:
908
+ fts_hostname = urlparse(external_host).hostname
909
+ if fts_hostname is not None:
910
+ token = request_token(audience=fts_hostname, scope='fts')
911
+ if token is not None:
912
+ self.logger(logging.INFO, 'Using a token to authenticate with FTS instance %s', fts_hostname)
913
+ self.token = token
914
+ else:
915
+ self.logger(logging.WARNING, 'Failed to procure a token to authenticate with FTS instance %s', fts_hostname)
833
916
 
834
917
  self.deterministic_id = config_get_bool('conveyor', 'use_deterministic_id', False, False)
835
918
  self.headers = {'Content-Type': 'application/json'}
@@ -849,29 +932,29 @@ class FTS3Transfertool(Transfertool):
849
932
  self.scitags_exp_id, self.scitags_activity_ids = _scitags_ids(logger=logger)
850
933
 
851
934
  @classmethod
852
- def _pick_fts_servers(cls, source_rse: "RseData", dest_rse: "RseData"):
935
+ def _pick_fts_servers(cls, source_rse: "RseData", dest_rse: "RseData") -> Optional[list[str]]:
853
936
  """
854
937
  Pick fts servers to use for submission between the two given rse
855
938
  """
856
- source_servers = source_rse.attributes.get('fts', None)
857
- dest_servers = dest_rse.attributes.get('fts', None)
939
+ source_servers = source_rse.attributes.get(RseAttr.FTS, None)
940
+ dest_servers = dest_rse.attributes.get(RseAttr.FTS, None)
858
941
  if source_servers is None or dest_servers is None:
859
942
  return None
860
943
 
861
944
  servers_to_use = dest_servers
862
- if source_rse.attributes.get('sign_url', None) == 'gcs':
945
+ if source_rse.attributes.get(RseAttr.SIGN_URL, None) == 'gcs':
863
946
  servers_to_use = source_servers
864
947
 
865
948
  return servers_to_use.split(',')
866
949
 
867
950
  @classmethod
868
- def can_perform_transfer(cls, source_rse: "RseData", dest_rse: "RseData"):
951
+ def can_perform_transfer(cls, source_rse: "RseData", dest_rse: "RseData") -> bool:
869
952
  if cls._pick_fts_servers(source_rse, dest_rse):
870
953
  return True
871
954
  return False
872
955
 
873
956
  @classmethod
874
- def submission_builder_for_path(cls, transfer_path, logger=logging.log):
957
+ def submission_builder_for_path(cls, transfer_path: "list[DirectTransfer]", logger: "LoggerFunction" = logging.log):
875
958
  vo = None
876
959
  if config_get_bool('common', 'multi_vo', False, None):
877
960
  vo = transfer_path[-1].rws.scope.vo
@@ -890,15 +973,15 @@ class FTS3Transfertool(Transfertool):
890
973
  logger(logging.INFO, 'FTS3Transfertool can only submit {} hops from {}'.format(len(sub_path), [str(hop) for hop in transfer_path]))
891
974
 
892
975
  if sub_path:
893
- oidc_account = None
894
- if all(oidc_supported(t) for t in sub_path):
976
+ oidc_support = False
977
+ if all(_use_tokens(t) for t in sub_path):
895
978
  logger(logging.DEBUG, 'OAuth2/OIDC available for transfer {}'.format([str(hop) for hop in sub_path]))
896
- oidc_account = transfer_path[-1].rws.account
897
- return sub_path, TransferToolBuilder(cls, external_host=fts_hosts[0], oidc_account=oidc_account, vo=vo)
979
+ oidc_support = True
980
+ return sub_path, TransferToolBuilder(cls, external_host=fts_hosts[0], oidc_support=oidc_support, vo=vo)
898
981
  else:
899
982
  return [], None
900
983
 
901
- def group_into_submit_jobs(self, transfer_paths):
984
+ def group_into_submit_jobs(self, transfer_paths: "list[list[DirectTransfer]]") -> list[dict[str, Any]]:
902
985
  jobs = bulk_group_transfers(
903
986
  transfer_paths,
904
987
  policy=self.group_policy,
@@ -912,11 +995,11 @@ class FTS3Transfertool(Transfertool):
912
995
  )
913
996
  return jobs
914
997
 
915
- def _file_from_transfer(self, transfer, job_params):
998
+ def _file_from_transfer(self, transfer: "DirectTransfer", job_params: dict[str, str]) -> dict[str, Any]:
916
999
  rws = transfer.rws
917
1000
  checksum_to_use = _pick_fts_checksum(transfer, path_strategy=job_params['verify_checksum'])
918
1001
  t_file = {
919
- 'sources': [s[1] for s in transfer.legacy_sources],
1002
+ 'sources': [transfer.source_url(s) for s in transfer.sources],
920
1003
  'destinations': [transfer.dest_url],
921
1004
  'metadata': {
922
1005
  'request_id': rws.request_id,
@@ -939,13 +1022,32 @@ class FTS3Transfertool(Transfertool):
939
1022
  'selection_strategy': self.source_strategy if self.source_strategy else _configured_source_strategy(transfer.rws.activity, logger=self.logger),
940
1023
  'activity': rws.activity
941
1024
  }
1025
+
1026
+ if self.token:
1027
+ t_file['source_tokens'] = []
1028
+ for source in transfer.sources:
1029
+ src_audience = determine_audience_for_rse(rse_id=source.rse.id)
1030
+ src_scope = determine_scope_for_rse(rse_id=source.rse.id, scopes=['storage.read'], extra_scopes=['offline_access'])
1031
+ t_file['source_tokens'].append(request_token(src_audience, src_scope))
1032
+
1033
+ dst_audience = determine_audience_for_rse(transfer.dst.rse.id)
1034
+ # FIXME: At the time of writing, StoRM requires `storage.read` in
1035
+ # order to perform a stat operation.
1036
+ dst_scope = determine_scope_for_rse(transfer.dst.rse.id, scopes=['storage.modify', 'storage.read'], extra_scopes=['offline_access'])
1037
+ t_file['destination_tokens'] = [request_token(dst_audience, dst_scope)]
1038
+
942
1039
  if isinstance(self.scitags_exp_id, int):
943
1040
  activity_id = self.scitags_activity_ids.get(rws.activity)
944
1041
  if isinstance(activity_id, int):
945
1042
  t_file['scitag'] = self.scitags_exp_id << 6 | activity_id
1043
+
1044
+ if t_file['metadata']['dst_type'] == 'TAPE':
1045
+ for plugin in self.tape_metadata_plugins:
1046
+ t_file = deep_merge_dict(source=plugin.hints(t_file['metadata']), destination=t_file)
1047
+
946
1048
  return t_file
947
1049
 
948
- def submit(self, transfers, job_params, timeout=None):
1050
+ def submit(self, transfers: "Sequence[DirectTransfer]", job_params: dict[str, str], timeout: Optional[int] = None) -> str:
949
1051
  """
950
1052
  Submit transfers to FTS3 via JSON.
951
1053
 
@@ -1031,7 +1133,7 @@ class FTS3Transfertool(Transfertool):
1031
1133
  METRICS.timer('submit_transfers_fts3').observe(stopwatch.elapsed / (len(transfers) or 1))
1032
1134
  return transfer_id
1033
1135
 
1034
- def cancel(self, transfer_ids, timeout=None):
1136
+ def cancel(self, transfer_ids: "Sequence[str]", timeout: Optional[int] = None) -> dict[str, Any]:
1035
1137
  """
1036
1138
  Cancel transfers that have been submitted to FTS3.
1037
1139
 
@@ -1059,7 +1161,7 @@ class FTS3Transfertool(Transfertool):
1059
1161
  CANCEL_COUNTER.labels(state='failure', host=self.__extract_host(self.external_host)).inc()
1060
1162
  raise Exception('Could not cancel transfer: %s', job.content)
1061
1163
 
1062
- def update_priority(self, transfer_id, priority, timeout=None):
1164
+ def update_priority(self, transfer_id: str, priority: int, timeout: Optional[int] = None) -> dict[str, Any]:
1063
1165
  """
1064
1166
  Update the priority of a transfer that has been submitted to FTS via JSON.
1065
1167
 
@@ -1087,7 +1189,7 @@ class FTS3Transfertool(Transfertool):
1087
1189
  UPDATE_PRIORITY_COUNTER.labels(state='failure', host=self.__extract_host(self.external_host)).inc()
1088
1190
  raise Exception('Could not update priority of transfer: %s', job.content)
1089
1191
 
1090
- def query(self, transfer_ids, details=False, timeout=None):
1192
+ def query(self, transfer_ids: "Sequence[str]", details: bool = False, timeout: Optional[int] = None) -> Union[Optional[dict[str, Any]], list[dict[str, Any]]]:
1091
1193
  """
1092
1194
  Query the status of a transfer in FTS3 via JSON.
1093
1195
 
@@ -1120,7 +1222,7 @@ class FTS3Transfertool(Transfertool):
1120
1222
 
1121
1223
  # Public methods, not part of the common interface specification (FTS3 specific)
1122
1224
 
1123
- def whoami(self):
1225
+ def whoami(self) -> dict[str, Any]:
1124
1226
  """
1125
1227
  Returns credential information from the FTS3 server.
1126
1228
 
@@ -1141,7 +1243,7 @@ class FTS3Transfertool(Transfertool):
1141
1243
  WHOAMI_COUNTER.labels(state='failure', host=self.__extract_host(self.external_host)).inc()
1142
1244
  raise Exception('Could not retrieve credentials: %s', get_result.content)
1143
1245
 
1144
- def version(self):
1246
+ def version(self) -> dict[str, Any]:
1145
1247
  """
1146
1248
  Returns FTS3 server information.
1147
1249
 
@@ -1162,7 +1264,7 @@ class FTS3Transfertool(Transfertool):
1162
1264
  VERSION_COUNTER.labels(state='failure', host=self.__extract_host(self.external_host)).inc()
1163
1265
  raise Exception('Could not retrieve version: %s', get_result.content)
1164
1266
 
1165
- def bulk_query(self, requests_by_eid, timeout=None):
1267
+ def bulk_query(self, requests_by_eid: dict[str, dict[str, dict[str, Any]]], timeout: Optional[int] = None) -> dict[str, Any]:
1166
1268
  """
1167
1269
  Query the status of a bulk of transfers in FTS3 via JSON.
1168
1270
 
@@ -1201,11 +1303,11 @@ class FTS3Transfertool(Transfertool):
1201
1303
 
1202
1304
  return responses
1203
1305
 
1204
- def list_se_status(self):
1306
+ def list_se_status(self) -> dict[str, Any]:
1205
1307
  """
1206
1308
  Get the list of banned Storage Elements.
1207
1309
 
1208
- :returns: Detailed dictionnary of banned Storage Elements.
1310
+ :returns: Detailed dictionary of banned Storage Elements.
1209
1311
  """
1210
1312
 
1211
1313
  try:
@@ -1220,7 +1322,7 @@ class FTS3Transfertool(Transfertool):
1220
1322
  return result.json()
1221
1323
  raise Exception('Could not retrieve transfer information: %s', result.content)
1222
1324
 
1223
- def get_se_config(self, storage_element):
1325
+ def get_se_config(self, storage_element: str) -> dict[str, Any]:
1224
1326
  """
1225
1327
  Get the Json response for the configuration of a storage element.
1226
1328
  :returns: a Json result for the configuration of a storage element.
@@ -1241,7 +1343,15 @@ class FTS3Transfertool(Transfertool):
1241
1343
  return config_se
1242
1344
  raise Exception('Could not get the configuration of %s , status code returned : %s', (storage_element, result.status_code if result else None))
1243
1345
 
1244
- def set_se_config(self, storage_element, inbound_max_active=None, outbound_max_active=None, inbound_max_throughput=None, outbound_max_throughput=None, staging=None):
1346
+ def set_se_config(
1347
+ self,
1348
+ storage_element: str,
1349
+ inbound_max_active: Optional[int] = None,
1350
+ outbound_max_active: Optional[int] = None,
1351
+ inbound_max_throughput: Optional[float] = None,
1352
+ outbound_max_throughput: Optional[float] = None,
1353
+ staging: Optional[int] = None
1354
+ ) -> dict[str, Any]:
1245
1355
  """
1246
1356
  Set the configuration for a storage element. Used for alleviating transfer failures due to timeout.
1247
1357
 
@@ -1301,7 +1411,7 @@ class FTS3Transfertool(Transfertool):
1301
1411
  return configSe
1302
1412
  raise Exception('Could not set the configuration of %s , status code returned : %s', (storage_element, result.status_code if result else None))
1303
1413
 
1304
- def set_se_status(self, storage_element, message, ban=True, timeout=None):
1414
+ def set_se_status(self, storage_element: str, message: str, ban: bool = True, timeout: Optional[int] = None) -> int:
1305
1415
  """
1306
1416
  Ban a Storage Element. Used when a site is in downtime.
1307
1417
  One can use a timeout in seconds. In that case the jobs will wait before being cancel.
@@ -1315,7 +1425,7 @@ class FTS3Transfertool(Transfertool):
1315
1425
  :returns: 0 in case of success, otherwise raise Exception
1316
1426
  """
1317
1427
 
1318
- params_dict = {'storage': storage_element, 'message': message}
1428
+ params_dict: dict[str, Any] = {'storage': storage_element, 'message': message}
1319
1429
  status = 'CANCEL'
1320
1430
  if timeout:
1321
1431
  params_dict['timeout'] = timeout
@@ -1355,11 +1465,13 @@ class FTS3Transfertool(Transfertool):
1355
1465
  # Private methods unique to the FTS3 Transfertool
1356
1466
 
1357
1467
  @staticmethod
1358
- def __extract_host(external_host):
1468
+ def __extract_host(external_host: str) -> Optional[str]:
1359
1469
  # graphite does not like the dots in the FQDN
1360
- return urlparse(external_host).hostname.replace('.', '_')
1470
+ parsed_url = urlparse(external_host)
1471
+ if parsed_url.hostname:
1472
+ return parsed_url.hostname.replace('.', '_')
1361
1473
 
1362
- def __get_transfer_baseid_voname(self):
1474
+ def __get_transfer_baseid_voname(self) -> tuple[Optional[str], Optional[str]]:
1363
1475
  """
1364
1476
  Get transfer VO name from the external host.
1365
1477
 
@@ -1402,7 +1514,7 @@ class FTS3Transfertool(Transfertool):
1402
1514
  result = (None, None)
1403
1515
  return result
1404
1516
 
1405
- def __get_deterministic_id(self, sid):
1517
+ def __get_deterministic_id(self, sid: str) -> Optional[str]:
1406
1518
  """
1407
1519
  Get deterministic FTS job id.
1408
1520
 
@@ -1417,7 +1529,7 @@ class FTS3Transfertool(Transfertool):
1417
1529
  jobid = uuid.uuid5(atlas, sid)
1418
1530
  return str(jobid)
1419
1531
 
1420
- def __bulk_query_responses(self, jobs_response, requests_by_eid):
1532
+ def __bulk_query_responses(self, jobs_response: list[dict[str, Any]], requests_by_eid: dict[str, dict[str, dict[str, Any]]]) -> dict[str, Any]:
1421
1533
  if not isinstance(jobs_response, list):
1422
1534
  jobs_response = [jobs_response]
1423
1535
 
@@ -1431,7 +1543,7 @@ class FTS3Transfertool(Transfertool):
1431
1543
  FTS_STATE.FINISHEDDIRTY,
1432
1544
  FTS_STATE.CANCELED,
1433
1545
  FTS_STATE.FINISHED]:
1434
- # multipe source replicas jobs is still running. should wait
1546
+ # multiple source replicas jobs is still running. should wait
1435
1547
  responses[transfer_id] = {}
1436
1548
  continue
1437
1549
 
@@ -1461,7 +1573,7 @@ class FTS3Transfertool(Transfertool):
1461
1573
  job_response['http_message'] if 'http_message' in job_response else None))
1462
1574
  return responses
1463
1575
 
1464
- def __query_details(self, transfer_id):
1576
+ def __query_details(self, transfer_id: str) -> Optional[dict[str, Any]]:
1465
1577
  """
1466
1578
  Query the detailed status of a transfer in FTS3 via JSON.
1467
1579