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
@@ -0,0 +1,1596 @@
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 datetime
16
+ import json
17
+ import logging
18
+ import pathlib
19
+ import traceback
20
+ import uuid
21
+ from configparser import NoOptionError, NoSectionError
22
+ from json import loads
23
+ from typing import TYPE_CHECKING, Any, Optional, Union
24
+ from urllib.parse import urlparse
25
+
26
+ import requests
27
+ from dogpile.cache.api import NoValue
28
+ from requests.adapters import ReadTimeout
29
+ from requests.packages.urllib3 import disable_warnings # pylint: disable=import-error
30
+
31
+ from rucio.common.cache import make_region_memcached
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
35
+ from rucio.common.stopwatch import Stopwatch
36
+ from rucio.common.utils import PREFERRED_CHECKSUM, APIEncoder, chunks, deep_merge_dict
37
+ from rucio.core.monitor import MetricManager
38
+ from rucio.core.oidc import request_token
39
+ from rucio.core.request import get_source_rse, get_transfer_error
40
+ from rucio.core.rse import determine_audience_for_rse, determine_scope_for_rse, get_rse_supported_checksums_from_attributes
41
+ from rucio.db.sqla.constants import RequestState
42
+ from rucio.transfertool.fts3_plugins import FTS3TapeMetadataPlugin
43
+ from rucio.transfertool.transfertool import TransferStatusReport, Transfertool, TransferToolBuilder
44
+
45
+ if TYPE_CHECKING:
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
52
+ from rucio.core.rse import RseData
53
+
54
+ logging.getLogger("requests").setLevel(logging.CRITICAL)
55
+ disable_warnings()
56
+
57
+ REGION_SHORT = make_region_memcached(expiration_time=900)
58
+ METRICS = MetricManager(module=__name__)
59
+
60
+ SUBMISSION_COUNTER = METRICS.counter(name='{host}.submission.{state}',
61
+ documentation='Number of transfers submitted', labelnames=('state', 'host'))
62
+ CANCEL_COUNTER = METRICS.counter(name='{host}.cancel.{state}',
63
+ documentation='Number of cancelled transfers', labelnames=('state', 'host'))
64
+ UPDATE_PRIORITY_COUNTER = METRICS.counter(name='{host}.update_priority.{state}',
65
+ documentation='Number of priority updates', labelnames=('state', 'host'))
66
+ QUERY_COUNTER = METRICS.counter(name='{host}.query.{state}',
67
+ documentation='Number of queried transfers', labelnames=('state', 'host'))
68
+ WHOAMI_COUNTER = METRICS.counter(name='{host}.whoami.{state}',
69
+ documentation='Number of whoami requests', labelnames=('state', 'host'))
70
+ VERSION_COUNTER = METRICS.counter(name='{host}.version.{state}',
71
+ documentation='Number of version requests', labelnames=('state', 'host'))
72
+ BULK_QUERY_COUNTER = METRICS.counter(name='{host}.bulk_query.{state}',
73
+ documentation='Number of bulk queries', labelnames=('state', 'host'))
74
+ QUERY_DETAILS_COUNTER = METRICS.counter(name='{host}.query_details.{state}',
75
+ documentation='Number of detailed status queries', labelnames=('state', 'host'))
76
+
77
+ REWRITE_HTTPS_TO_DAVS = config_get_bool('transfers', 'rewrite_https_to_davs', default=False)
78
+ VO_CERTS_PATH = config_get('conveyor', 'vo_certs_path', False, None)
79
+
80
+ # https://fts3-docs.web.cern.ch/fts3-docs/docs/state_machine.html
81
+ FINAL_FTS_JOB_STATES = (FTS_STATE.FAILED, FTS_STATE.CANCELED, FTS_STATE.FINISHED, FTS_STATE.FINISHEDDIRTY)
82
+ FINAL_FTS_FILE_STATES = (FTS_STATE.FAILED, FTS_STATE.CANCELED, FTS_STATE.FINISHED, FTS_STATE.NOT_USED)
83
+
84
+ # In a multi-hop transfer, we must compute a checksum validation strategy valid for the whole path.
85
+ # This state-machine defines how strategies of hops are merged into a path-wide strategy.
86
+ # For example, if HOP1 supports only validation of checksum at source while HOP2 only
87
+ # supports validation at destination, the strategy for the whole path MUST be "none". Otherwise,
88
+ # transfers will fail when FTS will try to validate the checksum.
89
+ PATH_CHECKSUM_VALIDATION_STRATEGY: dict[tuple[str, str], str] = {
90
+ ('both', 'both'): 'both',
91
+ ('both', 'target'): 'target',
92
+ ('both', 'source'): 'source',
93
+ ('both', 'none'): 'none',
94
+ ('target', 'both'): 'target',
95
+ ('target', 'target'): 'target',
96
+ ('target', 'source'): 'none',
97
+ ('target', 'none'): 'none',
98
+ ('source', 'both'): 'source',
99
+ ('source', 'target'): 'none',
100
+ ('source', 'source'): 'source',
101
+ ('source', 'none'): 'none',
102
+ ('none', 'both'): 'none',
103
+ ('none', 'target'): 'none',
104
+ ('none', 'source'): 'none',
105
+ ('none', 'none'): 'none',
106
+ }
107
+
108
+ _SCITAGS_NEXT_REFRESH = datetime.datetime.utcnow()
109
+ _SCITAGS_EXP_ID = None
110
+ _SCITAGS_ACTIVITY_IDS = {}
111
+
112
+
113
+ def _scitags_ids(logger: "LoggerFunction" = logging.log) -> "tuple[int | None, dict[str, int]]":
114
+ """
115
+ Re-fetch if needed and return the scitags ids
116
+ """
117
+ enabled = config_get_bool('packet-marking', 'enabled', default=False)
118
+ if not enabled:
119
+ return None, {}
120
+
121
+ now = datetime.datetime.utcnow()
122
+ global _SCITAGS_ACTIVITY_IDS
123
+ global _SCITAGS_EXP_ID
124
+ global _SCITAGS_NEXT_REFRESH
125
+ if _SCITAGS_NEXT_REFRESH < now:
126
+ exp_name = config_get('packet-marking', 'exp_name', default='')
127
+ fetch_url = config_get('packet-marking', 'fetch_url', default='https://www.scitags.org/api.json')
128
+ fetch_interval = config_get_int('packet-marking', 'fetch_interval', default=int(datetime.timedelta(hours=48).total_seconds()))
129
+ fetch_timeout = config_get_int('packet-marking', 'fetch_timeout', default=5)
130
+
131
+ _SCITAGS_NEXT_REFRESH = now + datetime.timedelta(seconds=fetch_interval)
132
+
133
+ if exp_name:
134
+ had_exception = False
135
+ exp_id = None
136
+ activity_ids = {}
137
+ try:
138
+ result = requests.get(fetch_url, timeout=fetch_timeout)
139
+ if result and result.status_code == 200:
140
+ marks = result.json()
141
+ for experiment in marks.get('experiments', []):
142
+ if experiment.get('expName') == exp_name:
143
+ exp_id = experiment.get('expId')
144
+ for activity_dict in experiment.get('activities', []):
145
+ activity_name = activity_dict.get('activityName')
146
+ activity_id = activity_dict.get('activityId')
147
+ if activity_name and activity_id:
148
+ activity_ids[activity_name] = int(activity_id)
149
+ break
150
+ except (requests.exceptions.RequestException, TypeError, ValueError):
151
+ had_exception = True
152
+ logger(logging.WARNING, 'Failed to fetch the scitags markings', exc_info=True)
153
+
154
+ if had_exception:
155
+ # Retry quicker after fetch errors
156
+ _SCITAGS_NEXT_REFRESH = min(_SCITAGS_NEXT_REFRESH, now + datetime.timedelta(minutes=5))
157
+ else:
158
+ _SCITAGS_EXP_ID = exp_id
159
+ _SCITAGS_ACTIVITY_IDS = activity_ids
160
+
161
+ return _SCITAGS_EXP_ID, _SCITAGS_ACTIVITY_IDS
162
+
163
+
164
+ def _pick_cert_file(vo: Optional[str]) -> Optional[str]:
165
+ cert = None
166
+ if vo:
167
+ vo_cert = config_get('vo_certs', vo, False, None)
168
+ if vo_cert:
169
+ cert = vo_cert
170
+ elif VO_CERTS_PATH:
171
+ vo_cert = pathlib.Path(VO_CERTS_PATH) / vo
172
+ if vo_cert.exists():
173
+ cert = str(vo_cert)
174
+ if not cert:
175
+ usercert = config_get('conveyor', 'usercert', False, None)
176
+ if usercert:
177
+ cert = usercert
178
+ return cert
179
+
180
+
181
+ def _configured_source_strategy(activity: str, logger: "LoggerFunction") -> str:
182
+ """
183
+ Retrieve from the configuration the source selection strategy for the given activity
184
+ """
185
+ try:
186
+ default_source_strategy = config_get(section='conveyor', option='default-source-strategy')
187
+ except (NoOptionError, NoSectionError, RuntimeError):
188
+ default_source_strategy = 'orderly'
189
+
190
+ try:
191
+ activity_source_strategy = config_get(section='conveyor', option='activity-source-strategy')
192
+ activity_source_strategy = loads(activity_source_strategy)
193
+ except (NoOptionError, NoSectionError, RuntimeError):
194
+ activity_source_strategy = {}
195
+ except ValueError:
196
+ logger(logging.WARNING, 'activity_source_strategy not properly defined')
197
+ activity_source_strategy = {}
198
+
199
+ return activity_source_strategy.get(str(activity), default_source_strategy)
200
+
201
+
202
+ def _available_checksums(
203
+ transfer: "DirectTransfer",
204
+ ) -> tuple[set[str], set[str]]:
205
+ """
206
+ Get checksums which can be used for file validation on the source and the destination RSE
207
+ """
208
+ src_attributes = transfer.src.rse.attributes
209
+ if src_attributes.get(RseAttr.VERIFY_CHECKSUM, True):
210
+ src_checksums = set(get_rse_supported_checksums_from_attributes(src_attributes))
211
+ else:
212
+ src_checksums = set()
213
+
214
+ dst_attributes = transfer.dst.rse.attributes
215
+ if dst_attributes.get(RseAttr.VERIFY_CHECKSUM, True):
216
+ dst_checksums = set(get_rse_supported_checksums_from_attributes(dst_attributes))
217
+ else:
218
+ dst_checksums = set()
219
+
220
+ return src_checksums, dst_checksums
221
+
222
+
223
+ def _hop_checksum_validation_strategy(
224
+ transfer: "DirectTransfer",
225
+ logger: "LoggerFunction",
226
+ ) -> tuple[str, set[str]]:
227
+ """
228
+ Compute the checksum validation strategy (none, source, destination or both) depending
229
+ on available source and destination checksums for a single hop transfer
230
+ """
231
+ src_checksums, dst_checksums = _available_checksums(transfer)
232
+ intersection = src_checksums.intersection(dst_checksums)
233
+
234
+ if intersection:
235
+ strategy, possible_checksums = 'both', intersection
236
+ elif dst_checksums:
237
+ # The prioritization of destination over source here is desired, not random
238
+ logger(logging.INFO, f'No common checksum method for {transfer}. Verifying destination only.')
239
+ strategy, possible_checksums = 'target', dst_checksums
240
+ elif src_checksums:
241
+ logger(logging.INFO, f'No common checksum method for {transfer}. Verifying source only.')
242
+ strategy, possible_checksums = 'source', src_checksums
243
+ else:
244
+ logger(logging.INFO, f'No common checksum method for {transfer}. Not verifying source nor destination.')
245
+ strategy, possible_checksums = 'none', set()
246
+ return strategy, possible_checksums
247
+
248
+
249
+ def _path_checksum_validation_strategy(
250
+ transfer_path: "list[DirectTransfer]",
251
+ logger: "LoggerFunction",
252
+ ) -> str:
253
+ """
254
+ Compute the checksum validation strategy for the whole transfer path.
255
+ """
256
+
257
+ path_strategy = 'both'
258
+ for transfer_hop in transfer_path:
259
+ hop_strategy, _ = _hop_checksum_validation_strategy(transfer_hop, logger)
260
+
261
+ path_strategy = PATH_CHECKSUM_VALIDATION_STRATEGY.get((path_strategy, hop_strategy), 'none')
262
+
263
+ return path_strategy
264
+
265
+
266
+ def _pick_fts_checksum(
267
+ transfer: "DirectTransfer",
268
+ path_strategy: "str",
269
+ ) -> Optional[str]:
270
+ """
271
+ Pick the checksum to use for validating file integrity on this particular transfer hop.
272
+ This function will only work correctly for values of 'path_strategy' which are
273
+ valid for the englobing multi-hop transfer path.
274
+
275
+ Returns the checksum as a string in the format expected by the FTS bulks submission API.
276
+ """
277
+ src_checksums, dst_checksums = _available_checksums(transfer)
278
+
279
+ if path_strategy == 'both':
280
+ possible_checksums = src_checksums.intersection(dst_checksums)
281
+ elif path_strategy == 'target':
282
+ possible_checksums = dst_checksums
283
+ elif path_strategy == 'source':
284
+ possible_checksums = src_checksums
285
+ else:
286
+ possible_checksums = set()
287
+
288
+ checksum_to_use = None
289
+ for checksum_name in possible_checksums:
290
+ checksum_value = getattr(transfer.rws, checksum_name, '')
291
+ if not checksum_value:
292
+ continue
293
+
294
+ checksum_to_use = '%s:%s' % (checksum_name.upper(), checksum_value)
295
+ if checksum_name == PREFERRED_CHECKSUM:
296
+ break
297
+
298
+ return checksum_to_use
299
+
300
+
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]:
322
+ """
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.
326
+ """
327
+
328
+ # The last hop is the main request (the one which triggered the whole transfer),
329
+ # so most attributes will come from it
330
+ last_hop = transfer_path[-1]
331
+ first_hop = transfer_path[0]
332
+
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
+
339
+ if first_hop.src.rse.is_tape_or_staging_required():
340
+ # Activate bring_online if it was requested by first hop
341
+ # We don't allow multihop via a tape, so bring_online should not be set on any other hop
342
+ bring_online_local = bring_online
343
+
344
+ if last_hop.dst.rse.is_tape():
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))
373
+
374
+ # Get dest space token
375
+ dest_protocol = last_hop.protocol_factory.protocol(last_hop.dst.rse, last_hop.dst.scheme, last_hop.operation_dest)
376
+ dest_spacetoken = None
377
+ if dest_protocol.attributes and 'extended_attributes' in dest_protocol.attributes and \
378
+ dest_protocol.attributes['extended_attributes'] and 'space_token' in dest_protocol.attributes['extended_attributes']:
379
+ dest_spacetoken = dest_protocol.attributes['extended_attributes']['space_token']
380
+
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)
383
+
384
+ job_params = {'account': last_hop.rws.account,
385
+ 'verify_checksum': _path_checksum_validation_strategy(transfer_path, logger=logger),
386
+ 'copy_pin_lifetime': last_hop.rws.attributes.get('lifetime', default_lifetime),
387
+ 'bring_online': bring_online_local,
388
+ 'job_metadata': {
389
+ 'issuer': 'rucio',
390
+ 'multi_sources': False,
391
+ 'overwrite_when_only_on_disk': overwrite_when_only_on_disk,
392
+ },
393
+ 'overwrite': overwrite,
394
+ 'overwrite_when_only_on_disk': overwrite_when_only_on_disk,
395
+ 'priority': last_hop.rws.priority}
396
+
397
+ if len(transfer_path) > 1:
398
+ job_params['multihop'] = True
399
+ job_params['job_metadata']['multihop'] = True
400
+ elif len(last_hop.sources) > 1:
401
+ job_params['job_metadata']['multi_sources'] = True
402
+ if strict_copy:
403
+ job_params['strict_copy'] = strict_copy
404
+ if dest_spacetoken:
405
+ job_params['spacetoken'] = dest_spacetoken
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)):
408
+ job_params['ipv4'] = True
409
+ job_params['ipv6'] = False
410
+
411
+ # assume s3alternate True (path-style URL S3 RSEs)
412
+ job_params['s3alternate'] = True
413
+ src_rse_s3_url_style = first_hop.src.rse.attributes.get(RseAttr.S3_URL_STYLE, None)
414
+ if src_rse_s3_url_style == "host":
415
+ job_params['s3alternate'] = False
416
+ dst_rse_s3_url_style = last_hop.dst.rse.attributes.get(RseAttr.S3_URL_STYLE, None)
417
+ if dst_rse_s3_url_style == "host":
418
+ job_params['s3alternate'] = False
419
+
420
+ if archive_timeout and last_hop.dst.rse.is_tape():
421
+ try:
422
+ archive_timeout = int(archive_timeout)
423
+ if archive_timeout_override is None:
424
+ job_params['archive_timeout'] = archive_timeout
425
+ elif archive_timeout_override != 0:
426
+ job_params['archive_timeout'] = archive_timeout_override
427
+ # FTS only supports dst_file metadata if archive_timeout is set
428
+ job_params['dst_file_report'] = True
429
+ logger(logging.DEBUG, 'Added archive timeout to transfer.')
430
+ except ValueError:
431
+ logger(logging.WARNING, 'Could not set archive_timeout for %s. Must be integer.', last_hop)
432
+ pass
433
+ if max_time_in_queue:
434
+ if last_hop.rws.activity in max_time_in_queue:
435
+ job_params['max_time_in_queue'] = max_time_in_queue[last_hop.rws.activity]
436
+ elif 'default' in max_time_in_queue:
437
+ job_params['max_time_in_queue'] = max_time_in_queue['default']
438
+
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
+
456
+ if not job_params['overwrite'] and overwrite_hop:
457
+ job_params['overwrite_hop'] = overwrite_hop
458
+
459
+ logger(logging.DEBUG, 'Job parameters are : %s' % (job_params))
460
+ return job_params
461
+
462
+
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]]:
473
+ """
474
+ Group transfers in bulk based on certain criteria
475
+
476
+ :param transfer_paths: List of transfer paths to group. Each path is a list of single-hop transfers.
477
+ :param policy: Policy to use to group.
478
+ :param group_bulk: Bulk sizes.
479
+ :param source_strategy: Strategy to group sources
480
+ :param max_time_in_queue: Maximum time in queue
481
+ :param archive_timeout_override: Override the archive_timeout parameter for any transfers with it set (0 to unset)
482
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
483
+ :return: List of grouped transfers.
484
+ """
485
+
486
+ grouped_transfers = {}
487
+ fts_jobs = []
488
+
489
+ for transfer_path in transfer_paths:
490
+ job_params = build_job_params(
491
+ transfer_path=transfer_path,
492
+ bring_online=bring_online,
493
+ default_lifetime=default_lifetime,
494
+ archive_timeout_override=archive_timeout_override,
495
+ max_time_in_queue=max_time_in_queue,
496
+ logger=logger
497
+ )
498
+ logger(logging.DEBUG, 'bulk_group_transfers: Job parameters are: %s' % (job_params))
499
+ if job_params['job_metadata'].get('multi_sources') or job_params['job_metadata'].get('multihop'):
500
+ # for multi-hop and multi-source transfers, no bulk submission.
501
+ fts_jobs.append({'transfers': transfer_path[0:group_bulk], 'job_params': job_params})
502
+ else:
503
+ # it's a single-hop, single-source, transfer. Hence, a candidate for bulk submission.
504
+ transfer = transfer_path[0]
505
+
506
+ # we cannot group transfers together if their job_key differ
507
+ job_key = '%s,%s,%s,%s,%s,%s,%s,%s' % (
508
+ job_params['verify_checksum'],
509
+ job_params.get('spacetoken', ''),
510
+ job_params['copy_pin_lifetime'],
511
+ job_params['bring_online'],
512
+ job_params['job_metadata'],
513
+ job_params['overwrite'],
514
+ job_params['priority'],
515
+ job_params.get('max_time_in_queue', '')
516
+ )
517
+
518
+ # Additionally, we don't want to group transfers together if their policy_key differ
519
+ policy_key = ''
520
+ if policy == 'rule':
521
+ policy_key = '%s' % transfer.rws.rule_id
522
+ if policy == 'dest':
523
+ policy_key = '%s' % transfer.dst.rse.name
524
+ if policy == 'src_dest':
525
+ policy_key = '%s,%s' % (transfer.src.rse.name, transfer.dst.rse.name)
526
+ if policy == 'rule_src_dest':
527
+ policy_key = '%s,%s,%s' % (transfer.rws.rule_id, transfer.src.rse.name, transfer.dst.rse.name)
528
+ if policy == 'activity_dest':
529
+ policy_key = '%s %s' % (transfer.rws.activity, transfer.dst.rse.name)
530
+ policy_key = "_".join(policy_key.split(' '))
531
+ if policy == 'activity_src_dest':
532
+ policy_key = '%s %s %s' % (transfer.rws.activity, transfer.src.rse.name, transfer.dst.rse.name)
533
+ policy_key = "_".join(policy_key.split(' '))
534
+ # maybe here we need to hash the key if it's too long
535
+
536
+ group_key = "%s_%s" % (job_key, policy_key)
537
+ if group_key not in grouped_transfers:
538
+ grouped_transfers[group_key] = {'transfers': [], 'job_params': job_params}
539
+ grouped_transfers[group_key]['transfers'].append(transfer)
540
+
541
+ # split transfer groups to have at most group_bulk elements in each one
542
+ for group in grouped_transfers.values():
543
+ job_params = group['job_params']
544
+ logger(logging.DEBUG, 'bulk_group_transfers: grouped_transfers.values(): Job parameters are: %s' % (job_params))
545
+ for transfer_paths in chunks(group['transfers'], group_bulk):
546
+ fts_jobs.append({'transfers': transfer_paths, 'job_params': job_params})
547
+
548
+ return fts_jobs
549
+
550
+
551
+ class Fts3TransferStatusReport(TransferStatusReport):
552
+
553
+ supported_db_fields = [
554
+ 'state',
555
+ 'external_id',
556
+ 'started_at',
557
+ 'transferred_at',
558
+ 'staging_started_at',
559
+ 'staging_finished_at',
560
+ 'source_rse_id',
561
+ 'err_msg',
562
+ 'attributes',
563
+ ]
564
+
565
+ def __init__(self, external_host: str, request_id: str, request: Optional[dict] = None):
566
+ super().__init__(request_id, request=request)
567
+ self.external_host = external_host
568
+
569
+ # Initialized in child class constructors:
570
+ self._transfer_id = None
571
+ self._file_metadata = {}
572
+ self._multi_sources = None
573
+ self._src_url = None
574
+ self._dst_url = None
575
+ # Initialized in child class initialize():
576
+ self._reason = None
577
+ self._src_rse = None
578
+ self._fts_address = self.external_host
579
+ # Supported db fields below:
580
+ self.state = None
581
+ self.external_id = None
582
+ self.started_at = None
583
+ self.transferred_at = None
584
+ self.staging_started_at = None
585
+ self.staging_finished_at = None
586
+ self.source_rse_id = None
587
+ self.err_msg = None
588
+ self.attributes = None
589
+
590
+ def __str__(self):
591
+ return f'Transfer {self._transfer_id} of {self._file_metadata["scope"]}:{self._file_metadata["name"]} ' \
592
+ f'{self._file_metadata["src_rse"]} --({self._file_metadata["request_id"]})-> {self._file_metadata["dst_rse"]}'
593
+
594
+ def initialize(self, session: "Session", logger: "LoggerFunction" = logging.log) -> None:
595
+ raise NotImplementedError(f"{self.__class__.__name__} is abstract and shouldn't be used directly")
596
+
597
+ def get_monitor_msg_fields(self, session: "Session", logger: "LoggerFunction" = logging.log) -> dict[str, Any]:
598
+ self.ensure_initialized(session, logger)
599
+ fields = {
600
+ 'transfer_link': self._transfer_link(),
601
+ 'reason': self._reason,
602
+ 'src-type': self._file_metadata.get('src_type'),
603
+ 'src-rse': self._src_rse,
604
+ 'src-url': self._src_url,
605
+ 'dst-type': self._file_metadata.get('src_type'),
606
+ 'dst-rse': self._file_metadata.get('dst_rse'),
607
+ 'dst-url': self._dst_url,
608
+ 'started_at': self.started_at,
609
+ 'transferred_at': self.transferred_at,
610
+ }
611
+ return fields
612
+
613
+ def _transfer_link(self) -> str:
614
+ return '%s/fts3/ftsmon/#/job/%s' % (self._fts_address.replace('8446', '8449'), self._transfer_id)
615
+
616
+ def _find_attribute_updates(self, request: dict, new_state: RequestState, reason: str, overwrite_corrupted_files: Optional[bool] = None) -> Optional[dict[str, Any]]:
617
+ attributes = None
618
+ if new_state == RequestState.FAILED and 'Destination file exists and overwrite is not enabled' in (reason or ''):
619
+ dst_file = self._file_metadata.get('dst_file', {})
620
+ if self._dst_file_set_and_file_corrupted(request, dst_file):
621
+ if overwrite_corrupted_files:
622
+ attributes = request['attributes']
623
+ attributes['overwrite'] = True
624
+ return attributes
625
+
626
+ def _find_used_source_rse(self, session: "Session", logger: "LoggerFunction") -> tuple[Optional[str], Optional[str]]:
627
+ """
628
+ For multi-source transfers, FTS has a choice between multiple sources.
629
+ Find which of the possible sources FTS actually used for the transfer.
630
+ """
631
+ meta_rse_name = self._file_metadata.get('src_rse', None)
632
+ meta_rse_id = self._file_metadata.get('src_rse_id', None)
633
+ request_id = self._file_metadata.get('request_id', None)
634
+
635
+ if self._multi_sources and self._src_url:
636
+ rse_name, rse_id = get_source_rse(request_id, self._src_url, session=session)
637
+ if rse_name and rse_name != meta_rse_name:
638
+ logger(logging.DEBUG, 'Correct RSE: %s for source surl: %s' % (rse_name, self._src_url))
639
+ return rse_name, rse_id
640
+
641
+ return meta_rse_name, meta_rse_id
642
+
643
+ @staticmethod
644
+ def _dst_file_set_and_file_corrupted(request: dict, dst_file: dict) -> bool:
645
+ """
646
+ Returns True if the `dst_file` dict returned by fts was filled and its content allows to
647
+ affirm that the file is corrupted.
648
+ """
649
+ if (request and dst_file and (
650
+ dst_file.get('file_size') is not None and dst_file['file_size'] != request.get('bytes')
651
+ or dst_file.get('checksum_type', '').lower() == 'adler32' and dst_file.get('checksum_value') != request.get('adler32')
652
+ or dst_file.get('checksum_type', '').lower() == 'md5' and dst_file.get('checksum_value') != request.get('md5'))):
653
+ return True
654
+ return False
655
+
656
+ @staticmethod
657
+ def _dst_file_set_and_file_correct(request: dict, dst_file: dict) -> bool:
658
+ """
659
+ Returns True if the `dst_file` dict returned by fts was filled and its content allows to
660
+ affirm that the file is correct.
661
+ """
662
+ if (request and dst_file
663
+ and dst_file.get('file_size')
664
+ and dst_file.get('file_size') == request.get('bytes')
665
+ and (dst_file.get('checksum_type', '').lower() == 'adler32' and dst_file.get('checksum_value') == request.get('adler32')
666
+ or dst_file.get('checksum_type', '').lower() == 'md5' and dst_file.get('checksum_value') == request.get('md5'))):
667
+ return True
668
+ return False
669
+
670
+ @classmethod
671
+ def _is_recoverable_fts_overwrite_error(cls, request: dict[str, Any], reason: str,
672
+ file_metadata: dict[str, Any]) -> bool:
673
+ """
674
+ Verify the special case when FTS cannot copy a file because destination exists and overwrite is disabled,
675
+ but the destination file is actually correct.
676
+
677
+ This can happen when some transitory error happened during a previous submission attempt.
678
+ Hence, the transfer is correctly executed by FTS, but rucio doesn't know about it.
679
+
680
+ Returns true when the request must be marked as successful even if it was reported failed by FTS.
681
+ """
682
+ if not request or not file_metadata:
683
+ return False
684
+ dst_file = file_metadata.get('dst_file', {})
685
+ dst_type = file_metadata.get('dst_type', None)
686
+ METRICS.counter('overwrite.check.{rsetype}.{rse}').labels(rse=file_metadata["dst_rse"], rsetype=dst_type).inc()
687
+
688
+ if 'Destination file exists and overwrite is not enabled' in (reason or ''):
689
+ if cls._dst_file_set_and_file_correct(request, dst_file):
690
+ if dst_type == 'DISK' or dst_file.get('file_on_tape'):
691
+ METRICS.counter('overwrite.ok.{rsetype}.{rse}').labels(rse=file_metadata["dst_rse"], rsetype=dst_type).inc()
692
+ return True
693
+
694
+ METRICS.counter('overwrite.fail.{rsetype}.{rse}').labels(rse=file_metadata["dst_rse"], rsetype=dst_type).inc()
695
+ return False
696
+
697
+
698
+ class FTS3CompletionMessageTransferStatusReport(Fts3TransferStatusReport):
699
+ """
700
+ Parses FTS Completion messages received via the message queue
701
+ """
702
+ def __init__(self, external_host: str, request_id: str, fts_message: dict[str, Any]):
703
+ super().__init__(external_host=external_host, request_id=request_id)
704
+
705
+ self.fts_message = fts_message
706
+
707
+ transfer_id = fts_message.get('tr_id')
708
+ if transfer_id is not None:
709
+ self._transfer_id = transfer_id.split("__")[-1]
710
+
711
+ self._file_metadata = fts_message['file_metadata']
712
+ self._multi_sources = str(fts_message.get('job_metadata', {}).get('multi_sources', '')).lower() == 'true'
713
+ self._src_url = fts_message.get('src_url', None)
714
+ self._dst_url = fts_message.get('dst_url', None)
715
+
716
+ def initialize(self, session: "Session", logger: "LoggerFunction" = logging.log) -> None:
717
+
718
+ fts_message = self.fts_message
719
+ request_id = self.request_id
720
+
721
+ reason = fts_message.get('t__error_message', None)
722
+ # job_state = fts_message.get('t_final_transfer_state', None)
723
+ new_state = None
724
+ if str(fts_message['t_final_transfer_state']) == FTS_COMPLETE_STATE.OK and not fts_message.get('is_archiving'): # pylint:disable=no-member
725
+ new_state = RequestState.DONE
726
+ elif str(fts_message['t_final_transfer_state']) == FTS_COMPLETE_STATE.ERROR:
727
+ request = self.request(session)
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
733
+
734
+ transfer_id = self._transfer_id
735
+ if new_state:
736
+ request = self.request(session)
737
+ if not request:
738
+ logger(logging.WARNING, '%s: no request with this id in the database. Skipping. external_id: %s (%s). new_state: %s', request_id, transfer_id, self.external_host, new_state)
739
+ return
740
+ if request and request['external_id'] == transfer_id and request['state'] != new_state:
741
+ src_rse_name, src_rse_id = self._find_used_source_rse(session, logger)
742
+
743
+ self._reason = reason
744
+ self._src_rse = src_rse_name
745
+ self._fts_address = request['external_host'] or self._fts_address
746
+
747
+ self.state = new_state
748
+ self.external_id = transfer_id
749
+ self.started_at = datetime.datetime.utcfromtimestamp(float(fts_message.get('tr_timestamp_start', 0)) / 1000)
750
+ self.transferred_at = datetime.datetime.utcfromtimestamp(float(fts_message.get('tr_timestamp_complete', 0)) / 1000)
751
+ self.staging_started_at = None
752
+ self.staging_finished_at = None
753
+ self.source_rse_id = src_rse_id
754
+ self.err_msg = get_transfer_error(self.state, reason)
755
+ if self.err_msg and self._file_metadata.get('src_type') == "TAPE":
756
+ self.err_msg = '[TAPE SOURCE] ' + self.err_msg
757
+ self.attributes = self._find_attribute_updates(
758
+ request=request,
759
+ new_state=new_state,
760
+ reason=reason,
761
+ overwrite_corrupted_files=config_get_bool('transfers', 'overwrite_corrupted_files', default=False, session=session),
762
+ )
763
+ elif request['external_id'] != transfer_id:
764
+ logger(logging.WARNING, "Response %s with transfer id %s is different from the request transfer id %s, will not update" % (request_id, transfer_id, request['external_id']))
765
+ else:
766
+ logger(logging.DEBUG, "Request %s is already in %s state, will not update" % (request_id, new_state))
767
+ else:
768
+ logger(logging.DEBUG, "No state change computed for %s. Skipping request update." % request_id)
769
+
770
+
771
+ class FTS3ApiTransferStatusReport(Fts3TransferStatusReport):
772
+ """
773
+ Parses FTS api response
774
+ """
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):
776
+ super().__init__(external_host=external_host, request_id=request_id, request=request)
777
+
778
+ self.job_response = job_response
779
+ self.file_response = file_response
780
+
781
+ self._transfer_id = job_response.get('job_id')
782
+
783
+ self._file_metadata = file_response['file_metadata']
784
+ self._multi_sources = str(job_response['job_metadata'].get('multi_sources', '')).lower() == 'true'
785
+ self._src_url = file_response.get('source_surl', None)
786
+ self._dst_url = file_response.get('dest_surl', None)
787
+ self.logger = logging.log
788
+
789
+ def initialize(self, session: "Session", logger=logging.log) -> None:
790
+
791
+ self.logger = logger
792
+ job_response = self.job_response
793
+ file_response = self.file_response
794
+ request_id = self.request_id
795
+
796
+ file_state = file_response['file_state']
797
+ reason = file_response.get('reason', None)
798
+
799
+ new_state = None
800
+ job_state = job_response.get('job_state', None)
801
+ multi_hop = job_response.get('job_type') == FTS_JOB_TYPE.MULTI_HOP
802
+ job_state_is_final = job_state in FINAL_FTS_JOB_STATES
803
+ file_state_is_final = file_state in FINAL_FTS_FILE_STATES
804
+ if file_state_is_final:
805
+ if file_state == FTS_STATE.FINISHED:
806
+ new_state = RequestState.DONE
807
+ elif file_state == FTS_STATE.FAILED and job_state_is_final or \
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
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
815
+ elif job_state_is_final and file_state == FTS_STATE.CANCELED:
816
+ new_state = RequestState.FAILED
817
+ elif job_state_is_final and file_state == FTS_STATE.NOT_USED:
818
+ if job_state == FTS_STATE.FINISHED:
819
+ # it is a multi-source transfer. This source wasn't used, but another one was successful
820
+ new_state = RequestState.DONE
821
+ else:
822
+ # failed multi-source or multi-hop (you cannot have unused sources in a successful multi-hop)
823
+ new_state = RequestState.FAILED
824
+ if not reason and multi_hop:
825
+ reason = 'Unused hop in multi-hop'
826
+
827
+ transfer_id = self._transfer_id
828
+ if new_state:
829
+ request = self.request(session)
830
+ if not request:
831
+ logger(logging.WARNING, '%s: no request with this id in the database. Skipping. external_id: %s (%s). new_state: %s', request_id, transfer_id, self.external_host, new_state)
832
+ return
833
+ if request['external_id'] == transfer_id and request['state'] != new_state:
834
+ src_rse_name, src_rse_id = self._find_used_source_rse(session, logger)
835
+
836
+ self._reason = reason
837
+ self._src_rse = src_rse_name
838
+
839
+ self.state = new_state
840
+ self.external_id = transfer_id
841
+ self.started_at = datetime.datetime.strptime(file_response['start_time'], '%Y-%m-%dT%H:%M:%S') if file_response['start_time'] else None
842
+ self.transferred_at = datetime.datetime.strptime(file_response['finish_time'], '%Y-%m-%dT%H:%M:%S') if file_response['finish_time'] else None
843
+ self.staging_started_at = datetime.datetime.strptime(file_response['staging_start'], '%Y-%m-%dT%H:%M:%S') if file_response['staging_start'] else None
844
+ self.staging_finished_at = datetime.datetime.strptime(file_response['staging_finished'], '%Y-%m-%dT%H:%M:%S') if file_response['staging_finished'] else None
845
+ self.source_rse_id = src_rse_id
846
+ self.err_msg = get_transfer_error(self.state, reason)
847
+ if self.err_msg and self._file_metadata.get('src_type') == "TAPE":
848
+ self.err_msg = '[TAPE SOURCE] ' + self.err_msg
849
+ self.attributes = self._find_attribute_updates(
850
+ request=request,
851
+ new_state=new_state,
852
+ reason=reason,
853
+ overwrite_corrupted_files=config_get_bool('transfers', 'overwrite_corrupted_files', default=False, session=session),
854
+ )
855
+ elif request['external_id'] != transfer_id:
856
+ logger(logging.WARNING, "Response %s with transfer id %s is different from the request transfer id %s, will not update" % (request_id, transfer_id, request['external_id']))
857
+ else:
858
+ logger(logging.DEBUG, "Request %s is already in %s state, will not update" % (request_id, new_state))
859
+
860
+
861
+ class FTS3Transfertool(Transfertool):
862
+ """
863
+ FTS3 implementation of a Rucio transfertool
864
+ """
865
+
866
+ external_name = 'fts3'
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
+ ):
884
+ """
885
+ Initializes the transfertool
886
+
887
+ :param external_host: The external host where the transfertool API is running
888
+ """
889
+ super().__init__(external_host, logger)
890
+
891
+ self.group_policy = group_policy
892
+ self.group_bulk = group_bulk
893
+ self.source_strategy = source_strategy
894
+ self.max_time_in_queue = max_time_in_queue or {}
895
+ self.bring_online = bring_online
896
+ self.default_lifetime = default_lifetime
897
+ self.archive_timeout_override = archive_timeout_override
898
+
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
+
906
+ self.token = None
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)
916
+
917
+ self.deterministic_id = config_get_bool('conveyor', 'use_deterministic_id', False, False)
918
+ self.headers = {'Content-Type': 'application/json'}
919
+ if self.external_host.startswith('https://'):
920
+ if self.token:
921
+ self.cert = None
922
+ self.verify = False
923
+ self.headers['Authorization'] = 'Bearer ' + self.token
924
+ else:
925
+ cert = _pick_cert_file(vo=vo)
926
+ self.cert = (cert, cert)
927
+ self.verify = False
928
+ else:
929
+ self.cert = None
930
+ self.verify = True # True is the default setting of a requests.* method
931
+
932
+ self.scitags_exp_id, self.scitags_activity_ids = _scitags_ids(logger=logger)
933
+
934
+ @classmethod
935
+ def _pick_fts_servers(cls, source_rse: "RseData", dest_rse: "RseData") -> Optional[list[str]]:
936
+ """
937
+ Pick fts servers to use for submission between the two given rse
938
+ """
939
+ source_servers = source_rse.attributes.get(RseAttr.FTS, None)
940
+ dest_servers = dest_rse.attributes.get(RseAttr.FTS, None)
941
+ if source_servers is None or dest_servers is None:
942
+ return None
943
+
944
+ servers_to_use = dest_servers
945
+ if source_rse.attributes.get(RseAttr.SIGN_URL, None) == 'gcs':
946
+ servers_to_use = source_servers
947
+
948
+ return servers_to_use.split(',')
949
+
950
+ @classmethod
951
+ def can_perform_transfer(cls, source_rse: "RseData", dest_rse: "RseData") -> bool:
952
+ if cls._pick_fts_servers(source_rse, dest_rse):
953
+ return True
954
+ return False
955
+
956
+ @classmethod
957
+ def submission_builder_for_path(cls, transfer_path: "list[DirectTransfer]", logger: "LoggerFunction" = logging.log):
958
+ vo = None
959
+ if config_get_bool('common', 'multi_vo', False, None):
960
+ vo = transfer_path[-1].rws.scope.vo
961
+
962
+ sub_path = []
963
+ fts_hosts = []
964
+ for hop in transfer_path:
965
+ hosts = cls._pick_fts_servers(hop.src.rse, hop.dst.rse)
966
+ if hosts:
967
+ fts_hosts = hosts
968
+ sub_path.append(hop)
969
+ else:
970
+ break
971
+
972
+ if len(sub_path) < len(transfer_path):
973
+ logger(logging.INFO, 'FTS3Transfertool can only submit {} hops from {}'.format(len(sub_path), [str(hop) for hop in transfer_path]))
974
+
975
+ if sub_path:
976
+ oidc_support = False
977
+ if all(_use_tokens(t) for t in sub_path):
978
+ logger(logging.DEBUG, 'OAuth2/OIDC available for transfer {}'.format([str(hop) for hop in sub_path]))
979
+ oidc_support = True
980
+ return sub_path, TransferToolBuilder(cls, external_host=fts_hosts[0], oidc_support=oidc_support, vo=vo)
981
+ else:
982
+ return [], None
983
+
984
+ def group_into_submit_jobs(self, transfer_paths: "list[list[DirectTransfer]]") -> list[dict[str, Any]]:
985
+ jobs = bulk_group_transfers(
986
+ transfer_paths,
987
+ policy=self.group_policy,
988
+ group_bulk=self.group_bulk,
989
+ source_strategy=self.source_strategy,
990
+ max_time_in_queue=self.max_time_in_queue,
991
+ bring_online=self.bring_online,
992
+ default_lifetime=self.default_lifetime,
993
+ archive_timeout_override=self.archive_timeout_override,
994
+ logger=self.logger,
995
+ )
996
+ return jobs
997
+
998
+ def _file_from_transfer(self, transfer: "DirectTransfer", job_params: dict[str, str]) -> dict[str, Any]:
999
+ rws = transfer.rws
1000
+ checksum_to_use = _pick_fts_checksum(transfer, path_strategy=job_params['verify_checksum'])
1001
+ t_file = {
1002
+ 'sources': [transfer.source_url(s) for s in transfer.sources],
1003
+ 'destinations': [transfer.dest_url],
1004
+ 'metadata': {
1005
+ 'request_id': rws.request_id,
1006
+ 'scope': rws.scope,
1007
+ 'name': rws.name,
1008
+ 'activity': rws.activity,
1009
+ 'request_type': rws.request_type,
1010
+ 'src_type': "TAPE" if transfer.src.rse.is_tape_or_staging_required() else 'DISK',
1011
+ 'dst_type': "TAPE" if transfer.dst.rse.is_tape() else 'DISK',
1012
+ 'src_rse': transfer.src.rse.name,
1013
+ 'dst_rse': transfer.dst.rse.name,
1014
+ 'src_rse_id': transfer.src.rse.id,
1015
+ 'dest_rse_id': transfer.dst.rse.id,
1016
+ 'filesize': rws.byte_count,
1017
+ 'md5': rws.md5,
1018
+ 'adler32': rws.adler32
1019
+ },
1020
+ 'filesize': rws.byte_count,
1021
+ 'checksum': checksum_to_use,
1022
+ 'selection_strategy': self.source_strategy if self.source_strategy else _configured_source_strategy(transfer.rws.activity, logger=self.logger),
1023
+ 'activity': rws.activity
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
+
1039
+ if isinstance(self.scitags_exp_id, int):
1040
+ activity_id = self.scitags_activity_ids.get(rws.activity)
1041
+ if isinstance(activity_id, int):
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
+
1048
+ return t_file
1049
+
1050
+ def submit(self, transfers: "Sequence[DirectTransfer]", job_params: dict[str, str], timeout: Optional[int] = None) -> str:
1051
+ """
1052
+ Submit transfers to FTS3 via JSON.
1053
+
1054
+ :param files: List of dictionaries describing the file transfers.
1055
+ :param job_params: Dictionary containing key/value pairs, for all transfers.
1056
+ :param timeout: Timeout in seconds.
1057
+ :returns: FTS transfer identifier.
1058
+ """
1059
+ files = []
1060
+ for transfer in transfers:
1061
+ files.append(self._file_from_transfer(transfer, job_params))
1062
+
1063
+ # FTS3 expects 'davs' as the scheme identifier instead of https
1064
+ for transfer_file in files:
1065
+ if not transfer_file['sources'] or transfer_file['sources'] == []:
1066
+ raise Exception('No sources defined')
1067
+
1068
+ # TODO: remove the following logic in rucio 1.31
1069
+ if REWRITE_HTTPS_TO_DAVS:
1070
+ new_src_urls = []
1071
+ new_dst_urls = []
1072
+ for url in transfer_file['sources']:
1073
+ if url.startswith('https'):
1074
+ new_src_urls.append(':'.join(['davs'] + url.split(':')[1:]))
1075
+ else:
1076
+ new_src_urls.append(url)
1077
+ for url in transfer_file['destinations']:
1078
+ if url.startswith('https'):
1079
+ new_dst_urls.append(':'.join(['davs'] + url.split(':')[1:]))
1080
+ else:
1081
+ new_dst_urls.append(url)
1082
+
1083
+ transfer_file['sources'] = new_src_urls
1084
+ transfer_file['destinations'] = new_dst_urls
1085
+
1086
+ transfer_id = None
1087
+ expected_transfer_id = None
1088
+ if self.deterministic_id:
1089
+ job_params = job_params.copy()
1090
+ job_params["id_generator"] = "deterministic"
1091
+ job_params["sid"] = files[0]['metadata']['request_id']
1092
+ expected_transfer_id = self.__get_deterministic_id(job_params["sid"])
1093
+ self.logger(logging.DEBUG, "Submit bulk transfers in deterministic mode, sid %s, expected transfer id: %s", job_params["sid"], expected_transfer_id)
1094
+
1095
+ # bulk submission
1096
+ params_dict = {'files': files, 'params': job_params}
1097
+ params_str = json.dumps(params_dict, cls=APIEncoder)
1098
+
1099
+ post_result = None
1100
+ stopwatch = Stopwatch()
1101
+ try:
1102
+ post_result = requests.post('%s/jobs' % self.external_host,
1103
+ verify=self.verify,
1104
+ cert=self.cert,
1105
+ data=params_str,
1106
+ headers=self.headers,
1107
+ timeout=timeout)
1108
+ labels = {'host': self.__extract_host(self.external_host)}
1109
+ METRICS.timer('submit_transfer.{host}').labels(**labels).observe(stopwatch.elapsed / (len(files) or 1))
1110
+ except ReadTimeout as error:
1111
+ raise TransferToolTimeout(error)
1112
+ except json.JSONDecodeError as error:
1113
+ raise TransferToolWrongAnswer(error)
1114
+ except Exception as error:
1115
+ self.logger(logging.WARNING, 'Could not submit transfer to %s - %s' % (self.external_host, str(error)))
1116
+
1117
+ if post_result and post_result.status_code == 200:
1118
+ SUBMISSION_COUNTER.labels(state='success', host=self.__extract_host(self.external_host)).inc(len(files))
1119
+ transfer_id = str(post_result.json()['job_id'])
1120
+ elif post_result and post_result.status_code == 409:
1121
+ SUBMISSION_COUNTER.labels(state='failure', host=self.__extract_host(self.external_host)).inc(len(files))
1122
+ raise DuplicateFileTransferSubmission()
1123
+ else:
1124
+ if expected_transfer_id:
1125
+ transfer_id = expected_transfer_id
1126
+ self.logger(logging.WARNING, "Failed to submit transfer to %s, will use expected transfer id %s, error: %s", self.external_host, transfer_id, post_result.text if post_result is not None else post_result)
1127
+ else:
1128
+ self.logger(logging.WARNING, "Failed to submit transfer to %s, error: %s", self.external_host, post_result.text if post_result is not None else post_result)
1129
+ SUBMISSION_COUNTER.labels(state='failure', host=self.__extract_host(self.external_host)).inc(len(files))
1130
+
1131
+ if not transfer_id:
1132
+ raise TransferToolWrongAnswer('No transfer id returned by %s' % self.external_host)
1133
+ METRICS.timer('submit_transfers_fts3').observe(stopwatch.elapsed / (len(transfers) or 1))
1134
+ return transfer_id
1135
+
1136
+ def cancel(self, transfer_ids: "Sequence[str]", timeout: Optional[int] = None) -> dict[str, Any]:
1137
+ """
1138
+ Cancel transfers that have been submitted to FTS3.
1139
+
1140
+ :param transfer_ids: FTS transfer identifiers as list of strings.
1141
+ :param timeout: Timeout in seconds.
1142
+ :returns: True if cancellation was successful.
1143
+ """
1144
+
1145
+ if len(transfer_ids) > 1:
1146
+ raise NotImplementedError('Bulk cancelling not implemented')
1147
+ transfer_id = transfer_ids[0]
1148
+
1149
+ job = None
1150
+
1151
+ job = requests.delete('%s/jobs/%s' % (self.external_host, transfer_id),
1152
+ verify=self.verify,
1153
+ cert=self.cert,
1154
+ headers=self.headers,
1155
+ timeout=timeout)
1156
+
1157
+ if job and job.status_code == 200:
1158
+ CANCEL_COUNTER.labels(state='success', host=self.__extract_host(self.external_host)).inc()
1159
+ return job.json()
1160
+
1161
+ CANCEL_COUNTER.labels(state='failure', host=self.__extract_host(self.external_host)).inc()
1162
+ raise Exception('Could not cancel transfer: %s', job.content)
1163
+
1164
+ def update_priority(self, transfer_id: str, priority: int, timeout: Optional[int] = None) -> dict[str, Any]:
1165
+ """
1166
+ Update the priority of a transfer that has been submitted to FTS via JSON.
1167
+
1168
+ :param transfer_id: FTS transfer identifier as a string.
1169
+ :param priority: FTS job priority as an integer from 1 to 5.
1170
+ :param timeout: Timeout in seconds.
1171
+ :returns: True if update was successful.
1172
+ """
1173
+
1174
+ job = None
1175
+ params_dict = {"params": {"priority": priority}}
1176
+ params_str = json.dumps(params_dict, cls=APIEncoder)
1177
+
1178
+ job = requests.post('%s/jobs/%s' % (self.external_host, transfer_id),
1179
+ verify=self.verify,
1180
+ data=params_str,
1181
+ cert=self.cert,
1182
+ headers=self.headers,
1183
+ timeout=timeout) # TODO set to 3 in conveyor
1184
+
1185
+ if job and job.status_code == 200:
1186
+ UPDATE_PRIORITY_COUNTER.labels(state='success', host=self.__extract_host(self.external_host)).inc()
1187
+ return job.json()
1188
+
1189
+ UPDATE_PRIORITY_COUNTER.labels(state='failure', host=self.__extract_host(self.external_host)).inc()
1190
+ raise Exception('Could not update priority of transfer: %s', job.content)
1191
+
1192
+ def query(self, transfer_ids: "Sequence[str]", details: bool = False, timeout: Optional[int] = None) -> Union[Optional[dict[str, Any]], list[dict[str, Any]]]:
1193
+ """
1194
+ Query the status of a transfer in FTS3 via JSON.
1195
+
1196
+ :param transfer_ids: FTS transfer identifiers as list of strings.
1197
+ :param details: Switch if detailed information should be listed.
1198
+ :param timeout: Timeout in seconds.
1199
+ :returns: Transfer status information as a list of dictionaries.
1200
+ """
1201
+
1202
+ if len(transfer_ids) > 1:
1203
+ raise NotImplementedError('FTS3 transfertool query not bulk ready')
1204
+
1205
+ transfer_id = transfer_ids[0]
1206
+ if details:
1207
+ return self.__query_details(transfer_id=transfer_id)
1208
+
1209
+ job = None
1210
+
1211
+ job = requests.get('%s/jobs/%s' % (self.external_host, transfer_id),
1212
+ verify=self.verify,
1213
+ cert=self.cert,
1214
+ headers=self.headers,
1215
+ timeout=timeout) # TODO Set to 5 in conveyor
1216
+ if job and job.status_code == 200:
1217
+ QUERY_COUNTER.labels(state='success', host=self.__extract_host(self.external_host)).inc()
1218
+ return [job.json()]
1219
+
1220
+ QUERY_COUNTER.labels(state='failure', host=self.__extract_host(self.external_host)).inc()
1221
+ raise Exception('Could not retrieve transfer information: %s', job.content)
1222
+
1223
+ # Public methods, not part of the common interface specification (FTS3 specific)
1224
+
1225
+ def whoami(self) -> dict[str, Any]:
1226
+ """
1227
+ Returns credential information from the FTS3 server.
1228
+
1229
+ :returns: Credentials as stored by the FTS3 server as a dictionary.
1230
+ """
1231
+
1232
+ get_result = None
1233
+
1234
+ get_result = requests.get('%s/whoami' % self.external_host,
1235
+ verify=self.verify,
1236
+ cert=self.cert,
1237
+ headers=self.headers)
1238
+
1239
+ if get_result and get_result.status_code == 200:
1240
+ WHOAMI_COUNTER.labels(state='success', host=self.__extract_host(self.external_host)).inc()
1241
+ return get_result.json()
1242
+
1243
+ WHOAMI_COUNTER.labels(state='failure', host=self.__extract_host(self.external_host)).inc()
1244
+ raise Exception('Could not retrieve credentials: %s', get_result.content)
1245
+
1246
+ def version(self) -> dict[str, Any]:
1247
+ """
1248
+ Returns FTS3 server information.
1249
+
1250
+ :returns: FTS3 server information as a dictionary.
1251
+ """
1252
+
1253
+ get_result = None
1254
+
1255
+ get_result = requests.get('%s/' % self.external_host,
1256
+ verify=self.verify,
1257
+ cert=self.cert,
1258
+ headers=self.headers)
1259
+
1260
+ if get_result and get_result.status_code == 200:
1261
+ VERSION_COUNTER.labels(state='success', host=self.__extract_host(self.external_host)).inc()
1262
+ return get_result.json()
1263
+
1264
+ VERSION_COUNTER.labels(state='failure', host=self.__extract_host(self.external_host)).inc()
1265
+ raise Exception('Could not retrieve version: %s', get_result.content)
1266
+
1267
+ def bulk_query(self, requests_by_eid: dict[str, dict[str, dict[str, Any]]], timeout: Optional[int] = None) -> dict[str, Any]:
1268
+ """
1269
+ Query the status of a bulk of transfers in FTS3 via JSON.
1270
+
1271
+ :param requests_by_eid: dictionary {external_id1: {request_id1: request1, ...}, ...} of request to be queried
1272
+ :returns: Transfer status information as a dictionary.
1273
+ """
1274
+
1275
+ responses = {}
1276
+ fts_session = requests.Session()
1277
+ xfer_ids = ','.join(requests_by_eid)
1278
+ jobs = fts_session.get('%s/jobs/%s?files=file_state,dest_surl,finish_time,start_time,staging_start,staging_finished,reason,source_surl,file_metadata' % (self.external_host, xfer_ids),
1279
+ verify=self.verify,
1280
+ cert=self.cert,
1281
+ headers=self.headers,
1282
+ timeout=timeout)
1283
+
1284
+ if jobs is None:
1285
+ BULK_QUERY_COUNTER.labels(state='failure', host=self.__extract_host(self.external_host)).inc()
1286
+ for transfer_id in requests_by_eid:
1287
+ responses[transfer_id] = Exception('Transfer information returns None: %s' % jobs)
1288
+ elif jobs.status_code in (200, 207, 404):
1289
+ try:
1290
+ BULK_QUERY_COUNTER.labels(state='success', host=self.__extract_host(self.external_host)).inc()
1291
+ jobs_response = jobs.json()
1292
+ responses = self.__bulk_query_responses(jobs_response, requests_by_eid)
1293
+ except ReadTimeout as error:
1294
+ raise TransferToolTimeout(error)
1295
+ except json.JSONDecodeError as error:
1296
+ raise TransferToolWrongAnswer(error)
1297
+ except Exception as error:
1298
+ raise Exception("Failed to parse the job response: %s, error: %s" % (str(jobs), str(error)))
1299
+ else:
1300
+ BULK_QUERY_COUNTER.labels(state='failure', host=self.__extract_host(self.external_host)).inc()
1301
+ for transfer_id in requests_by_eid:
1302
+ responses[transfer_id] = Exception('Could not retrieve transfer information: %s', jobs.content)
1303
+
1304
+ return responses
1305
+
1306
+ def list_se_status(self) -> dict[str, Any]:
1307
+ """
1308
+ Get the list of banned Storage Elements.
1309
+
1310
+ :returns: Detailed dictionary of banned Storage Elements.
1311
+ """
1312
+
1313
+ try:
1314
+ result = requests.get('%s/ban/se' % self.external_host,
1315
+ verify=self.verify,
1316
+ cert=self.cert,
1317
+ headers=self.headers,
1318
+ timeout=None)
1319
+ except Exception as error:
1320
+ raise Exception('Could not retrieve transfer information: %s', error)
1321
+ if result and result.status_code == 200:
1322
+ return result.json()
1323
+ raise Exception('Could not retrieve transfer information: %s', result.content)
1324
+
1325
+ def get_se_config(self, storage_element: str) -> dict[str, Any]:
1326
+ """
1327
+ Get the Json response for the configuration of a storage element.
1328
+ :returns: a Json result for the configuration of a storage element.
1329
+ :param storage_element: the storage element you want the configuration for.
1330
+ """
1331
+
1332
+ try:
1333
+ result = requests.get('%s/config/se' % (self.external_host),
1334
+ verify=self.verify,
1335
+ cert=self.cert,
1336
+ headers=self.headers,
1337
+ timeout=None)
1338
+ except Exception:
1339
+ self.logger(logging.WARNING, 'Could not get config of %s on %s - %s', storage_element, self.external_host, str(traceback.format_exc()))
1340
+ if result and result.status_code == 200:
1341
+ C = result.json()
1342
+ config_se = C[storage_element]
1343
+ return config_se
1344
+ raise Exception('Could not get the configuration of %s , status code returned : %s', (storage_element, result.status_code if result else None))
1345
+
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]:
1355
+ """
1356
+ Set the configuration for a storage element. Used for alleviating transfer failures due to timeout.
1357
+
1358
+ :param storage_element: The storage element to be configured
1359
+ :param inbound_max_active: the integer to set the inbound_max_active for the SE.
1360
+ :param outbound_max_active: the integer to set the outbound_max_active for the SE.
1361
+ :param inbound_max_throughput: the float to set the inbound_max_throughput for the SE.
1362
+ :param outbound_max_throughput: the float to set the outbound_max_throughput for the SE.
1363
+ :param staging: the integer to set the staging for the operation of a SE.
1364
+ :returns: JSON post response in case of success, otherwise raise Exception.
1365
+ """
1366
+
1367
+ params_dict = {storage_element: {'operations': {}, 'se_info': {}}}
1368
+ if staging is not None:
1369
+ try:
1370
+ policy = config_get('policy', 'permission')
1371
+ except Exception:
1372
+ self.logger(logging.WARNING, 'Could not get policy from config')
1373
+ params_dict[storage_element]['operations'] = {policy: {'staging': staging}}
1374
+ # A lot of try-excepts to avoid dictionary overwrite's,
1375
+ # see https://stackoverflow.com/questions/27118687/updating-nested-dictionaries-when-data-has-existing-key/27118776
1376
+ if inbound_max_active is not None:
1377
+ try:
1378
+ params_dict[storage_element]['se_info']['inbound_max_active'] = inbound_max_active
1379
+ except KeyError:
1380
+ params_dict[storage_element]['se_info'] = {'inbound_max_active': inbound_max_active}
1381
+ if outbound_max_active is not None:
1382
+ try:
1383
+ params_dict[storage_element]['se_info']['outbound_max_active'] = outbound_max_active
1384
+ except KeyError:
1385
+ params_dict[storage_element]['se_info'] = {'outbound_max_active': outbound_max_active}
1386
+ if inbound_max_throughput is not None:
1387
+ try:
1388
+ params_dict[storage_element]['se_info']['inbound_max_throughput'] = inbound_max_throughput
1389
+ except KeyError:
1390
+ params_dict[storage_element]['se_info'] = {'inbound_max_throughput': inbound_max_throughput}
1391
+ if outbound_max_throughput is not None:
1392
+ try:
1393
+ params_dict[storage_element]['se_info']['outbound_max_throughput'] = outbound_max_throughput
1394
+ except KeyError:
1395
+ params_dict[storage_element]['se_info'] = {'outbound_max_throughput': outbound_max_throughput}
1396
+
1397
+ params_str = json.dumps(params_dict, cls=APIEncoder)
1398
+
1399
+ try:
1400
+ result = requests.post('%s/config/se' % (self.external_host),
1401
+ verify=self.verify,
1402
+ cert=self.cert,
1403
+ data=params_str,
1404
+ headers=self.headers,
1405
+ timeout=None)
1406
+
1407
+ except Exception:
1408
+ self.logger(logging.WARNING, 'Could not set the config of %s on %s - %s', storage_element, self.external_host, str(traceback.format_exc()))
1409
+ if result and result.status_code == 200:
1410
+ configSe = result.json()
1411
+ return configSe
1412
+ raise Exception('Could not set the configuration of %s , status code returned : %s', (storage_element, result.status_code if result else None))
1413
+
1414
+ def set_se_status(self, storage_element: str, message: str, ban: bool = True, timeout: Optional[int] = None) -> int:
1415
+ """
1416
+ Ban a Storage Element. Used when a site is in downtime.
1417
+ One can use a timeout in seconds. In that case the jobs will wait before being cancel.
1418
+ If no timeout is specified, the jobs are canceled immediately
1419
+
1420
+ :param storage_element: The Storage Element that will be banned.
1421
+ :param message: The reason of the ban.
1422
+ :param ban: Boolean. If set to True, ban the SE, if set to False unban the SE.
1423
+ :param timeout: if None, send to FTS status 'cancel' else 'waiting' + the corresponding timeout.
1424
+
1425
+ :returns: 0 in case of success, otherwise raise Exception
1426
+ """
1427
+
1428
+ params_dict: dict[str, Any] = {'storage': storage_element, 'message': message}
1429
+ status = 'CANCEL'
1430
+ if timeout:
1431
+ params_dict['timeout'] = timeout
1432
+ status = 'WAIT'
1433
+ params_dict['status'] = status
1434
+ params_str = json.dumps(params_dict, cls=APIEncoder)
1435
+
1436
+ result = None
1437
+ if ban:
1438
+ try:
1439
+ result = requests.post('%s/ban/se' % self.external_host,
1440
+ verify=self.verify,
1441
+ cert=self.cert,
1442
+ data=params_str,
1443
+ headers=self.headers,
1444
+ timeout=None)
1445
+ except Exception:
1446
+ self.logger(logging.WARNING, 'Could not ban %s on %s - %s', storage_element, self.external_host, str(traceback.format_exc()))
1447
+ if result and result.status_code == 200:
1448
+ return 0
1449
+ raise Exception('Could not ban the storage %s , status code returned : %s', (storage_element, result.status_code if result else None))
1450
+ else:
1451
+
1452
+ try:
1453
+ result = requests.delete('%s/ban/se?storage=%s' % (self.external_host, storage_element),
1454
+ verify=self.verify,
1455
+ cert=self.cert,
1456
+ data=params_str,
1457
+ headers=self.headers,
1458
+ timeout=None)
1459
+ except Exception:
1460
+ self.logger(logging.WARNING, 'Could not unban %s on %s - %s', storage_element, self.external_host, str(traceback.format_exc()))
1461
+ if result and result.status_code == 204:
1462
+ return 0
1463
+ raise Exception('Could not unban the storage %s , status code returned : %s', (storage_element, result.status_code if result else None))
1464
+
1465
+ # Private methods unique to the FTS3 Transfertool
1466
+
1467
+ @staticmethod
1468
+ def __extract_host(external_host: str) -> Optional[str]:
1469
+ # graphite does not like the dots in the FQDN
1470
+ parsed_url = urlparse(external_host)
1471
+ if parsed_url.hostname:
1472
+ return parsed_url.hostname.replace('.', '_')
1473
+
1474
+ def __get_transfer_baseid_voname(self) -> tuple[Optional[str], Optional[str]]:
1475
+ """
1476
+ Get transfer VO name from the external host.
1477
+
1478
+ :returns base id as a string and VO name as a string.
1479
+ """
1480
+ result = (None, None)
1481
+ try:
1482
+ key = 'voname:%s' % self.external_host
1483
+ result = REGION_SHORT.get(key)
1484
+ if isinstance(result, NoValue):
1485
+ self.logger(logging.DEBUG, "Refresh transfer baseid and voname for %s", self.external_host)
1486
+
1487
+ get_result = None
1488
+ try:
1489
+ get_result = requests.get('%s/whoami' % self.external_host,
1490
+ verify=self.verify,
1491
+ cert=self.cert,
1492
+ headers=self.headers,
1493
+ timeout=5)
1494
+ except ReadTimeout as error:
1495
+ raise TransferToolTimeout(error)
1496
+ except json.JSONDecodeError as error:
1497
+ raise TransferToolWrongAnswer(error)
1498
+ except Exception as error:
1499
+ self.logger(logging.WARNING, 'Could not get baseid and voname from %s - %s' % (self.external_host, str(error)))
1500
+
1501
+ if get_result and get_result.status_code == 200:
1502
+ baseid = str(get_result.json()['base_id'])
1503
+ voname = str(get_result.json()['vos'][0])
1504
+ result = (baseid, voname)
1505
+
1506
+ REGION_SHORT.set(key, result)
1507
+
1508
+ self.logger(logging.DEBUG, "Get baseid %s and voname %s from %s", baseid, voname, self.external_host)
1509
+ else:
1510
+ self.logger(logging.WARNING, "Failed to get baseid and voname from %s, error: %s", self.external_host, get_result.text if get_result is not None else get_result)
1511
+ result = (None, None)
1512
+ except Exception as error:
1513
+ self.logger(logging.WARNING, "Failed to get baseid and voname from %s: %s" % (self.external_host, str(error)))
1514
+ result = (None, None)
1515
+ return result
1516
+
1517
+ def __get_deterministic_id(self, sid: str) -> Optional[str]:
1518
+ """
1519
+ Get deterministic FTS job id.
1520
+
1521
+ :param sid: FTS seed id.
1522
+ :returns: FTS transfer identifier.
1523
+ """
1524
+ baseid, voname = self.__get_transfer_baseid_voname()
1525
+ if baseid is None or voname is None:
1526
+ return None
1527
+ root = uuid.UUID(baseid)
1528
+ atlas = uuid.uuid5(root, voname)
1529
+ jobid = uuid.uuid5(atlas, sid)
1530
+ return str(jobid)
1531
+
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]:
1533
+ if not isinstance(jobs_response, list):
1534
+ jobs_response = [jobs_response]
1535
+
1536
+ responses = {}
1537
+ for job_response in jobs_response:
1538
+ transfer_id = job_response['job_id']
1539
+ if job_response['http_status'] == '200 Ok':
1540
+ files_response = job_response['files']
1541
+ multi_sources = job_response['job_metadata'].get('multi_sources', False)
1542
+ if multi_sources and job_response['job_state'] not in [FTS_STATE.FAILED,
1543
+ FTS_STATE.FINISHEDDIRTY,
1544
+ FTS_STATE.CANCELED,
1545
+ FTS_STATE.FINISHED]:
1546
+ # multiple source replicas jobs is still running. should wait
1547
+ responses[transfer_id] = {}
1548
+ continue
1549
+
1550
+ resps = {}
1551
+ for file_resp in files_response:
1552
+ file_state = file_resp['file_state']
1553
+ # for multiple source replicas jobs, the file_metadata(request_id) will be the same.
1554
+ # The next used file will overwrite the current used one. Only the last used file will return.
1555
+ if multi_sources and file_state == FTS_STATE.NOT_USED:
1556
+ continue
1557
+
1558
+ request_id = file_resp['file_metadata']['request_id']
1559
+ request = requests_by_eid.get(transfer_id, {}).get(request_id)
1560
+ if request is not None:
1561
+ resps[request_id] = FTS3ApiTransferStatusReport(self.external_host, request_id=request_id, request=request,
1562
+ job_response=job_response, file_response=file_resp)
1563
+
1564
+ # multiple source replicas jobs and we found the successful one, it's the final state.
1565
+ if multi_sources and file_state == FTS_STATE.FINISHED:
1566
+ break
1567
+ responses[transfer_id] = resps
1568
+ elif job_response['http_status'] == '404 Not Found':
1569
+ # Lost transfer
1570
+ responses[transfer_id] = None
1571
+ else:
1572
+ responses[transfer_id] = Exception('Could not retrieve transfer information(http_status: %s, http_message: %s)' % (job_response['http_status'],
1573
+ job_response['http_message'] if 'http_message' in job_response else None))
1574
+ return responses
1575
+
1576
+ def __query_details(self, transfer_id: str) -> Optional[dict[str, Any]]:
1577
+ """
1578
+ Query the detailed status of a transfer in FTS3 via JSON.
1579
+
1580
+ :param transfer_id: FTS transfer identifier as a string.
1581
+ :returns: Detailed transfer status information as a dictionary.
1582
+ """
1583
+
1584
+ files = None
1585
+
1586
+ files = requests.get('%s/jobs/%s/files' % (self.external_host, transfer_id),
1587
+ verify=self.verify,
1588
+ cert=self.cert,
1589
+ headers=self.headers,
1590
+ timeout=5)
1591
+ if files and (files.status_code == 200 or files.status_code == 207):
1592
+ QUERY_DETAILS_COUNTER.labels(state='success', host=self.__extract_host(self.external_host)).inc()
1593
+ return files.json()
1594
+
1595
+ QUERY_DETAILS_COUNTER.labels(state='failure', host=self.__extract_host(self.external_host)).inc()
1596
+ return