rucio 32.8.6__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (481) hide show
  1. rucio/__init__.py +18 -0
  2. rucio/alembicrevision.py +16 -0
  3. rucio/api/__init__.py +14 -0
  4. rucio/api/account.py +266 -0
  5. rucio/api/account_limit.py +287 -0
  6. rucio/api/authentication.py +302 -0
  7. rucio/api/config.py +218 -0
  8. rucio/api/credential.py +60 -0
  9. rucio/api/did.py +726 -0
  10. rucio/api/dirac.py +71 -0
  11. rucio/api/exporter.py +60 -0
  12. rucio/api/heartbeat.py +62 -0
  13. rucio/api/identity.py +160 -0
  14. rucio/api/importer.py +46 -0
  15. rucio/api/lifetime_exception.py +95 -0
  16. rucio/api/lock.py +131 -0
  17. rucio/api/meta.py +85 -0
  18. rucio/api/permission.py +72 -0
  19. rucio/api/quarantined_replica.py +69 -0
  20. rucio/api/replica.py +528 -0
  21. rucio/api/request.py +220 -0
  22. rucio/api/rse.py +601 -0
  23. rucio/api/rule.py +335 -0
  24. rucio/api/scope.py +89 -0
  25. rucio/api/subscription.py +255 -0
  26. rucio/api/temporary_did.py +49 -0
  27. rucio/api/vo.py +112 -0
  28. rucio/client/__init__.py +16 -0
  29. rucio/client/accountclient.py +413 -0
  30. rucio/client/accountlimitclient.py +155 -0
  31. rucio/client/baseclient.py +929 -0
  32. rucio/client/client.py +77 -0
  33. rucio/client/configclient.py +113 -0
  34. rucio/client/credentialclient.py +54 -0
  35. rucio/client/didclient.py +691 -0
  36. rucio/client/diracclient.py +48 -0
  37. rucio/client/downloadclient.py +1674 -0
  38. rucio/client/exportclient.py +44 -0
  39. rucio/client/fileclient.py +51 -0
  40. rucio/client/importclient.py +42 -0
  41. rucio/client/lifetimeclient.py +74 -0
  42. rucio/client/lockclient.py +99 -0
  43. rucio/client/metaclient.py +137 -0
  44. rucio/client/pingclient.py +45 -0
  45. rucio/client/replicaclient.py +444 -0
  46. rucio/client/requestclient.py +109 -0
  47. rucio/client/rseclient.py +664 -0
  48. rucio/client/ruleclient.py +287 -0
  49. rucio/client/scopeclient.py +88 -0
  50. rucio/client/subscriptionclient.py +161 -0
  51. rucio/client/touchclient.py +78 -0
  52. rucio/client/uploadclient.py +871 -0
  53. rucio/common/__init__.py +14 -0
  54. rucio/common/cache.py +74 -0
  55. rucio/common/config.py +796 -0
  56. rucio/common/constants.py +92 -0
  57. rucio/common/constraints.py +18 -0
  58. rucio/common/didtype.py +187 -0
  59. rucio/common/dumper/__init__.py +306 -0
  60. rucio/common/dumper/consistency.py +449 -0
  61. rucio/common/dumper/data_models.py +325 -0
  62. rucio/common/dumper/path_parsing.py +65 -0
  63. rucio/common/exception.py +1092 -0
  64. rucio/common/extra.py +37 -0
  65. rucio/common/logging.py +404 -0
  66. rucio/common/pcache.py +1387 -0
  67. rucio/common/policy.py +84 -0
  68. rucio/common/schema/__init__.py +143 -0
  69. rucio/common/schema/atlas.py +411 -0
  70. rucio/common/schema/belleii.py +406 -0
  71. rucio/common/schema/cms.py +478 -0
  72. rucio/common/schema/domatpc.py +399 -0
  73. rucio/common/schema/escape.py +424 -0
  74. rucio/common/schema/generic.py +431 -0
  75. rucio/common/schema/generic_multi_vo.py +410 -0
  76. rucio/common/schema/icecube.py +404 -0
  77. rucio/common/schema/lsst.py +423 -0
  78. rucio/common/stomp_utils.py +160 -0
  79. rucio/common/stopwatch.py +56 -0
  80. rucio/common/test_rucio_server.py +148 -0
  81. rucio/common/types.py +158 -0
  82. rucio/common/utils.py +1946 -0
  83. rucio/core/__init__.py +14 -0
  84. rucio/core/account.py +426 -0
  85. rucio/core/account_counter.py +171 -0
  86. rucio/core/account_limit.py +357 -0
  87. rucio/core/authentication.py +563 -0
  88. rucio/core/config.py +386 -0
  89. rucio/core/credential.py +218 -0
  90. rucio/core/did.py +3102 -0
  91. rucio/core/did_meta_plugins/__init__.py +250 -0
  92. rucio/core/did_meta_plugins/did_column_meta.py +326 -0
  93. rucio/core/did_meta_plugins/did_meta_plugin_interface.py +116 -0
  94. rucio/core/did_meta_plugins/filter_engine.py +573 -0
  95. rucio/core/did_meta_plugins/json_meta.py +215 -0
  96. rucio/core/did_meta_plugins/mongo_meta.py +199 -0
  97. rucio/core/did_meta_plugins/postgres_meta.py +317 -0
  98. rucio/core/dirac.py +208 -0
  99. rucio/core/distance.py +164 -0
  100. rucio/core/exporter.py +59 -0
  101. rucio/core/heartbeat.py +263 -0
  102. rucio/core/identity.py +290 -0
  103. rucio/core/importer.py +248 -0
  104. rucio/core/lifetime_exception.py +377 -0
  105. rucio/core/lock.py +474 -0
  106. rucio/core/message.py +241 -0
  107. rucio/core/meta.py +190 -0
  108. rucio/core/monitor.py +441 -0
  109. rucio/core/naming_convention.py +154 -0
  110. rucio/core/nongrid_trace.py +124 -0
  111. rucio/core/oidc.py +1339 -0
  112. rucio/core/permission/__init__.py +107 -0
  113. rucio/core/permission/atlas.py +1333 -0
  114. rucio/core/permission/belleii.py +1076 -0
  115. rucio/core/permission/cms.py +1166 -0
  116. rucio/core/permission/escape.py +1076 -0
  117. rucio/core/permission/generic.py +1128 -0
  118. rucio/core/permission/generic_multi_vo.py +1148 -0
  119. rucio/core/quarantined_replica.py +190 -0
  120. rucio/core/replica.py +3627 -0
  121. rucio/core/replica_sorter.py +368 -0
  122. rucio/core/request.py +2241 -0
  123. rucio/core/rse.py +1835 -0
  124. rucio/core/rse_counter.py +155 -0
  125. rucio/core/rse_expression_parser.py +460 -0
  126. rucio/core/rse_selector.py +277 -0
  127. rucio/core/rule.py +3419 -0
  128. rucio/core/rule_grouping.py +1473 -0
  129. rucio/core/scope.py +152 -0
  130. rucio/core/subscription.py +316 -0
  131. rucio/core/temporary_did.py +188 -0
  132. rucio/core/topology.py +448 -0
  133. rucio/core/trace.py +361 -0
  134. rucio/core/transfer.py +1233 -0
  135. rucio/core/vo.py +151 -0
  136. rucio/core/volatile_replica.py +123 -0
  137. rucio/daemons/__init__.py +14 -0
  138. rucio/daemons/abacus/__init__.py +14 -0
  139. rucio/daemons/abacus/account.py +106 -0
  140. rucio/daemons/abacus/collection_replica.py +113 -0
  141. rucio/daemons/abacus/rse.py +107 -0
  142. rucio/daemons/atropos/__init__.py +14 -0
  143. rucio/daemons/atropos/atropos.py +243 -0
  144. rucio/daemons/auditor/__init__.py +261 -0
  145. rucio/daemons/auditor/hdfs.py +86 -0
  146. rucio/daemons/auditor/srmdumps.py +284 -0
  147. rucio/daemons/automatix/__init__.py +14 -0
  148. rucio/daemons/automatix/automatix.py +281 -0
  149. rucio/daemons/badreplicas/__init__.py +14 -0
  150. rucio/daemons/badreplicas/minos.py +311 -0
  151. rucio/daemons/badreplicas/minos_temporary_expiration.py +173 -0
  152. rucio/daemons/badreplicas/necromancer.py +200 -0
  153. rucio/daemons/bb8/__init__.py +14 -0
  154. rucio/daemons/bb8/bb8.py +356 -0
  155. rucio/daemons/bb8/common.py +762 -0
  156. rucio/daemons/bb8/nuclei_background_rebalance.py +147 -0
  157. rucio/daemons/bb8/t2_background_rebalance.py +146 -0
  158. rucio/daemons/c3po/__init__.py +14 -0
  159. rucio/daemons/c3po/algorithms/__init__.py +14 -0
  160. rucio/daemons/c3po/algorithms/simple.py +131 -0
  161. rucio/daemons/c3po/algorithms/t2_free_space.py +125 -0
  162. rucio/daemons/c3po/algorithms/t2_free_space_only_pop.py +127 -0
  163. rucio/daemons/c3po/algorithms/t2_free_space_only_pop_with_network.py +279 -0
  164. rucio/daemons/c3po/c3po.py +342 -0
  165. rucio/daemons/c3po/collectors/__init__.py +14 -0
  166. rucio/daemons/c3po/collectors/agis.py +108 -0
  167. rucio/daemons/c3po/collectors/free_space.py +62 -0
  168. rucio/daemons/c3po/collectors/jedi_did.py +48 -0
  169. rucio/daemons/c3po/collectors/mock_did.py +46 -0
  170. rucio/daemons/c3po/collectors/network_metrics.py +63 -0
  171. rucio/daemons/c3po/collectors/workload.py +110 -0
  172. rucio/daemons/c3po/utils/__init__.py +14 -0
  173. rucio/daemons/c3po/utils/dataset_cache.py +40 -0
  174. rucio/daemons/c3po/utils/expiring_dataset_cache.py +45 -0
  175. rucio/daemons/c3po/utils/expiring_list.py +63 -0
  176. rucio/daemons/c3po/utils/popularity.py +82 -0
  177. rucio/daemons/c3po/utils/timeseries.py +76 -0
  178. rucio/daemons/cache/__init__.py +14 -0
  179. rucio/daemons/cache/consumer.py +191 -0
  180. rucio/daemons/common.py +391 -0
  181. rucio/daemons/conveyor/__init__.py +14 -0
  182. rucio/daemons/conveyor/common.py +530 -0
  183. rucio/daemons/conveyor/finisher.py +492 -0
  184. rucio/daemons/conveyor/poller.py +372 -0
  185. rucio/daemons/conveyor/preparer.py +198 -0
  186. rucio/daemons/conveyor/receiver.py +206 -0
  187. rucio/daemons/conveyor/stager.py +127 -0
  188. rucio/daemons/conveyor/submitter.py +379 -0
  189. rucio/daemons/conveyor/throttler.py +468 -0
  190. rucio/daemons/follower/__init__.py +14 -0
  191. rucio/daemons/follower/follower.py +97 -0
  192. rucio/daemons/hermes/__init__.py +14 -0
  193. rucio/daemons/hermes/hermes.py +738 -0
  194. rucio/daemons/judge/__init__.py +14 -0
  195. rucio/daemons/judge/cleaner.py +149 -0
  196. rucio/daemons/judge/evaluator.py +172 -0
  197. rucio/daemons/judge/injector.py +154 -0
  198. rucio/daemons/judge/repairer.py +144 -0
  199. rucio/daemons/oauthmanager/__init__.py +14 -0
  200. rucio/daemons/oauthmanager/oauthmanager.py +199 -0
  201. rucio/daemons/reaper/__init__.py +14 -0
  202. rucio/daemons/reaper/dark_reaper.py +272 -0
  203. rucio/daemons/reaper/light_reaper.py +255 -0
  204. rucio/daemons/reaper/reaper.py +701 -0
  205. rucio/daemons/replicarecoverer/__init__.py +14 -0
  206. rucio/daemons/replicarecoverer/suspicious_replica_recoverer.py +487 -0
  207. rucio/daemons/storage/__init__.py +14 -0
  208. rucio/daemons/storage/consistency/__init__.py +14 -0
  209. rucio/daemons/storage/consistency/actions.py +753 -0
  210. rucio/daemons/tracer/__init__.py +14 -0
  211. rucio/daemons/tracer/kronos.py +513 -0
  212. rucio/daemons/transmogrifier/__init__.py +14 -0
  213. rucio/daemons/transmogrifier/transmogrifier.py +753 -0
  214. rucio/daemons/undertaker/__init__.py +14 -0
  215. rucio/daemons/undertaker/undertaker.py +137 -0
  216. rucio/db/__init__.py +14 -0
  217. rucio/db/sqla/__init__.py +38 -0
  218. rucio/db/sqla/constants.py +192 -0
  219. rucio/db/sqla/migrate_repo/__init__.py +14 -0
  220. rucio/db/sqla/migrate_repo/env.py +111 -0
  221. rucio/db/sqla/migrate_repo/versions/01eaf73ab656_add_new_rule_notification_state_progress.py +71 -0
  222. rucio/db/sqla/migrate_repo/versions/0437a40dbfd1_add_eol_at_in_rules.py +50 -0
  223. rucio/db/sqla/migrate_repo/versions/0f1adb7a599a_create_transfer_hops_table.py +61 -0
  224. rucio/db/sqla/migrate_repo/versions/102efcf145f4_added_stuck_at_column_to_rules.py +46 -0
  225. rucio/db/sqla/migrate_repo/versions/13d4f70c66a9_introduce_transfer_limits.py +93 -0
  226. rucio/db/sqla/migrate_repo/versions/140fef722e91_cleanup_distances_table.py +78 -0
  227. rucio/db/sqla/migrate_repo/versions/14ec5aeb64cf_add_request_external_host.py +46 -0
  228. rucio/db/sqla/migrate_repo/versions/156fb5b5a14_add_request_type_to_requests_idx.py +53 -0
  229. rucio/db/sqla/migrate_repo/versions/1677d4d803c8_split_rse_availability_into_multiple.py +69 -0
  230. rucio/db/sqla/migrate_repo/versions/16a0aca82e12_create_index_on_table_replicas_path.py +42 -0
  231. rucio/db/sqla/migrate_repo/versions/1803333ac20f_adding_provenance_and_phys_group.py +46 -0
  232. rucio/db/sqla/migrate_repo/versions/1a29d6a9504c_add_didtype_chck_to_requests.py +61 -0
  233. rucio/db/sqla/migrate_repo/versions/1a80adff031a_create_index_on_rules_hist_recent.py +42 -0
  234. rucio/db/sqla/migrate_repo/versions/1c45d9730ca6_increase_identity_length.py +141 -0
  235. rucio/db/sqla/migrate_repo/versions/1d1215494e95_add_quarantined_replicas_table.py +75 -0
  236. rucio/db/sqla/migrate_repo/versions/1d96f484df21_asynchronous_rules_and_rule_approval.py +75 -0
  237. rucio/db/sqla/migrate_repo/versions/1f46c5f240ac_add_bytes_column_to_bad_replicas.py +46 -0
  238. rucio/db/sqla/migrate_repo/versions/1fc15ab60d43_add_message_history_table.py +51 -0
  239. rucio/db/sqla/migrate_repo/versions/2190e703eb6e_move_rse_settings_to_rse_attributes.py +135 -0
  240. rucio/db/sqla/migrate_repo/versions/21d6b9dc9961_add_mismatch_scheme_state_to_requests.py +65 -0
  241. rucio/db/sqla/migrate_repo/versions/22cf51430c78_add_availability_column_to_table_rses.py +42 -0
  242. rucio/db/sqla/migrate_repo/versions/22d887e4ec0a_create_sources_table.py +66 -0
  243. rucio/db/sqla/migrate_repo/versions/25821a8a45a3_remove_unique_constraint_on_requests.py +54 -0
  244. rucio/db/sqla/migrate_repo/versions/25fc855625cf_added_unique_constraint_to_rules.py +43 -0
  245. rucio/db/sqla/migrate_repo/versions/269fee20dee9_add_repair_cnt_to_locks.py +46 -0
  246. rucio/db/sqla/migrate_repo/versions/271a46ea6244_add_ignore_availability_column_to_rules.py +47 -0
  247. rucio/db/sqla/migrate_repo/versions/277b5fbb41d3_switch_heartbeats_executable.py +54 -0
  248. rucio/db/sqla/migrate_repo/versions/27e3a68927fb_remove_replicas_tombstone_and_replicas_.py +39 -0
  249. rucio/db/sqla/migrate_repo/versions/2854cd9e168_added_rule_id_column.py +48 -0
  250. rucio/db/sqla/migrate_repo/versions/295289b5a800_processed_by_and__at_in_requests.py +47 -0
  251. rucio/db/sqla/migrate_repo/versions/2962ece31cf4_add_nbaccesses_column_in_the_did_table.py +48 -0
  252. rucio/db/sqla/migrate_repo/versions/2af3291ec4c_added_replicas_history_table.py +59 -0
  253. rucio/db/sqla/migrate_repo/versions/2b69addda658_add_columns_for_third_party_copy_read_.py +47 -0
  254. rucio/db/sqla/migrate_repo/versions/2b8e7bcb4783_add_config_table.py +72 -0
  255. rucio/db/sqla/migrate_repo/versions/2ba5229cb54c_add_submitted_at_to_requests_table.py +46 -0
  256. rucio/db/sqla/migrate_repo/versions/2cbee484dcf9_added_column_volume_to_rse_transfer_.py +45 -0
  257. rucio/db/sqla/migrate_repo/versions/2edee4a83846_add_source_to_requests_and_requests_.py +48 -0
  258. rucio/db/sqla/migrate_repo/versions/2eef46be23d4_change_tokens_pk.py +48 -0
  259. rucio/db/sqla/migrate_repo/versions/2f648fc909f3_index_in_rule_history_on_scope_name.py +42 -0
  260. rucio/db/sqla/migrate_repo/versions/3082b8cef557_add_naming_convention_table_and_closed_.py +69 -0
  261. rucio/db/sqla/migrate_repo/versions/30fa38b6434e_add_index_on_service_column_in_the_message_table.py +46 -0
  262. rucio/db/sqla/migrate_repo/versions/3152492b110b_added_staging_area_column.py +78 -0
  263. rucio/db/sqla/migrate_repo/versions/32c7d2783f7e_create_bad_replicas_table.py +62 -0
  264. rucio/db/sqla/migrate_repo/versions/3345511706b8_replicas_table_pk_definition_is_in_.py +74 -0
  265. rucio/db/sqla/migrate_repo/versions/35ef10d1e11b_change_index_on_table_requests.py +44 -0
  266. rucio/db/sqla/migrate_repo/versions/379a19b5332d_create_rse_limits_table.py +67 -0
  267. rucio/db/sqla/migrate_repo/versions/384b96aa0f60_created_rule_history_tables.py +134 -0
  268. rucio/db/sqla/migrate_repo/versions/3ac1660a1a72_extend_distance_table.py +58 -0
  269. rucio/db/sqla/migrate_repo/versions/3ad36e2268b0_create_collection_replicas_updates_table.py +79 -0
  270. rucio/db/sqla/migrate_repo/versions/3c9df354071b_extend_waiting_request_state.py +61 -0
  271. rucio/db/sqla/migrate_repo/versions/3d9813fab443_add_a_new_state_lost_in_badfilesstatus.py +45 -0
  272. rucio/db/sqla/migrate_repo/versions/40ad39ce3160_add_transferred_at_to_requests_table.py +46 -0
  273. rucio/db/sqla/migrate_repo/versions/4207be2fd914_add_notification_column_to_rules.py +65 -0
  274. rucio/db/sqla/migrate_repo/versions/42db2617c364_create_index_on_requests_external_id.py +42 -0
  275. rucio/db/sqla/migrate_repo/versions/436827b13f82_added_column_activity_to_table_requests.py +46 -0
  276. rucio/db/sqla/migrate_repo/versions/44278720f774_update_requests_typ_sta_upd_idx_index.py +46 -0
  277. rucio/db/sqla/migrate_repo/versions/45378a1e76a8_create_collection_replica_table.py +80 -0
  278. rucio/db/sqla/migrate_repo/versions/469d262be19_removing_created_at_index.py +43 -0
  279. rucio/db/sqla/migrate_repo/versions/4783c1f49cb4_create_distance_table.py +61 -0
  280. rucio/db/sqla/migrate_repo/versions/49a21b4d4357_create_index_on_table_tokens.py +47 -0
  281. rucio/db/sqla/migrate_repo/versions/4a2cbedda8b9_add_source_replica_expression_column_to_.py +46 -0
  282. rucio/db/sqla/migrate_repo/versions/4a7182d9578b_added_bytes_length_accessed_at_columns.py +52 -0
  283. rucio/db/sqla/migrate_repo/versions/4bab9edd01fc_create_index_on_requests_rule_id.py +42 -0
  284. rucio/db/sqla/migrate_repo/versions/4c3a4acfe006_new_attr_account_table.py +65 -0
  285. rucio/db/sqla/migrate_repo/versions/4cf0a2e127d4_adding_transient_metadata.py +46 -0
  286. rucio/db/sqla/migrate_repo/versions/50280c53117c_add_qos_class_to_rse.py +47 -0
  287. rucio/db/sqla/migrate_repo/versions/52153819589c_add_rse_id_to_replicas_table.py +45 -0
  288. rucio/db/sqla/migrate_repo/versions/52fd9f4916fa_added_activity_to_rules.py +46 -0
  289. rucio/db/sqla/migrate_repo/versions/53b479c3cb0f_fix_did_meta_table_missing_updated_at_.py +48 -0
  290. rucio/db/sqla/migrate_repo/versions/5673b4b6e843_add_wfms_metadata_to_rule_tables.py +50 -0
  291. rucio/db/sqla/migrate_repo/versions/575767d9f89_added_source_history_table.py +59 -0
  292. rucio/db/sqla/migrate_repo/versions/58bff7008037_add_started_at_to_requests.py +48 -0
  293. rucio/db/sqla/migrate_repo/versions/58c8b78301ab_rename_callback_to_message.py +108 -0
  294. rucio/db/sqla/migrate_repo/versions/5f139f77382a_added_child_rule_id_column.py +57 -0
  295. rucio/db/sqla/migrate_repo/versions/688ef1840840_adding_did_meta_table.py +51 -0
  296. rucio/db/sqla/migrate_repo/versions/6e572a9bfbf3_add_new_split_container_column_to_rules.py +50 -0
  297. rucio/db/sqla/migrate_repo/versions/70587619328_add_comment_column_for_subscriptions.py +46 -0
  298. rucio/db/sqla/migrate_repo/versions/739064d31565_remove_history_table_pks.py +42 -0
  299. rucio/db/sqla/migrate_repo/versions/7541902bf173_add_didsfollowed_and_followevents_table.py +93 -0
  300. rucio/db/sqla/migrate_repo/versions/7ec22226cdbf_new_replica_state_for_temporary_.py +73 -0
  301. rucio/db/sqla/migrate_repo/versions/810a41685bc1_added_columns_rse_transfer_limits.py +52 -0
  302. rucio/db/sqla/migrate_repo/versions/83f991c63a93_correct_rse_expression_length.py +45 -0
  303. rucio/db/sqla/migrate_repo/versions/8523998e2e76_increase_size_of_extended_attributes_.py +46 -0
  304. rucio/db/sqla/migrate_repo/versions/8ea9122275b1_adding_missing_function_based_indices.py +54 -0
  305. rucio/db/sqla/migrate_repo/versions/90f47792bb76_add_clob_payload_to_messages.py +48 -0
  306. rucio/db/sqla/migrate_repo/versions/914b8f02df38_new_table_for_lifetime_model_exceptions.py +70 -0
  307. rucio/db/sqla/migrate_repo/versions/94a5961ddbf2_add_estimator_columns.py +48 -0
  308. rucio/db/sqla/migrate_repo/versions/9a1b149a2044_add_saml_identity_type.py +95 -0
  309. rucio/db/sqla/migrate_repo/versions/9a45bc4ea66d_add_vp_table.py +55 -0
  310. rucio/db/sqla/migrate_repo/versions/9eb936a81eb1_true_is_true.py +74 -0
  311. rucio/db/sqla/migrate_repo/versions/a118956323f8_added_vo_table_and_vo_col_to_rse.py +78 -0
  312. rucio/db/sqla/migrate_repo/versions/a193a275255c_add_status_column_in_messages.py +49 -0
  313. rucio/db/sqla/migrate_repo/versions/a5f6f6e928a7_1_7_0.py +124 -0
  314. rucio/db/sqla/migrate_repo/versions/a616581ee47_added_columns_to_table_requests.py +60 -0
  315. rucio/db/sqla/migrate_repo/versions/a6eb23955c28_state_idx_non_functional.py +53 -0
  316. rucio/db/sqla/migrate_repo/versions/a74275a1ad30_added_global_quota_table.py +56 -0
  317. rucio/db/sqla/migrate_repo/versions/a93e4e47bda_heartbeats.py +67 -0
  318. rucio/db/sqla/migrate_repo/versions/ae2a56fcc89_added_comment_column_to_rules.py +50 -0
  319. rucio/db/sqla/migrate_repo/versions/b4293a99f344_added_column_identity_to_table_tokens.py +46 -0
  320. rucio/db/sqla/migrate_repo/versions/b7d287de34fd_removal_of_replicastate_source.py +92 -0
  321. rucio/db/sqla/migrate_repo/versions/b818052fa670_add_index_to_quarantined_replicas.py +42 -0
  322. rucio/db/sqla/migrate_repo/versions/b8caac94d7f0_add_comments_column_for_subscriptions_.py +46 -0
  323. rucio/db/sqla/migrate_repo/versions/b96a1c7e1cc4_new_bad_pfns_table_and_bad_replicas_.py +147 -0
  324. rucio/db/sqla/migrate_repo/versions/bb695f45c04_extend_request_state.py +78 -0
  325. rucio/db/sqla/migrate_repo/versions/bc68e9946deb_add_staging_timestamps_to_request.py +53 -0
  326. rucio/db/sqla/migrate_repo/versions/bf3baa1c1474_correct_pk_and_idx_for_history_tables.py +74 -0
  327. rucio/db/sqla/migrate_repo/versions/c0937668555f_add_qos_policy_map_table.py +56 -0
  328. rucio/db/sqla/migrate_repo/versions/c129ccdb2d5_add_lumiblocknr_to_dids.py +46 -0
  329. rucio/db/sqla/migrate_repo/versions/ccdbcd48206e_add_did_type_column_index_on_did_meta_.py +68 -0
  330. rucio/db/sqla/migrate_repo/versions/cebad904c4dd_new_payload_column_for_heartbeats.py +48 -0
  331. rucio/db/sqla/migrate_repo/versions/d1189a09c6e0_oauth2_0_and_jwt_feature_support_adding_.py +149 -0
  332. rucio/db/sqla/migrate_repo/versions/d23453595260_extend_request_state_for_preparer.py +106 -0
  333. rucio/db/sqla/migrate_repo/versions/d6dceb1de2d_added_purge_column_to_rules.py +47 -0
  334. rucio/db/sqla/migrate_repo/versions/d6e2c3b2cf26_remove_third_party_copy_column_from_rse.py +45 -0
  335. rucio/db/sqla/migrate_repo/versions/d91002c5841_new_account_limits_table.py +105 -0
  336. rucio/db/sqla/migrate_repo/versions/e138c364ebd0_extending_columns_for_filter_and_.py +52 -0
  337. rucio/db/sqla/migrate_repo/versions/e59300c8b179_support_for_archive.py +106 -0
  338. rucio/db/sqla/migrate_repo/versions/f1b14a8c2ac1_postgres_use_check_constraints.py +30 -0
  339. rucio/db/sqla/migrate_repo/versions/f41ffe206f37_oracle_global_temporary_tables.py +75 -0
  340. rucio/db/sqla/migrate_repo/versions/f85a2962b021_adding_transfertool_column_to_requests_.py +49 -0
  341. rucio/db/sqla/migrate_repo/versions/fa7a7d78b602_increase_refresh_token_size.py +45 -0
  342. rucio/db/sqla/migrate_repo/versions/fb28a95fe288_add_replicas_rse_id_tombstone_idx.py +38 -0
  343. rucio/db/sqla/migrate_repo/versions/fe1a65b176c9_set_third_party_copy_read_and_write_.py +44 -0
  344. rucio/db/sqla/migrate_repo/versions/fe8ea2fa9788_added_third_party_copy_column_to_rse_.py +46 -0
  345. rucio/db/sqla/models.py +1834 -0
  346. rucio/db/sqla/sautils.py +48 -0
  347. rucio/db/sqla/session.py +470 -0
  348. rucio/db/sqla/types.py +207 -0
  349. rucio/db/sqla/util.py +521 -0
  350. rucio/rse/__init__.py +97 -0
  351. rucio/rse/protocols/__init__.py +14 -0
  352. rucio/rse/protocols/cache.py +123 -0
  353. rucio/rse/protocols/dummy.py +112 -0
  354. rucio/rse/protocols/gfal.py +701 -0
  355. rucio/rse/protocols/globus.py +243 -0
  356. rucio/rse/protocols/gsiftp.py +93 -0
  357. rucio/rse/protocols/http_cache.py +83 -0
  358. rucio/rse/protocols/mock.py +124 -0
  359. rucio/rse/protocols/ngarc.py +210 -0
  360. rucio/rse/protocols/posix.py +251 -0
  361. rucio/rse/protocols/protocol.py +530 -0
  362. rucio/rse/protocols/rclone.py +365 -0
  363. rucio/rse/protocols/rfio.py +137 -0
  364. rucio/rse/protocols/srm.py +339 -0
  365. rucio/rse/protocols/ssh.py +414 -0
  366. rucio/rse/protocols/storm.py +207 -0
  367. rucio/rse/protocols/webdav.py +547 -0
  368. rucio/rse/protocols/xrootd.py +295 -0
  369. rucio/rse/rsemanager.py +752 -0
  370. rucio/tests/__init__.py +14 -0
  371. rucio/tests/common.py +244 -0
  372. rucio/tests/common_server.py +132 -0
  373. rucio/transfertool/__init__.py +14 -0
  374. rucio/transfertool/fts3.py +1484 -0
  375. rucio/transfertool/globus.py +200 -0
  376. rucio/transfertool/globus_library.py +182 -0
  377. rucio/transfertool/mock.py +81 -0
  378. rucio/transfertool/transfertool.py +212 -0
  379. rucio/vcsversion.py +11 -0
  380. rucio/version.py +46 -0
  381. rucio/web/__init__.py +14 -0
  382. rucio/web/rest/__init__.py +14 -0
  383. rucio/web/rest/flaskapi/__init__.py +14 -0
  384. rucio/web/rest/flaskapi/authenticated_bp.py +28 -0
  385. rucio/web/rest/flaskapi/v1/__init__.py +14 -0
  386. rucio/web/rest/flaskapi/v1/accountlimits.py +234 -0
  387. rucio/web/rest/flaskapi/v1/accounts.py +1088 -0
  388. rucio/web/rest/flaskapi/v1/archives.py +100 -0
  389. rucio/web/rest/flaskapi/v1/auth.py +1642 -0
  390. rucio/web/rest/flaskapi/v1/common.py +385 -0
  391. rucio/web/rest/flaskapi/v1/config.py +305 -0
  392. rucio/web/rest/flaskapi/v1/credentials.py +213 -0
  393. rucio/web/rest/flaskapi/v1/dids.py +2204 -0
  394. rucio/web/rest/flaskapi/v1/dirac.py +116 -0
  395. rucio/web/rest/flaskapi/v1/export.py +77 -0
  396. rucio/web/rest/flaskapi/v1/heartbeats.py +129 -0
  397. rucio/web/rest/flaskapi/v1/identities.py +263 -0
  398. rucio/web/rest/flaskapi/v1/import.py +133 -0
  399. rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +315 -0
  400. rucio/web/rest/flaskapi/v1/locks.py +360 -0
  401. rucio/web/rest/flaskapi/v1/main.py +83 -0
  402. rucio/web/rest/flaskapi/v1/meta.py +226 -0
  403. rucio/web/rest/flaskapi/v1/metrics.py +37 -0
  404. rucio/web/rest/flaskapi/v1/nongrid_traces.py +97 -0
  405. rucio/web/rest/flaskapi/v1/ping.py +89 -0
  406. rucio/web/rest/flaskapi/v1/redirect.py +366 -0
  407. rucio/web/rest/flaskapi/v1/replicas.py +1866 -0
  408. rucio/web/rest/flaskapi/v1/requests.py +841 -0
  409. rucio/web/rest/flaskapi/v1/rses.py +2204 -0
  410. rucio/web/rest/flaskapi/v1/rules.py +824 -0
  411. rucio/web/rest/flaskapi/v1/scopes.py +161 -0
  412. rucio/web/rest/flaskapi/v1/subscriptions.py +646 -0
  413. rucio/web/rest/flaskapi/v1/templates/auth_crash.html +80 -0
  414. rucio/web/rest/flaskapi/v1/templates/auth_granted.html +82 -0
  415. rucio/web/rest/flaskapi/v1/tmp_dids.py +115 -0
  416. rucio/web/rest/flaskapi/v1/traces.py +100 -0
  417. rucio/web/rest/flaskapi/v1/vos.py +280 -0
  418. rucio/web/rest/main.py +19 -0
  419. rucio/web/rest/metrics.py +28 -0
  420. rucio-32.8.6.data/data/rucio/etc/alembic.ini.template +71 -0
  421. rucio-32.8.6.data/data/rucio/etc/alembic_offline.ini.template +74 -0
  422. rucio-32.8.6.data/data/rucio/etc/globus-config.yml.template +5 -0
  423. rucio-32.8.6.data/data/rucio/etc/ldap.cfg.template +30 -0
  424. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_approval_request.tmpl +38 -0
  425. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +4 -0
  426. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_approved_user.tmpl +17 -0
  427. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +6 -0
  428. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_denied_user.tmpl +17 -0
  429. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +19 -0
  430. rucio-32.8.6.data/data/rucio/etc/rse-accounts.cfg.template +25 -0
  431. rucio-32.8.6.data/data/rucio/etc/rucio.cfg.atlas.client.template +42 -0
  432. rucio-32.8.6.data/data/rucio/etc/rucio.cfg.template +257 -0
  433. rucio-32.8.6.data/data/rucio/etc/rucio_multi_vo.cfg.template +234 -0
  434. rucio-32.8.6.data/data/rucio/requirements.txt +55 -0
  435. rucio-32.8.6.data/data/rucio/tools/bootstrap.py +34 -0
  436. rucio-32.8.6.data/data/rucio/tools/merge_rucio_configs.py +147 -0
  437. rucio-32.8.6.data/data/rucio/tools/reset_database.py +40 -0
  438. rucio-32.8.6.data/scripts/rucio +2540 -0
  439. rucio-32.8.6.data/scripts/rucio-abacus-account +75 -0
  440. rucio-32.8.6.data/scripts/rucio-abacus-collection-replica +47 -0
  441. rucio-32.8.6.data/scripts/rucio-abacus-rse +79 -0
  442. rucio-32.8.6.data/scripts/rucio-admin +2434 -0
  443. rucio-32.8.6.data/scripts/rucio-atropos +61 -0
  444. rucio-32.8.6.data/scripts/rucio-auditor +199 -0
  445. rucio-32.8.6.data/scripts/rucio-automatix +51 -0
  446. rucio-32.8.6.data/scripts/rucio-bb8 +58 -0
  447. rucio-32.8.6.data/scripts/rucio-c3po +86 -0
  448. rucio-32.8.6.data/scripts/rucio-cache-client +135 -0
  449. rucio-32.8.6.data/scripts/rucio-cache-consumer +43 -0
  450. rucio-32.8.6.data/scripts/rucio-conveyor-finisher +59 -0
  451. rucio-32.8.6.data/scripts/rucio-conveyor-poller +67 -0
  452. rucio-32.8.6.data/scripts/rucio-conveyor-preparer +38 -0
  453. rucio-32.8.6.data/scripts/rucio-conveyor-receiver +44 -0
  454. rucio-32.8.6.data/scripts/rucio-conveyor-stager +77 -0
  455. rucio-32.8.6.data/scripts/rucio-conveyor-submitter +140 -0
  456. rucio-32.8.6.data/scripts/rucio-conveyor-throttler +105 -0
  457. rucio-32.8.6.data/scripts/rucio-dark-reaper +54 -0
  458. rucio-32.8.6.data/scripts/rucio-dumper +159 -0
  459. rucio-32.8.6.data/scripts/rucio-follower +45 -0
  460. rucio-32.8.6.data/scripts/rucio-hermes +55 -0
  461. rucio-32.8.6.data/scripts/rucio-judge-cleaner +90 -0
  462. rucio-32.8.6.data/scripts/rucio-judge-evaluator +138 -0
  463. rucio-32.8.6.data/scripts/rucio-judge-injector +45 -0
  464. rucio-32.8.6.data/scripts/rucio-judge-repairer +45 -0
  465. rucio-32.8.6.data/scripts/rucio-kronos +45 -0
  466. rucio-32.8.6.data/scripts/rucio-light-reaper +53 -0
  467. rucio-32.8.6.data/scripts/rucio-minos +54 -0
  468. rucio-32.8.6.data/scripts/rucio-minos-temporary-expiration +51 -0
  469. rucio-32.8.6.data/scripts/rucio-necromancer +121 -0
  470. rucio-32.8.6.data/scripts/rucio-oauth-manager +64 -0
  471. rucio-32.8.6.data/scripts/rucio-reaper +84 -0
  472. rucio-32.8.6.data/scripts/rucio-replica-recoverer +249 -0
  473. rucio-32.8.6.data/scripts/rucio-storage-consistency-actions +75 -0
  474. rucio-32.8.6.data/scripts/rucio-transmogrifier +78 -0
  475. rucio-32.8.6.data/scripts/rucio-undertaker +77 -0
  476. rucio-32.8.6.dist-info/METADATA +83 -0
  477. rucio-32.8.6.dist-info/RECORD +481 -0
  478. rucio-32.8.6.dist-info/WHEEL +5 -0
  479. rucio-32.8.6.dist-info/licenses/AUTHORS.rst +94 -0
  480. rucio-32.8.6.dist-info/licenses/LICENSE +201 -0
  481. rucio-32.8.6.dist-info/top_level.txt +1 -0
rucio/core/rule.py ADDED
@@ -0,0 +1,3419 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright European Organization for Nuclear Research (CERN) since 2012
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import json
17
+ import logging
18
+ from configparser import NoOptionError
19
+ from copy import deepcopy
20
+ from datetime import datetime, timedelta
21
+ from os import path
22
+ from re import match
23
+ from string import Template
24
+ from typing import Any, Optional
25
+ from typing import TYPE_CHECKING
26
+
27
+ from dogpile.cache.api import NO_VALUE
28
+ from sqlalchemy import select, update
29
+ from sqlalchemy.exc import IntegrityError, StatementError
30
+ from sqlalchemy.orm.exc import NoResultFound
31
+ from sqlalchemy.sql import func
32
+ from sqlalchemy.sql.expression import and_, or_, true, null, tuple_, false
33
+
34
+ import rucio.core.did
35
+ import rucio.core.lock # import get_replica_locks, get_files_and_replica_locks_of_dataset
36
+ import rucio.core.replica # import get_and_lock_file_replicas, get_and_lock_file_replicas_for_dataset
37
+ from rucio.common.cache import make_region_memcached
38
+ from rucio.common.config import config_get
39
+ from rucio.common.exception import (InvalidRSEExpression, InvalidReplicationRule, InsufficientAccountLimit,
40
+ DataIdentifierNotFound, RuleNotFound, InputValidationError, RSEOverQuota,
41
+ ReplicationRuleCreationTemporaryFailed, InsufficientTargetRSEs, RucioException,
42
+ InvalidRuleWeight, StagingAreaRuleRequiresLifetime, DuplicateRule,
43
+ InvalidObject, RSEWriteBlocked, RuleReplaceFailed, RequestNotFound,
44
+ ManualRuleApprovalBlocked, UnsupportedOperation, UndefinedPolicy, InvalidValueForKey,
45
+ InvalidSourceReplicaExpression)
46
+ from rucio.common.policy import policy_filter, get_scratchdisk_lifetime
47
+ from rucio.common.schema import validate_schema
48
+ from rucio.common.types import InternalScope, InternalAccount
49
+ from rucio.common.utils import str_to_date, sizefmt, chunks
50
+ from rucio.core import account_counter, rse_counter, request as request_core, transfer as transfer_core
51
+ from rucio.core.account import get_account
52
+ from rucio.core.account import has_account_attribute
53
+ from rucio.core.lifetime_exception import define_eol
54
+ from rucio.core.message import add_message
55
+ from rucio.core.monitor import MetricManager
56
+ from rucio.core.rse import get_rse_name, list_rse_attributes, get_rse, get_rse_usage
57
+ from rucio.core.rse_expression_parser import parse_expression
58
+ from rucio.core.rse_selector import RSESelector
59
+ from rucio.core.rule_grouping import apply_rule_grouping, repair_stuck_locks_and_apply_rule_grouping, create_transfer_dict, apply_rule
60
+ from rucio.db.sqla import models, filter_thread_work
61
+ from rucio.db.sqla.constants import (LockState, ReplicaState, RuleState, RuleGrouping,
62
+ DIDAvailability, DIDReEvaluation, DIDType, BadFilesStatus,
63
+ RequestType, RuleNotification, OBSOLETE, RSEType)
64
+ from rucio.db.sqla.session import read_session, transactional_session, stream_session
65
+
66
+ if TYPE_CHECKING:
67
+ from sqlalchemy.orm import Session
68
+
69
+
70
+ REGION = make_region_memcached(expiration_time=900)
71
+ METRICS = MetricManager(module=__name__)
72
+
73
+
74
+ @transactional_session
75
+ def add_rule(dids, account, copies, rse_expression, grouping, weight, lifetime, locked, subscription_id,
76
+ source_replica_expression=None, activity='User Subscriptions', notify=None, purge_replicas=False,
77
+ ignore_availability=False, comment=None, ask_approval=False, asynchronous=False, ignore_account_limit=False,
78
+ priority=3, delay_injection=None, split_container=False, meta=None, *, session: "Session", logger=logging.log):
79
+ """
80
+ Adds a replication rule for every did in dids
81
+
82
+ :param dids: List of data identifiers.
83
+ :param account: Account issuing the rule.
84
+ :param copies: The number of replicas.
85
+ :param rse_expression: RSE expression which gets resolved into a list of rses.
86
+ :param grouping: ALL - All files will be replicated to the same RSE.
87
+ DATASET - All files in the same dataset will be replicated to the same RSE.
88
+ NONE - Files will be completely spread over all allowed RSEs without any grouping considerations at all.
89
+ :param weight: Weighting scheme to be used.
90
+ :param lifetime: The lifetime of the replication rule in seconds.
91
+ :param locked: If the rule is locked.
92
+ :param subscription_id: The subscription_id, if the rule is created by a subscription.
93
+ :param source_replica_expression: Only use replicas as source from this RSEs.
94
+ :param activity: Activity to be passed on to the conveyor.
95
+ :param notify: Notification setting of the rule ('Y', 'N', 'C'; None = 'N').
96
+ :param purge_replicas: Purge setting if a replica should be directly deleted after the rule is deleted.
97
+ :param ignore_availability: Option to ignore the availability of RSEs.
98
+ :param comment: Comment about the rule.
99
+ :param ask_approval: Ask for approval for this rule.
100
+ :param asynchronous: Create replication rule asynchronously by the judge-injector.
101
+ :param delay_injection: Create replication after 'delay' seconds. Implies asynchronous=True.
102
+ :param ignore_account_limit: Ignore quota and create the rule outside of the account limits.
103
+ :param priority: Priority of the rule and the transfers which should be submitted.
104
+ :param split_container: Should a container rule be split into individual dataset rules.
105
+ :param meta: Dictionary with metadata from the WFMS.
106
+ :param session: The database session in use.
107
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
108
+ :returns: A list of created replication rule ids.
109
+ :raises: InvalidReplicationRule, InsufficientAccountLimit, InvalidRSEExpression, DataIdentifierNotFound, ReplicationRuleCreationTemporaryFailed, InvalidRuleWeight,
110
+ StagingAreaRuleRequiresLifetime, DuplicateRule, RSEWriteBlocked, ScratchDiskLifetimeConflict, ManualRuleApprovalBlocked, RSEOverQuota
111
+ """
112
+ if copies <= 0:
113
+ raise InvalidValueForKey("The number of copies for a replication rule should be greater than 0.")
114
+
115
+ rule_ids = []
116
+
117
+ grouping = {'ALL': RuleGrouping.ALL, 'NONE': RuleGrouping.NONE}.get(grouping, RuleGrouping.DATASET)
118
+
119
+ with METRICS.timer('add_rule.total'):
120
+ # 1. Resolve the rse_expression into a list of RSE-ids
121
+ with METRICS.timer('add_rule.parse_rse_expression'):
122
+ vo = account.vo
123
+ if ignore_availability:
124
+ rses = parse_expression(rse_expression, filter_={'vo': vo}, session=session)
125
+ else:
126
+ rses = parse_expression(rse_expression, filter_={'vo': vo, 'availability_write': True}, session=session)
127
+
128
+ if lifetime is None: # Check if one of the rses is a staging area
129
+ if [rse for rse in rses if rse.get('staging_area', False)]:
130
+ raise StagingAreaRuleRequiresLifetime('Rules for a staging area must include a lifetime')
131
+
132
+ # Check SCRATCHDISK Policy
133
+ try:
134
+ lifetime = get_scratch_policy(account, rses, lifetime, session=session)
135
+ except UndefinedPolicy:
136
+ pass
137
+
138
+ # Auto-lock rules for TAPE rses
139
+ if not locked and lifetime is None:
140
+ if [rse for rse in rses if rse.get('rse_type', RSEType.DISK) == RSEType.TAPE]:
141
+ locked = True
142
+
143
+ # Block manual approval if RSE does not allow it
144
+ if ask_approval:
145
+ for rse in rses:
146
+ if list_rse_attributes(rse_id=rse['id'], session=session).get('block_manual_approval', False):
147
+ raise ManualRuleApprovalBlocked()
148
+
149
+ if source_replica_expression:
150
+ try:
151
+ source_rses = parse_expression(source_replica_expression, filter_={'vo': vo}, session=session)
152
+ except InvalidRSEExpression:
153
+ raise InvalidSourceReplicaExpression
154
+ else:
155
+ source_rses = []
156
+
157
+ # 2. Create the rse selector
158
+ with METRICS.timer('add_rule.create_rse_selector'):
159
+ rseselector = RSESelector(account=account, rses=rses, weight=weight, copies=copies, ignore_account_limit=ask_approval or ignore_account_limit, session=session)
160
+
161
+ expires_at = datetime.utcnow() + timedelta(seconds=lifetime) if lifetime is not None else None
162
+
163
+ notify = {'Y': RuleNotification.YES, 'C': RuleNotification.CLOSE, 'P': RuleNotification.PROGRESS}.get(notify, RuleNotification.NO)
164
+
165
+ for elem in dids:
166
+ # 3. Get the did
167
+ with METRICS.timer('add_rule.get_did'):
168
+ try:
169
+ did = session.query(models.DataIdentifier).filter(models.DataIdentifier.scope == elem['scope'],
170
+ models.DataIdentifier.name == elem['name']).one()
171
+ except NoResultFound:
172
+ raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (elem['scope'], elem['name']))
173
+ except TypeError as error:
174
+ raise InvalidObject(error.args)
175
+
176
+ # 3.1 If the did is a constituent, relay the rule to the archive
177
+ if did.did_type == DIDType.FILE and did.constituent:
178
+ # Check if a single replica of this DID exists; Do not put rule on file if there are only replicas on TAPE
179
+ replica_cnt = session.query(models.RSEFileAssociation).join(models.RSE, models.RSEFileAssociation.rse_id == models.RSE.id)\
180
+ .filter(models.RSEFileAssociation.scope == did.scope,
181
+ models.RSEFileAssociation.name == did.name,
182
+ models.RSEFileAssociation.state == ReplicaState.AVAILABLE,
183
+ models.RSE.rse_type != RSEType.TAPE).count()
184
+ if replica_cnt == 0: # Put the rule on the archive
185
+ archive = session.query(models.ConstituentAssociation).join(models.RSEFileAssociation,
186
+ and_(models.ConstituentAssociation.scope == models.RSEFileAssociation.scope,
187
+ models.ConstituentAssociation.name == models.RSEFileAssociation.name))\
188
+ .filter(models.ConstituentAssociation.child_scope == did.scope,
189
+ models.ConstituentAssociation.child_name == did.name).first()
190
+ if archive is not None:
191
+ elem['name'] = archive.name
192
+ elem['scope'] = archive.scope
193
+ try:
194
+ did = session.query(models.DataIdentifier).filter(models.DataIdentifier.scope == elem['scope'],
195
+ models.DataIdentifier.name == elem['name']).one()
196
+ except NoResultFound:
197
+ raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (elem['scope'], elem['name']))
198
+ except TypeError as error:
199
+ raise InvalidObject(error.args)
200
+ else: # Put the rule on the constituent directly
201
+ pass
202
+
203
+ # 3.2 Get the lifetime
204
+ eol_at = define_eol(elem['scope'], elem['name'], rses, session=session)
205
+
206
+ # 4. Create the replication rule
207
+ with METRICS.timer('add_rule.create_rule'):
208
+ meta_json = None
209
+ if meta is not None:
210
+ try:
211
+ meta_json = json.dumps(meta)
212
+ except Exception:
213
+ meta_json = None
214
+
215
+ new_rule = models.ReplicationRule(account=account,
216
+ name=elem['name'],
217
+ scope=elem['scope'],
218
+ did_type=did.did_type,
219
+ copies=copies,
220
+ rse_expression=rse_expression,
221
+ locked=locked,
222
+ grouping=grouping,
223
+ expires_at=expires_at,
224
+ weight=weight,
225
+ source_replica_expression=source_replica_expression,
226
+ activity=activity,
227
+ subscription_id=subscription_id,
228
+ notification=notify,
229
+ purge_replicas=purge_replicas,
230
+ ignore_availability=ignore_availability,
231
+ comments=comment,
232
+ ignore_account_limit=ignore_account_limit,
233
+ priority=priority,
234
+ split_container=split_container,
235
+ meta=meta_json,
236
+ eol_at=eol_at)
237
+ try:
238
+ new_rule.save(session=session)
239
+ except IntegrityError as error:
240
+ if match('.*ORA-00001.*', str(error.args[0])) \
241
+ or match('.*IntegrityError.*UNIQUE constraint failed.*', str(error.args[0])) \
242
+ or match('.*1062.*Duplicate entry.*for key.*', str(error.args[0])) \
243
+ or match('.*IntegrityError.*duplicate key value violates unique constraint.*', error.args[0]) \
244
+ or match('.*UniqueViolation.*duplicate key value violates unique constraint.*', error.args[0]) \
245
+ or match('.*IntegrityError.*columns? .*not unique.*', error.args[0]):
246
+ raise DuplicateRule(error.args[0])
247
+ raise InvalidReplicationRule(error.args[0])
248
+ rule_ids.append(new_rule.id)
249
+
250
+ if ask_approval:
251
+ new_rule.state = RuleState.WAITING_APPROVAL
252
+ # Block manual approval for multi-rse rules
253
+ if len(rses) > 1:
254
+ raise InvalidReplicationRule('Ask approval is not allowed for rules with multiple RSEs')
255
+ if len(rses) == 1 and not did.is_open and did.bytes is not None and did.length is not None:
256
+ # This rule can be considered for auto-approval:
257
+ rse_attr = list_rse_attributes(rse_id=rses[0]['id'], session=session)
258
+ auto_approve = False
259
+ if 'auto_approve_bytes' in rse_attr and 'auto_approve_files' in rse_attr:
260
+ if did.bytes < int(rse_attr.get('auto_approve_bytes')) and did.length < int(rse_attr.get('auto_approve_bytes')):
261
+ auto_approve = True
262
+ elif did.bytes < int(rse_attr.get('auto_approve_bytes', -1)):
263
+ auto_approve = True
264
+ elif did.length < int(rse_attr.get('auto_approve_files', -1)):
265
+ auto_approve = True
266
+ if auto_approve:
267
+ logger(logging.DEBUG, "Auto approving rule %s", str(new_rule.id))
268
+ logger(logging.DEBUG, "Created rule %s for injection", str(new_rule.id))
269
+ approve_rule(rule_id=new_rule.id, notify_approvers=False, session=session)
270
+ continue
271
+ logger(logging.DEBUG, "Created rule %s in waiting for approval", str(new_rule.id))
272
+ __create_rule_approval_email(rule=new_rule, session=session)
273
+ continue
274
+
275
+ # Force ASYNC mode for large rules
276
+ if did.length is not None and (did.length * copies) >= 10000:
277
+ asynchronous = True
278
+ logger(logging.DEBUG, "Forced injection of rule %s", str(new_rule.id))
279
+
280
+ if asynchronous or delay_injection:
281
+ # TODO: asynchronous mode only available for closed dids (on the whole tree?)
282
+ new_rule.state = RuleState.INJECT
283
+ logger(logging.DEBUG, "Created rule %s for injection", str(new_rule.id))
284
+ if delay_injection:
285
+ new_rule.created_at = datetime.utcnow() + timedelta(seconds=delay_injection)
286
+ logger(logging.DEBUG, "Scheduled rule %s for injection on %s", str(new_rule.id), new_rule.created_at)
287
+ continue
288
+
289
+ # If Split Container is chosen, the rule will be processed ASYNC
290
+ if split_container and did.did_type == DIDType.CONTAINER:
291
+ new_rule.state = RuleState.INJECT
292
+ logger(logging.DEBUG, "Created rule %s for injection due to Split Container mode", str(new_rule.id))
293
+ continue
294
+
295
+ # 5. Apply the rule
296
+ with METRICS.timer('add_rule.apply_rule'):
297
+ try:
298
+ apply_rule(did, new_rule, [x['id'] for x in rses], [x['id'] for x in source_rses], rseselector, session=session)
299
+ except IntegrityError as error:
300
+ raise ReplicationRuleCreationTemporaryFailed(error.args[0])
301
+
302
+ if new_rule.locks_stuck_cnt > 0:
303
+ new_rule.state = RuleState.STUCK
304
+ new_rule.error = 'MissingSourceReplica'
305
+ if new_rule.grouping != RuleGrouping.NONE:
306
+ session.query(models.DatasetLock).filter_by(rule_id=new_rule.id).update({'state': LockState.STUCK})
307
+ elif new_rule.locks_replicating_cnt == 0:
308
+ new_rule.state = RuleState.OK
309
+ if new_rule.grouping != RuleGrouping.NONE:
310
+ session.query(models.DatasetLock).filter_by(rule_id=new_rule.id).update({'state': LockState.OK})
311
+ session.flush()
312
+ if new_rule.notification == RuleNotification.YES:
313
+ generate_email_for_rule_ok_notification(rule=new_rule, session=session)
314
+ generate_rule_notifications(rule=new_rule, replicating_locks_before=0, session=session)
315
+ else:
316
+ new_rule.state = RuleState.REPLICATING
317
+ if new_rule.grouping != RuleGrouping.NONE:
318
+ session.query(models.DatasetLock).filter_by(rule_id=new_rule.id).update({'state': LockState.REPLICATING})
319
+
320
+ # Add rule to History
321
+ insert_rule_history(rule=new_rule, recent=True, longterm=True, session=session)
322
+
323
+ logger(logging.INFO, "Created rule %s [%d/%d/%d] with new algorithm for did %s:%s in state %s", str(new_rule.id), new_rule.locks_ok_cnt,
324
+ new_rule.locks_replicating_cnt, new_rule.locks_stuck_cnt, new_rule.scope, new_rule.name, str(new_rule.state))
325
+
326
+ return rule_ids
327
+
328
+
329
+ @transactional_session
330
+ def add_rules(dids, rules, *, session: "Session", logger=logging.log):
331
+ """
332
+ Adds a list of replication rules to every did in dids
333
+
334
+ :params dids: List of data identifiers.
335
+ :param rules: List of dictionaries defining replication rules.
336
+ {account, copies, rse_expression, grouping, weight, lifetime, locked, subscription_id, source_replica_expression, activity, notifiy, purge_replicas}
337
+ :param session: The database session in use.
338
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
339
+ :returns: Dictionary (scope, name) with list of created rule ids
340
+ :raises: InvalidReplicationRule, InsufficientAccountLimit, InvalidRSEExpression, DataIdentifierNotFound, ReplicationRuleCreationTemporaryFailed, InvalidRuleWeight,
341
+ StagingAreaRuleRequiresLifetime, DuplicateRule, RSEWriteBlocked, ScratchDiskLifetimeConflict, ManualRuleApprovalBlocked
342
+ """
343
+ if any(r.get("copies", 1) <= 0 for r in rules):
344
+ raise InvalidValueForKey("The number of copies for a replication rule should be greater than 0.")
345
+
346
+ with METRICS.timer('add_rules.total'):
347
+ rule_ids = {}
348
+
349
+ # 1. Fetch the RSEs from the RSE expression to restrict further queries just on these RSEs
350
+ restrict_rses = []
351
+ all_source_rses = []
352
+ with METRICS.timer('add_rules.parse_rse_expressions'):
353
+ for rule in rules:
354
+ vo = rule['account'].vo
355
+ if rule.get('ignore_availability'):
356
+ restrict_rses.extend(parse_expression(rule['rse_expression'], filter_={'vo': vo}, session=session))
357
+ else:
358
+ restrict_rses.extend(parse_expression(rule['rse_expression'], filter_={'vo': vo, 'availability_write': True}, session=session))
359
+ restrict_rses = list(set([rse['id'] for rse in restrict_rses]))
360
+
361
+ for rule in rules:
362
+ if rule.get('source_replica_expression'):
363
+ vo = rule['account'].vo
364
+ all_source_rses.extend(parse_expression(rule.get('source_replica_expression'), filter_={'vo': vo}, session=session))
365
+ all_source_rses = list(set([rse['id'] for rse in all_source_rses]))
366
+
367
+ for elem in dids:
368
+ # 2. Get the did
369
+ with METRICS.timer('add_rules.get_did'):
370
+ try:
371
+ did = session.query(models.DataIdentifier).filter(
372
+ models.DataIdentifier.scope == elem['scope'],
373
+ models.DataIdentifier.name == elem['name']).one()
374
+ except NoResultFound:
375
+ raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (elem['scope'], elem['name']))
376
+ except TypeError as error:
377
+ raise InvalidObject(error.args)
378
+
379
+ # 2.1 If the did is a constituent, relay the rule to the archive
380
+ if did.did_type == DIDType.FILE and did.constituent: # Check if a single replica of this DID exists
381
+ replica_cnt = session.query(models.RSEFileAssociation).join(models.RSE, models.RSEFileAssociation.rse_id == models.RSE.id)\
382
+ .filter(models.RSEFileAssociation.scope == did.scope,
383
+ models.RSEFileAssociation.name == did.name,
384
+ models.RSEFileAssociation.state == ReplicaState.AVAILABLE,
385
+ models.RSE.rse_type != RSEType.TAPE).count()
386
+ if replica_cnt == 0: # Put the rule on the archive
387
+ archive = session.query(models.ConstituentAssociation).join(models.RSEFileAssociation,
388
+ and_(models.ConstituentAssociation.scope == models.RSEFileAssociation.scope,
389
+ models.ConstituentAssociation.name == models.RSEFileAssociation.name))\
390
+ .filter(models.ConstituentAssociation.child_scope == did.scope,
391
+ models.ConstituentAssociation.child_name == did.name).first()
392
+ if archive is not None:
393
+ elem['name'] = archive.name
394
+ elem['scope'] = archive.scope
395
+ try:
396
+ did = session.query(models.DataIdentifier).filter(models.DataIdentifier.scope == elem['scope'],
397
+ models.DataIdentifier.name == elem['name']).one()
398
+ except NoResultFound:
399
+ raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (elem['scope'], elem['name']))
400
+ except TypeError as error:
401
+ raise InvalidObject(error.args)
402
+ else: # Put the rule on the constituent directly
403
+ pass
404
+
405
+ rule_ids[(elem['scope'], elem['name'])] = []
406
+
407
+ # 3. Resolve the did into its contents
408
+ with METRICS.timer('add_rules.resolve_dids_to_locks_replicas'):
409
+ # Get all Replicas, not only the ones interesting for the rse_expression
410
+ datasetfiles, locks, replicas, source_replicas = __resolve_did_to_locks_and_replicas(did=did,
411
+ nowait=False,
412
+ restrict_rses=restrict_rses,
413
+ source_rses=all_source_rses,
414
+ session=session)
415
+
416
+ for rule in rules:
417
+ with METRICS.timer('add_rules.add_rule'):
418
+ # 4. Resolve the rse_expression into a list of RSE-ids
419
+ vo = rule['account'].vo
420
+ if rule.get('ignore_availability'):
421
+ rses = parse_expression(rule['rse_expression'], filter_={'vo': vo}, session=session)
422
+ else:
423
+ rses = parse_expression(rule['rse_expression'], filter_={'vo': vo, 'availability_write': True}, session=session)
424
+
425
+ if rule.get('lifetime', None) is None: # Check if one of the rses is a staging area
426
+ if [rse for rse in rses if rse.get('staging_area', False)]:
427
+ raise StagingAreaRuleRequiresLifetime()
428
+
429
+ # Check SCRATCHDISK Policy
430
+ try:
431
+ lifetime = get_scratch_policy(rule.get('account'), rses, rule.get('lifetime', None), session=session)
432
+ except UndefinedPolicy:
433
+ lifetime = rule.get('lifetime', None)
434
+
435
+ rule['lifetime'] = lifetime
436
+
437
+ # 4.5 Get the lifetime
438
+ eol_at = define_eol(did.scope, did.name, rses, session=session)
439
+
440
+ # Auto-lock rules for TAPE rses
441
+ if not rule.get('locked', False) and rule.get('lifetime', None) is None:
442
+ if [rse for rse in rses if rse.get('rse_type', RSEType.DISK) == RSEType.TAPE]:
443
+ rule['locked'] = True
444
+
445
+ # Block manual approval if RSE does not allow it
446
+ if rule.get('ask_approval', False):
447
+ for rse in rses:
448
+ if list_rse_attributes(rse_id=rse['id'], session=session).get('block_manual_approval', False):
449
+ raise ManualRuleApprovalBlocked()
450
+
451
+ if rule.get('source_replica_expression'):
452
+ source_rses = parse_expression(rule.get('source_replica_expression'), filter_={'vo': vo}, session=session)
453
+ else:
454
+ source_rses = []
455
+
456
+ # 5. Create the RSE selector
457
+ with METRICS.timer('add_rules.create_rse_selector'):
458
+ rseselector = RSESelector(account=rule['account'], rses=rses, weight=rule.get('weight'), copies=rule['copies'], ignore_account_limit=rule.get('ask_approval', False), session=session)
459
+
460
+ # 4. Create the replication rule
461
+ with METRICS.timer('add_rules.create_rule'):
462
+ grouping = {'ALL': RuleGrouping.ALL, 'NONE': RuleGrouping.NONE}.get(rule.get('grouping'), RuleGrouping.DATASET)
463
+
464
+ expires_at = datetime.utcnow() + timedelta(seconds=rule.get('lifetime')) if rule.get('lifetime') is not None else None
465
+
466
+ notify = {'Y': RuleNotification.YES, 'C': RuleNotification.CLOSE, 'P': RuleNotification.PROGRESS}.get(rule.get('notify'), RuleNotification.NO)
467
+
468
+ if rule.get('meta') is not None:
469
+ try:
470
+ meta = json.dumps(rule.get('meta'))
471
+ except Exception:
472
+ meta = None
473
+ else:
474
+ meta = None
475
+
476
+ new_rule = models.ReplicationRule(account=rule['account'],
477
+ name=did.name,
478
+ scope=did.scope,
479
+ did_type=did.did_type,
480
+ copies=rule['copies'],
481
+ rse_expression=rule['rse_expression'],
482
+ locked=rule.get('locked'),
483
+ grouping=grouping,
484
+ expires_at=expires_at,
485
+ weight=rule.get('weight'),
486
+ source_replica_expression=rule.get('source_replica_expression'),
487
+ activity=rule.get('activity'),
488
+ subscription_id=rule.get('subscription_id'),
489
+ notification=notify,
490
+ purge_replicas=rule.get('purge_replicas', False),
491
+ ignore_availability=rule.get('ignore_availability', False),
492
+ comments=rule.get('comment', None),
493
+ priority=rule.get('priority', 3),
494
+ split_container=rule.get('split_container', False),
495
+ meta=meta,
496
+ eol_at=eol_at)
497
+ try:
498
+ new_rule.save(session=session)
499
+ except IntegrityError as error:
500
+ if match('.*ORA-00001.*', str(error.args[0])):
501
+ raise DuplicateRule(error.args[0])
502
+ elif str(error.args[0]) == '(IntegrityError) UNIQUE constraint failed: rules.scope, rules.name, rules.account, rules.rse_expression, rules.copies':
503
+ raise DuplicateRule(error.args[0])
504
+ raise InvalidReplicationRule(error.args[0])
505
+
506
+ rule_ids[(did.scope, did.name)].append(new_rule.id)
507
+
508
+ if rule.get('ask_approval', False):
509
+ new_rule.state = RuleState.WAITING_APPROVAL
510
+ # Block manual approval for multi-rse rules
511
+ if len(rses) > 1:
512
+ raise InvalidReplicationRule('Ask approval is not allowed for rules with multiple RSEs')
513
+ if len(rses) == 1 and not did.is_open and did.bytes is not None and did.length is not None:
514
+ # This rule can be considered for auto-approval:
515
+ rse_attr = list_rse_attributes(rse_id=rses[0]['id'], session=session)
516
+ auto_approve = False
517
+ if 'auto_approve_bytes' in rse_attr and 'auto_approve_files' in rse_attr:
518
+ if did.bytes < int(rse_attr.get('auto_approve_bytes')) and did.length < int(rse_attr.get('auto_approve_bytes')):
519
+ auto_approve = True
520
+ elif did.bytes < int(rse_attr.get('auto_approve_bytes', -1)):
521
+ auto_approve = True
522
+ elif did.length < int(rse_attr.get('auto_approve_files', -1)):
523
+ auto_approve = True
524
+ if auto_approve:
525
+ logger(logging.DEBUG, "Auto approving rule %s", str(new_rule.id))
526
+ logger(logging.DEBUG, "Created rule %s for injection", str(new_rule.id))
527
+ approve_rule(rule_id=new_rule.id, notify_approvers=False, session=session)
528
+ continue
529
+ logger(logging.DEBUG, "Created rule %s in waiting for approval", str(new_rule.id))
530
+ __create_rule_approval_email(rule=new_rule, session=session)
531
+ continue
532
+
533
+ delay_injection = rule.get('delay_injection')
534
+ if rule.get('asynchronous', False) or delay_injection:
535
+ new_rule.state = RuleState.INJECT
536
+ logger(logging.DEBUG, "Created rule %s for injection", str(new_rule.id))
537
+ if delay_injection:
538
+ new_rule.created_at = datetime.utcnow() + timedelta(seconds=delay_injection)
539
+ logger(logging.DEBUG, "Scheduled rule %s for injection on %s", (str(new_rule.id), new_rule.created_at))
540
+ continue
541
+
542
+ if rule.get('split_container', False) and did.did_type == DIDType.CONTAINER:
543
+ new_rule.state = RuleState.INJECT
544
+ logger(logging.DEBUG, "Created rule %s for injection due to Split Container mode", str(new_rule.id))
545
+ continue
546
+
547
+ # 5. Apply the replication rule to create locks, replicas and transfers
548
+ with METRICS.timer('add_rules.create_locks_replicas_transfers'):
549
+ try:
550
+ __create_locks_replicas_transfers(datasetfiles=datasetfiles,
551
+ locks=locks,
552
+ replicas=replicas,
553
+ source_replicas=source_replicas,
554
+ rseselector=rseselector,
555
+ rule=new_rule,
556
+ preferred_rse_ids=[],
557
+ source_rses=[rse['id'] for rse in source_rses],
558
+ session=session)
559
+ except IntegrityError as error:
560
+ raise ReplicationRuleCreationTemporaryFailed(error.args[0])
561
+
562
+ if new_rule.locks_stuck_cnt > 0:
563
+ new_rule.state = RuleState.STUCK
564
+ new_rule.error = 'MissingSourceReplica'
565
+ if new_rule.grouping != RuleGrouping.NONE:
566
+ session.query(models.DatasetLock).filter_by(rule_id=new_rule.id).update({'state': LockState.STUCK})
567
+ elif new_rule.locks_replicating_cnt == 0:
568
+ new_rule.state = RuleState.OK
569
+ if new_rule.grouping != RuleGrouping.NONE:
570
+ session.query(models.DatasetLock).filter_by(rule_id=new_rule.id).update({'state': LockState.OK})
571
+ session.flush()
572
+ if new_rule.notification == RuleNotification.YES:
573
+ generate_email_for_rule_ok_notification(rule=new_rule, session=session)
574
+ generate_rule_notifications(rule=new_rule, replicating_locks_before=0, session=session)
575
+ else:
576
+ new_rule.state = RuleState.REPLICATING
577
+ if new_rule.grouping != RuleGrouping.NONE:
578
+ session.query(models.DatasetLock).filter_by(rule_id=new_rule.id).update({'state': LockState.REPLICATING})
579
+
580
+ # Add rule to History
581
+ insert_rule_history(rule=new_rule, recent=True, longterm=True, session=session)
582
+
583
+ logger(logging.INFO, "Created rule %s [%d/%d/%d] in state %s", str(new_rule.id), new_rule.locks_ok_cnt, new_rule.locks_replicating_cnt, new_rule.locks_stuck_cnt, str(new_rule.state))
584
+
585
+ return rule_ids
586
+
587
+
588
+ @transactional_session
589
+ def inject_rule(rule_id, *, session: "Session", logger=logging.log):
590
+ """
591
+ Inject a replication rule.
592
+
593
+ :param rule_id: The id of the rule to inject.
594
+ :param new_owner: The new owner of the rule.
595
+ :param session: The database session in use.
596
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
597
+ :raises: InvalidReplicationRule, InsufficientAccountLimit, InvalidRSEExpression, DataId, RSEOverQuota
598
+ """
599
+
600
+ try:
601
+ rule = session.query(models.ReplicationRule).filter(models.ReplicationRule.id == rule_id).with_for_update(nowait=True).one()
602
+ except NoResultFound:
603
+ raise RuleNotFound('No rule with the id %s found' % (rule_id))
604
+
605
+ # Check if rule will expire in the next 5 minutes:
606
+ if rule.child_rule_id is None and rule.expires_at is not None and rule.expires_at < datetime.utcnow() + timedelta(seconds=300):
607
+ logger(logging.INFO, 'Rule %s expiring soon, skipping', str(rule.id))
608
+ return
609
+
610
+ # Special R2D2 container handling
611
+ if (rule.did_type == DIDType.CONTAINER and '.r2d2_request.' in rule.name) or (rule.split_container and rule.did_type == DIDType.CONTAINER):
612
+ logger(logging.DEBUG, "Creating dataset rules for Split Container rule %s", str(rule.id))
613
+ # Get all child datasets and put rules on them
614
+ dids = [{'scope': dataset['scope'], 'name': dataset['name']} for dataset in rucio.core.did.list_child_datasets(scope=rule.scope, name=rule.name, session=session)]
615
+ # Remove duplicates from the list of dictionaries
616
+ dids = [dict(t) for t in {tuple(d.items()) for d in dids}]
617
+ # Remove dids which already have a similar rule
618
+ dids = [did for did in dids if session.query(models.ReplicationRule).filter_by(scope=did['scope'], name=did['name'], account=rule.account, rse_expression=rule.rse_expression).count() == 0]
619
+ if rule.expires_at:
620
+ lifetime = (rule.expires_at - datetime.utcnow()).days * 24 * 3600 + (rule.expires_at - datetime.utcnow()).seconds
621
+ else:
622
+ lifetime = None
623
+
624
+ notify = {RuleNotification.YES: 'Y', RuleNotification.CLOSE: 'C', RuleNotification.PROGRESS: 'P'}.get(rule.notification, 'N')
625
+
626
+ add_rule(dids=dids,
627
+ account=rule.account,
628
+ copies=rule.copies,
629
+ rse_expression=rule.rse_expression,
630
+ grouping='DATASET',
631
+ weight=None,
632
+ lifetime=lifetime,
633
+ locked=False,
634
+ subscription_id=None,
635
+ activity=rule.activity,
636
+ notify=notify,
637
+ comment=rule.comments,
638
+ asynchronous=True,
639
+ ignore_availability=rule.ignore_availability,
640
+ ignore_account_limit=True,
641
+ priority=rule.priority,
642
+ split_container=rule.split_container,
643
+ session=session)
644
+ rule.delete(session=session)
645
+ return
646
+
647
+ # 1. Resolve the rse_expression into a list of RSE-ids
648
+ with METRICS.timer('inject_rule.parse_rse_expression'):
649
+ vo = rule['account'].vo
650
+ if rule.ignore_availability:
651
+ rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
652
+ else:
653
+ rses = parse_expression(rule.rse_expression, filter_={'vo': vo, 'availability_write': True}, session=session)
654
+
655
+ if rule.source_replica_expression:
656
+ source_rses = parse_expression(rule.source_replica_expression, filter_={'vo': vo}, session=session)
657
+ else:
658
+ source_rses = []
659
+
660
+ # 2. Create the rse selector
661
+ with METRICS.timer('inject_rule.create_rse_selector'):
662
+ rseselector = RSESelector(account=rule['account'], rses=rses, weight=rule.weight, copies=rule.copies, ignore_account_limit=rule.ignore_account_limit, session=session)
663
+
664
+ # 3. Get the did
665
+ with METRICS.timer('inject_rule.get_did'):
666
+ try:
667
+ did = session.query(models.DataIdentifier).filter(models.DataIdentifier.scope == rule.scope,
668
+ models.DataIdentifier.name == rule.name).one()
669
+ except NoResultFound:
670
+ raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (rule.scope, rule.name))
671
+ except TypeError as error:
672
+ raise InvalidObject(error.args)
673
+
674
+ # 4. Apply the rule
675
+ with METRICS.timer('inject_rule.apply_rule'):
676
+ try:
677
+ apply_rule(did, rule, [x['id'] for x in rses], [x['id'] for x in source_rses], rseselector, session=session)
678
+ except IntegrityError as error:
679
+ raise ReplicationRuleCreationTemporaryFailed(error.args[0])
680
+
681
+ if rule.locks_stuck_cnt > 0:
682
+ rule.state = RuleState.STUCK
683
+ rule.error = 'MissingSourceReplica'
684
+ if rule.grouping != RuleGrouping.NONE:
685
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.STUCK})
686
+ elif rule.locks_replicating_cnt == 0:
687
+ rule.state = RuleState.OK
688
+ if rule.grouping != RuleGrouping.NONE:
689
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.OK})
690
+ session.flush()
691
+ if rule.notification == RuleNotification.YES:
692
+ generate_email_for_rule_ok_notification(rule=rule, session=session)
693
+ generate_rule_notifications(rule=rule, replicating_locks_before=0, session=session)
694
+ # Try to release potential parent rules
695
+ release_parent_rule(child_rule_id=rule.id, session=session)
696
+ else:
697
+ rule.state = RuleState.REPLICATING
698
+ if rule.grouping != RuleGrouping.NONE:
699
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.REPLICATING})
700
+
701
+ # Add rule to History
702
+ insert_rule_history(rule=rule, recent=True, longterm=True, session=session)
703
+
704
+ logger(logging.INFO, "Created rule %s [%d/%d/%d] with new algorithm for did %s:%s in state %s", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt, rule.scope, rule.name, str(rule.state))
705
+
706
+
707
+ @stream_session
708
+ def list_rules(filters={}, *, session: "Session"):
709
+ """
710
+ List replication rules.
711
+
712
+ :param filters: dictionary of attributes by which the results should be filtered.
713
+ :param session: The database session in use.
714
+ :raises: RucioException
715
+ """
716
+
717
+ stmt = select(
718
+ models.ReplicationRule,
719
+ models.DataIdentifier.bytes
720
+ ).join(
721
+ models.DataIdentifier,
722
+ and_(
723
+ models.ReplicationRule.scope == models.DataIdentifier.scope,
724
+ models.ReplicationRule.name == models.DataIdentifier.name
725
+ )
726
+ )
727
+ if filters:
728
+ for (key, value) in filters.items():
729
+ if key in ['account', 'scope']:
730
+ if '*' in value.internal:
731
+ value = value.internal.replace('*', '%')
732
+ stmt = stmt.where(getattr(models.ReplicationRule, key).like(value))
733
+ continue
734
+ # else fall through
735
+ elif key == 'created_before':
736
+ stmt = stmt.where(models.ReplicationRule.created_at <= str_to_date(value))
737
+ continue
738
+ elif key == 'created_after':
739
+ stmt = stmt.where(models.ReplicationRule.created_at >= str_to_date(value))
740
+ continue
741
+ elif key == 'updated_before':
742
+ stmt = stmt.where(models.ReplicationRule.updated_at <= str_to_date(value))
743
+ continue
744
+ elif key == 'updated_after':
745
+ stmt = stmt.where(models.ReplicationRule.updated_at >= str_to_date(value))
746
+ continue
747
+ elif key == 'state':
748
+ if isinstance(value, str):
749
+ value = RuleState(value)
750
+ else:
751
+ try:
752
+ value = RuleState[value]
753
+ except ValueError:
754
+ pass
755
+ elif key == 'did_type' and isinstance(value, str):
756
+ value = DIDType(value)
757
+ elif key == 'grouping' and isinstance(value, str):
758
+ value = RuleGrouping(value)
759
+ stmt = stmt.where(getattr(models.ReplicationRule, key) == value)
760
+
761
+ try:
762
+ for rule, data_identifier_bytes in session.execute(stmt).yield_per(5):
763
+ d = rule.to_dict()
764
+ d['bytes'] = data_identifier_bytes
765
+ yield d
766
+ except StatementError:
767
+ raise RucioException('Badly formatted input (IDs?)')
768
+
769
+
770
+ @stream_session
771
+ def list_rule_history(rule_id, *, session: "Session"):
772
+ """
773
+ List the rule history of a rule.
774
+
775
+ :param rule_id: The id of the rule.
776
+ :param session: The database session in use.
777
+ :raises: RucioException
778
+ """
779
+
780
+ query = session.query(models.ReplicationRuleHistoryRecent.updated_at,
781
+ models.ReplicationRuleHistoryRecent.state,
782
+ models.ReplicationRuleHistoryRecent.locks_ok_cnt,
783
+ models.ReplicationRuleHistoryRecent.locks_stuck_cnt,
784
+ models.ReplicationRuleHistoryRecent.locks_replicating_cnt).filter_by(id=rule_id).order_by(models.ReplicationRuleHistoryRecent.updated_at)
785
+
786
+ try:
787
+ for rule in query.yield_per(5):
788
+ yield {'updated_at': rule[0], 'state': rule[1], 'locks_ok_cnt': rule[2], 'locks_stuck_cnt': rule[3], 'locks_replicating_cnt': rule[4]}
789
+ except StatementError:
790
+ raise RucioException('Badly formatted input (IDs?)')
791
+
792
+
793
+ @stream_session
794
+ def list_rule_full_history(scope, name, *, session: "Session"):
795
+ """
796
+ List the rule history of a DID.
797
+
798
+ :param scope: The scope of the DID.
799
+ :param name: The name of the DID.
800
+ :param session: The database session in use.
801
+ :raises: RucioException
802
+ """
803
+
804
+ query = session.query(models.ReplicationRuleHistory.id,
805
+ models.ReplicationRuleHistory.created_at,
806
+ models.ReplicationRuleHistory.updated_at,
807
+ models.ReplicationRuleHistory.rse_expression,
808
+ models.ReplicationRuleHistory.state,
809
+ models.ReplicationRuleHistory.account,
810
+ models.ReplicationRuleHistory.locks_ok_cnt,
811
+ models.ReplicationRuleHistory.locks_stuck_cnt,
812
+ models.ReplicationRuleHistory.locks_replicating_cnt).\
813
+ with_hint(models.ReplicationRuleHistory, "INDEX(RULES_HISTORY_SCOPENAME_IDX)", 'oracle').\
814
+ filter(models.ReplicationRuleHistory.scope == scope, models.ReplicationRuleHistory.name == name).\
815
+ order_by(models.ReplicationRuleHistory.created_at, models.ReplicationRuleHistory.updated_at)
816
+
817
+ for rule in query.yield_per(5):
818
+ yield {'rule_id': rule[0], 'created_at': rule[1], 'updated_at': rule[2], 'rse_expression': rule[3], 'state': rule[4],
819
+ 'account': rule[5], 'locks_ok_cnt': rule[6], 'locks_stuck_cnt': rule[7], 'locks_replicating_cnt': rule[8]}
820
+
821
+
822
+ @stream_session
823
+ def list_associated_rules_for_file(scope, name, *, session: "Session"):
824
+ """
825
+ List replication rules a file is affected from.
826
+
827
+ :param scope: Scope of the file.
828
+ :param name: Name of the file.
829
+ :param session: The database session in use.
830
+ :raises: RucioException
831
+ """
832
+
833
+ rucio.core.did.get_did(scope=scope, name=name, session=session) # Check if the did acually exists
834
+ query = session.query(models.ReplicationRule).\
835
+ with_hint(models.ReplicaLock, "INDEX(LOCKS LOCKS_PK)", 'oracle').\
836
+ join(models.ReplicaLock, models.ReplicationRule.id == models.ReplicaLock.rule_id).\
837
+ filter(models.ReplicaLock.scope == scope, models.ReplicaLock.name == name).distinct()
838
+ try:
839
+ for rule in query.yield_per(5):
840
+ yield rule.to_dict()
841
+ except StatementError:
842
+ raise RucioException('Badly formatted input (IDs?)')
843
+
844
+
845
+ @transactional_session
846
+ def delete_rule(rule_id, purge_replicas=None, soft=False, delete_parent=False, nowait=False, *, session: "Session",
847
+ ignore_rule_lock=False):
848
+ """
849
+ Delete a replication rule.
850
+
851
+ :param rule_id: The rule to delete.
852
+ :param purge_replicas: Purge the replicas immediately.
853
+ :param soft: Only perform a soft deletion.
854
+ :param delete_parent: Delete rules even if they have a child_rule_id set.
855
+ :param nowait: Nowait parameter for the FOR UPDATE statement.
856
+ :param session: The database session in use.
857
+ :param ignore_rule_lock: Ignore any locks on the rule
858
+ :raises: RuleNotFound if no Rule can be found.
859
+ :raises: UnsupportedOperation if the Rule is locked.
860
+ """
861
+
862
+ with METRICS.timer('delete_rule.total'):
863
+ try:
864
+ rule = session.query(models.ReplicationRule)\
865
+ .filter(models.ReplicationRule.id == rule_id)\
866
+ .with_for_update(nowait=nowait).one()
867
+ except NoResultFound:
868
+ raise RuleNotFound('No rule with the id %s found' % rule_id)
869
+ if rule.locked and not ignore_rule_lock:
870
+ raise UnsupportedOperation('The replication rule is locked and has to be unlocked before it can be deleted.')
871
+
872
+ if rule.child_rule_id is not None and not delete_parent:
873
+ raise UnsupportedOperation('The replication rule has a child rule and thus cannot be deleted.')
874
+
875
+ if purge_replicas is not None:
876
+ rule.purge_replicas = purge_replicas
877
+
878
+ if soft:
879
+ if rule.expires_at:
880
+ rule.expires_at = min(datetime.utcnow() + timedelta(seconds=3600), rule.expires_at)
881
+ else:
882
+ rule.expires_at = datetime.utcnow() + timedelta(seconds=3600)
883
+ if rule.child_rule_id is not None and delete_parent:
884
+ rule.child_rule_id = None
885
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
886
+ return
887
+
888
+ locks = session.query(models.ReplicaLock)\
889
+ .filter(models.ReplicaLock.rule_id == rule_id)\
890
+ .with_for_update(nowait=nowait).yield_per(100)
891
+
892
+ # Remove locks, set tombstone if applicable
893
+ transfers_to_delete = [] # [{'scope': , 'name':, 'rse_id':}]
894
+ account_counter_decreases = {} # {'rse_id': [file_size, file_size, file_size]}
895
+
896
+ for lock in locks:
897
+ if __delete_lock_and_update_replica(lock=lock, purge_replicas=rule.purge_replicas,
898
+ nowait=nowait, session=session):
899
+ transfers_to_delete.append({'scope': lock.scope, 'name': lock.name, 'rse_id': lock.rse_id})
900
+ if lock.rse_id not in account_counter_decreases:
901
+ account_counter_decreases[lock.rse_id] = []
902
+ account_counter_decreases[lock.rse_id].append(lock.bytes)
903
+
904
+ # Delete the DatasetLocks
905
+ session.query(models.DatasetLock)\
906
+ .filter(models.DatasetLock.rule_id == rule_id)\
907
+ .delete(synchronize_session=False)
908
+
909
+ # Decrease account_counters
910
+ for rse_id in account_counter_decreases.keys():
911
+ account_counter.decrease(rse_id=rse_id, account=rule.account, files=len(account_counter_decreases[rse_id]),
912
+ bytes_=sum(account_counter_decreases[rse_id]), session=session)
913
+
914
+ # Try to release potential parent rules
915
+ release_parent_rule(child_rule_id=rule.id, remove_parent_expiration=True, session=session)
916
+
917
+ # Insert history
918
+ insert_rule_history(rule=rule, recent=False, longterm=True, session=session)
919
+
920
+ session.flush()
921
+ rule.delete(session=session)
922
+
923
+ for transfer in transfers_to_delete:
924
+ transfers_to_cancel = request_core.cancel_request_did(scope=transfer['scope'], name=transfer['name'],
925
+ dest_rse_id=transfer['rse_id'], session=session)
926
+ transfer_core.cancel_transfers(transfers_to_cancel)
927
+
928
+
929
+ @transactional_session
930
+ def repair_rule(rule_id, *, session: "Session", logger=logging.log):
931
+ """
932
+ Repair a STUCK replication rule.
933
+
934
+ :param rule_id: The rule to repair.
935
+ :param session: The database session in use.
936
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
937
+ """
938
+
939
+ # Rule error cases:
940
+ # (A) A rule get's an exception on rule-creation. This can only be the MissingSourceReplica exception.
941
+ # (B) A rule get's an error when re-evaluated: InvalidRSEExpression, InvalidRuleWeight, InsufficientTargetRSEs, RSEWriteBlocked
942
+ # InsufficientAccountLimit. The re-evaluation has to be done again and potential missing locks have to be
943
+ # created.
944
+ # (C) Transfers fail and mark locks (and the rule) as STUCK. All STUCK locks have to be repaired.
945
+ # (D) Files are declared as BAD.
946
+
947
+ # start_time = time.time()
948
+ try:
949
+ rule = session.query(models.ReplicationRule).filter(models.ReplicationRule.id == rule_id).with_for_update(nowait=True).one()
950
+ rule.updated_at = datetime.utcnow()
951
+
952
+ # Check if rule is longer than 2 weeks in STUCK
953
+ if rule.stuck_at is None:
954
+ rule.stuck_at = datetime.utcnow()
955
+ if rule.stuck_at < (datetime.utcnow() - timedelta(days=14)):
956
+ rule.state = RuleState.SUSPENDED
957
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
958
+ logger(logging.INFO, 'Replication rule %s has been SUSPENDED', rule_id)
959
+ return
960
+
961
+ # Evaluate the RSE expression to see if there is an alternative RSE anyway
962
+ try:
963
+ vo = rule.account.vo
964
+ rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
965
+ if rule.ignore_availability:
966
+ target_rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
967
+ else:
968
+ target_rses = parse_expression(rule.rse_expression, filter_={'vo': vo, 'availability_write': True}, session=session)
969
+ if rule.source_replica_expression:
970
+ source_rses = parse_expression(rule.source_replica_expression, filter_={'vo': vo}, session=session)
971
+ else:
972
+ source_rses = []
973
+ except (InvalidRSEExpression, RSEWriteBlocked) as error:
974
+ rule.state = RuleState.STUCK
975
+ rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
976
+ rule.save(session=session)
977
+ # Insert rule history
978
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
979
+ # Try to update the DatasetLocks
980
+ if rule.grouping != RuleGrouping.NONE:
981
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.STUCK})
982
+ logger(logging.DEBUG, '%s while repairing rule %s', str(error), rule_id)
983
+ return
984
+
985
+ # Create the RSESelector
986
+ try:
987
+ rseselector = RSESelector(account=rule.account,
988
+ rses=target_rses,
989
+ weight=rule.weight,
990
+ copies=rule.copies,
991
+ ignore_account_limit=rule.ignore_account_limit,
992
+ session=session)
993
+ except (InvalidRuleWeight, InsufficientTargetRSEs, InsufficientAccountLimit) as error:
994
+ rule.state = RuleState.STUCK
995
+ rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
996
+ rule.save(session=session)
997
+ # Insert rule history
998
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
999
+ # Try to update the DatasetLocks
1000
+ if rule.grouping != RuleGrouping.NONE:
1001
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.STUCK})
1002
+ logger(logging.DEBUG, '%s while repairing rule %s', type(error).__name__, rule_id)
1003
+ return
1004
+
1005
+ # Reset the counters
1006
+ logger(logging.DEBUG, "Resetting counters for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
1007
+ rule.locks_ok_cnt = 0
1008
+ rule.locks_replicating_cnt = 0
1009
+ rule.locks_stuck_cnt = 0
1010
+ rule_counts = session.query(models.ReplicaLock.state, func.count(models.ReplicaLock.state)).filter(models.ReplicaLock.rule_id == rule.id).group_by(models.ReplicaLock.state).all()
1011
+ for count in rule_counts:
1012
+ if count[0] == LockState.OK:
1013
+ rule.locks_ok_cnt = count[1]
1014
+ elif count[0] == LockState.REPLICATING:
1015
+ rule.locks_replicating_cnt = count[1]
1016
+ elif count[0] == LockState.STUCK:
1017
+ rule.locks_stuck_cnt = count[1]
1018
+ logger(logging.DEBUG, "Finished resetting counters for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
1019
+
1020
+ # Get the did
1021
+ did = session.query(models.DataIdentifier).filter(models.DataIdentifier.scope == rule.scope,
1022
+ models.DataIdentifier.name == rule.name).one()
1023
+
1024
+ # Detect if there is something wrong with the dataset and
1025
+ # make the decisison on soft or hard repair.
1026
+ hard_repair = False
1027
+ if did.did_type != DIDType.FILE:
1028
+ nr_files = rucio.core.did.get_did(scope=rule.scope, name=rule.name, dynamic_depth=DIDType.FILE, session=session)['length']
1029
+ else:
1030
+ nr_files = 1
1031
+ if nr_files * rule.copies != (rule.locks_ok_cnt + rule.locks_stuck_cnt + rule.locks_replicating_cnt):
1032
+ hard_repair = True
1033
+ logger(logging.DEBUG, 'Repairing rule %s in HARD mode', str(rule.id))
1034
+ elif rule.copies > 1 and rule.grouping == RuleGrouping.NONE:
1035
+ hard_repair = True
1036
+ logger(logging.DEBUG, 'Repairing rule %s in HARD mode', str(rule.id))
1037
+
1038
+ # Resolve the did to its contents
1039
+ datasetfiles, locks, replicas, source_replicas = __resolve_did_to_locks_and_replicas(did=did,
1040
+ nowait=True,
1041
+ restrict_rses=[rse['id'] for rse in rses],
1042
+ source_rses=[rse['id'] for rse in source_rses],
1043
+ only_stuck=not hard_repair,
1044
+ session=session)
1045
+
1046
+ session.flush()
1047
+
1048
+ # 1. Try to find missing locks and create them based on grouping
1049
+ if did.did_type != DIDType.FILE and hard_repair:
1050
+ try:
1051
+ __find_missing_locks_and_create_them(datasetfiles=datasetfiles,
1052
+ locks=locks,
1053
+ replicas=replicas,
1054
+ source_replicas=source_replicas,
1055
+ rseselector=rseselector,
1056
+ rule=rule,
1057
+ source_rses=[rse['id'] for rse in source_rses],
1058
+ session=session)
1059
+ except (InsufficientAccountLimit, InsufficientTargetRSEs) as error:
1060
+ rule.state = RuleState.STUCK
1061
+ rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
1062
+ rule.save(session=session)
1063
+ # Insert rule history
1064
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1065
+ # Try to update the DatasetLocks
1066
+ if rule.grouping != RuleGrouping.NONE:
1067
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.STUCK})
1068
+ logger(logging.DEBUG, '%s while repairing rule %s', type(error).__name__, rule_id)
1069
+ return
1070
+
1071
+ session.flush()
1072
+
1073
+ # 2. Try to find surplus locks and remove them
1074
+ if hard_repair:
1075
+ __find_surplus_locks_and_remove_them(datasetfiles=datasetfiles,
1076
+ locks=locks,
1077
+ replicas=replicas,
1078
+ source_replicas=source_replicas,
1079
+ rseselector=rseselector,
1080
+ rule=rule,
1081
+ source_rses=[rse['id'] for rse in source_rses],
1082
+ session=session)
1083
+
1084
+ session.flush()
1085
+
1086
+ # 3. Try to find STUCK locks and repair them based on grouping
1087
+ try:
1088
+ __find_stuck_locks_and_repair_them(datasetfiles=datasetfiles,
1089
+ locks=locks,
1090
+ replicas=replicas,
1091
+ source_replicas=source_replicas,
1092
+ rseselector=rseselector,
1093
+ rule=rule,
1094
+ source_rses=[rse['id'] for rse in source_rses],
1095
+ session=session)
1096
+ except (InsufficientAccountLimit, InsufficientTargetRSEs) as error:
1097
+ rule.state = RuleState.STUCK
1098
+ rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
1099
+ rule.save(session=session)
1100
+ # Insert rule history
1101
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1102
+ # Try to update the DatasetLocks
1103
+ if rule.grouping != RuleGrouping.NONE:
1104
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.STUCK})
1105
+ logger(logging.DEBUG, '%s while repairing rule %s', type(error).__name__, rule_id)
1106
+ return
1107
+
1108
+ # Delete Datasetlocks which are not relevant anymore
1109
+ validated_datasetlock_rse_ids = [rse_id[0] for rse_id in session.query(models.ReplicaLock.rse_id).filter(models.ReplicaLock.rule_id == rule.id).group_by(models.ReplicaLock.rse_id).all()]
1110
+ dataset_locks = session.query(models.DatasetLock).filter_by(rule_id=rule.id).all()
1111
+ for dataset_lock in dataset_locks:
1112
+ if dataset_lock.rse_id not in validated_datasetlock_rse_ids:
1113
+ dataset_lock.delete(session=session)
1114
+
1115
+ if rule.locks_stuck_cnt != 0:
1116
+ logger(logging.INFO, 'Rule %s [%d/%d/%d] state=STUCK', str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
1117
+ rule.state = RuleState.STUCK
1118
+ # Insert rule history
1119
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1120
+ # Try to update the DatasetLocks
1121
+ if rule.grouping != RuleGrouping.NONE:
1122
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.STUCK})
1123
+ # TODO: Increase some kind of Stuck Counter here, The rule should at some point be SUSPENDED
1124
+ return
1125
+
1126
+ rule.stuck_at = None
1127
+
1128
+ if rule.locks_replicating_cnt > 0:
1129
+ logger(logging.INFO, 'Rule %s [%d/%d/%d] state=REPLICATING', str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
1130
+ rule.state = RuleState.REPLICATING
1131
+ rule.error = None
1132
+ # Insert rule history
1133
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1134
+ # Try to update the DatasetLocks
1135
+ if rule.grouping != RuleGrouping.NONE:
1136
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.REPLICATING})
1137
+ return
1138
+
1139
+ rule.state = RuleState.OK
1140
+ rule.error = None
1141
+ # Insert rule history
1142
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1143
+ logger(logging.INFO, 'Rule %s [%d/%d/%d] state=OK', str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
1144
+
1145
+ if rule.grouping != RuleGrouping.NONE:
1146
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.OK})
1147
+ session.flush()
1148
+ if rule.notification == RuleNotification.YES:
1149
+ generate_email_for_rule_ok_notification(rule=rule, session=session)
1150
+ generate_rule_notifications(rule=rule, replicating_locks_before=0, session=session)
1151
+ # Try to release potential parent rules
1152
+ rucio.core.rule.release_parent_rule(child_rule_id=rule.id, session=session)
1153
+
1154
+ return
1155
+
1156
+ except NoResultFound:
1157
+ # The rule has been deleted in the meanwhile
1158
+ return
1159
+
1160
+
1161
+ @read_session
1162
+ def get_rule(rule_id, *, session: "Session"):
1163
+ """
1164
+ Get a specific replication rule.
1165
+
1166
+ :param rule_id: The rule_id to select.
1167
+ :param session: The database session in use.
1168
+ :raises: RuleNotFound if no Rule can be found.
1169
+ """
1170
+
1171
+ try:
1172
+ rule = session.query(models.ReplicationRule).filter_by(id=rule_id).one()
1173
+ return rule.to_dict()
1174
+ except NoResultFound:
1175
+ raise RuleNotFound('No rule with the id %s found' % (rule_id))
1176
+ except StatementError:
1177
+ raise RucioException('Badly formatted rule id (%s)' % (rule_id))
1178
+
1179
+
1180
+ @transactional_session
1181
+ def update_rule(rule_id: str, options: dict[str, Any], *, session: "Session") -> None:
1182
+ """
1183
+ Update a rules options.
1184
+
1185
+ :param rule_id: The rule_id to lock.
1186
+ :param options: Dictionary of options
1187
+ :param session: The database session in use.
1188
+ :raises: RuleNotFound if no Rule can be found, InputValidationError if invalid option is used, ScratchDiskLifetimeConflict if wrong ScratchDiskLifetime is used.
1189
+ """
1190
+
1191
+ valid_options = ['comment', 'locked', 'lifetime', 'account', 'state',
1192
+ 'activity', 'source_replica_expression', 'cancel_requests',
1193
+ 'priority', 'child_rule_id', 'eol_at', 'meta',
1194
+ 'purge_replicas', 'boost_rule']
1195
+
1196
+ for key in options:
1197
+ if key not in valid_options:
1198
+ raise InputValidationError('%s is not a valid option to set.' % key)
1199
+
1200
+ try:
1201
+ query = select(
1202
+ models.ReplicationRule
1203
+ ).where(
1204
+ models.ReplicationRule.id == rule_id
1205
+ )
1206
+ rule: models.ReplicationRule = session.execute(query).scalar_one()
1207
+
1208
+ for key in options:
1209
+ if key == 'lifetime':
1210
+ # Check SCRATCHDISK Policy
1211
+ vo = rule.account.vo
1212
+ rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
1213
+ try:
1214
+ lifetime = get_scratch_policy(rule.account, rses, options['lifetime'], session=session)
1215
+ except UndefinedPolicy:
1216
+ lifetime = options['lifetime']
1217
+ rule.expires_at = datetime.utcnow() + timedelta(seconds=lifetime) if lifetime is not None else None
1218
+ if key == 'source_replica_expression':
1219
+ rule.source_replica_expression = options['source_replica_expression']
1220
+
1221
+ if key == 'comment':
1222
+ rule.comments = options['comment']
1223
+
1224
+ if key == 'activity':
1225
+ validate_schema(
1226
+ 'activity', options['activity'], vo=rule.account.vo
1227
+ )
1228
+ rule.activity = options['activity']
1229
+ # Cancel transfers and re-submit them:
1230
+ query = select(
1231
+ models.ReplicaLock
1232
+ ).where(
1233
+ models.ReplicaLock.rule_id == rule.id,
1234
+ models.ReplicaLock.state == LockState.REPLICATING
1235
+ )
1236
+ for lock in session.execute(query).scalars().all():
1237
+ transfers_to_cancel = request_core.cancel_request_did(
1238
+ scope=lock.scope,
1239
+ name=lock.name,
1240
+ dest_rse_id=lock.rse_id,
1241
+ session=session
1242
+ )
1243
+ transfer_core.cancel_transfers(transfers_to_cancel)
1244
+ query = select(
1245
+ models.RSEFileAssociation.md5,
1246
+ models.RSEFileAssociation.bytes,
1247
+ models.RSEFileAssociation.adler32
1248
+ ).where(
1249
+ models.RSEFileAssociation.scope == lock.scope,
1250
+ models.RSEFileAssociation.name == lock.name,
1251
+ models.RSEFileAssociation.rse_id == lock.rse_id
1252
+ )
1253
+ md5, bytes_, adler32 = session.execute(query).one()
1254
+ session.flush()
1255
+
1256
+ requests = create_transfer_dict(
1257
+ dest_rse_id=lock.rse_id,
1258
+ request_type=RequestType.TRANSFER,
1259
+ scope=lock.scope,
1260
+ name=lock.name,
1261
+ rule=rule,
1262
+ lock=lock,
1263
+ bytes_=bytes_,
1264
+ md5=md5,
1265
+ adler32=adler32,
1266
+ ds_scope=rule.scope,
1267
+ ds_name=rule.name,
1268
+ copy_pin_lifetime=None,
1269
+ activity=rule.activity,
1270
+ session=session
1271
+ )
1272
+ request_core.queue_requests([requests], session=session)
1273
+
1274
+ elif key == 'account':
1275
+ # Check if the account exists
1276
+ get_account(options['account'], session=session)
1277
+ # Update locks
1278
+ query = select(
1279
+ models.ReplicaLock
1280
+ ).where(
1281
+ models.ReplicaLock.rule_id == rule.id
1282
+ )
1283
+ counter_rses = {}
1284
+ for lock in session.execute(query).scalars().all():
1285
+ if lock.rse_id in counter_rses:
1286
+ counter_rses[lock.rse_id].append(lock.bytes)
1287
+ else:
1288
+ counter_rses[lock.rse_id] = [lock.bytes]
1289
+ for locktype in (models.ReplicaLock, models.DatasetLock):
1290
+ query = update(
1291
+ locktype
1292
+ ).where(
1293
+ locktype.rule_id == rule.id
1294
+ ).values(
1295
+ account=options['account']
1296
+ )
1297
+ session.execute(query)
1298
+
1299
+ # Update counters
1300
+ for rse_id in counter_rses:
1301
+ account_counter.decrease(
1302
+ rse_id=rse_id,
1303
+ account=rule.account,
1304
+ files=len(counter_rses[rse_id]),
1305
+ bytes_=sum(counter_rses[rse_id]),
1306
+ session=session
1307
+ )
1308
+ account_counter.increase(
1309
+ rse_id=rse_id,
1310
+ account=options['account'],
1311
+ files=len(counter_rses[rse_id]),
1312
+ bytes_=sum(counter_rses[rse_id]),
1313
+ session=session
1314
+ )
1315
+ # Update rule
1316
+ rule.account = options['account']
1317
+ session.flush()
1318
+
1319
+ elif key == 'state':
1320
+ if options.get('cancel_requests', False):
1321
+ rule_ids_to_stuck = set()
1322
+ query = select(
1323
+ models.ReplicaLock
1324
+ ).where(
1325
+ models.ReplicaLock.rule_id == rule.id,
1326
+ models.ReplicaLock.state == LockState.REPLICATING
1327
+ )
1328
+ for lock in session.execute(query).scalars().all():
1329
+ # Set locks to stuck:
1330
+ query = select(
1331
+ models.ReplicaLock
1332
+ ).where(
1333
+ models.ReplicaLock.scope == lock.scope,
1334
+ models.ReplicaLock.name == lock.name,
1335
+ models.ReplicaLock.rse_id == lock.rse_id,
1336
+ models.ReplicaLock.state == LockState.REPLICATING
1337
+ )
1338
+ for lock2 in session.execute(query).scalars().all():
1339
+ lock2.state = LockState.STUCK
1340
+ rule_ids_to_stuck.add(lock2.rule_id)
1341
+ transfers_to_cancel = request_core.cancel_request_did(
1342
+ scope=lock.scope,
1343
+ name=lock.name,
1344
+ dest_rse_id=lock.rse_id,
1345
+ session=session
1346
+ )
1347
+ transfer_core.cancel_transfers(transfers_to_cancel)
1348
+ query = select(
1349
+ models.RSEFileAssociation
1350
+ ).where(
1351
+ models.RSEFileAssociation.scope == lock.scope,
1352
+ models.RSEFileAssociation.name == lock.name,
1353
+ models.RSEFileAssociation.rse_id == lock.rse_id
1354
+ )
1355
+ replica = session.execute(query).scalar_one()
1356
+ replica.state = ReplicaState.UNAVAILABLE
1357
+ # Set rules and DATASETLOCKS to STUCK:
1358
+ for rid in rule_ids_to_stuck:
1359
+ query = update(
1360
+ models.ReplicationRule
1361
+ ).where(
1362
+ models.ReplicationRule.id == rid,
1363
+ models.ReplicationRule.state != RuleState.SUSPENDED
1364
+ ).values(
1365
+ state=RuleState.STUCK
1366
+ )
1367
+ session.execute(query)
1368
+
1369
+ query = update(
1370
+ models.DatasetLock
1371
+ ).where(
1372
+ models.DatasetLock.rule_id == rid
1373
+ ).values(
1374
+ state=LockState.STUCK
1375
+ )
1376
+ session.execute(query)
1377
+
1378
+ if options['state'].lower() == 'suspended':
1379
+ rule.state = RuleState.SUSPENDED
1380
+
1381
+ elif options['state'].lower() == 'stuck':
1382
+ rule.state = RuleState.STUCK
1383
+ rule.stuck_at = datetime.utcnow()
1384
+ if not options.get('cancel_requests', False):
1385
+ query = update(
1386
+ models.ReplicaLock
1387
+ ).where(
1388
+ models.ReplicaLock.rule_id == rule.id,
1389
+ models.ReplicaLock.state == LockState.REPLICATING
1390
+ ).values(
1391
+ state=LockState.STUCK
1392
+ )
1393
+ session.execute(query)
1394
+
1395
+ query = update(
1396
+ models.DatasetLock
1397
+ ).where(
1398
+ models.DatasetLock.rule_id == rule_id
1399
+ ).values(
1400
+ state=LockState.STUCK
1401
+ )
1402
+ session.execute(query)
1403
+
1404
+ elif key == 'cancel_requests':
1405
+ pass
1406
+
1407
+ elif key == 'priority':
1408
+ try:
1409
+ rule.priority = options[key]
1410
+ transfers_to_update = request_core.update_requests_priority(priority=options[key], filter_={'rule_id': rule_id}, session=session)
1411
+ transfer_core.update_transfer_priority(transfers_to_update)
1412
+ except Exception:
1413
+ raise UnsupportedOperation('The FTS Requests are already in a final state.')
1414
+
1415
+ elif key == 'child_rule_id':
1416
+ # Check if the child rule has the same scope/name as the parent rule
1417
+ child_id: Optional[str] = options[key]
1418
+ if child_id is None:
1419
+ if not rule.child_rule_id:
1420
+ raise InputValidationError('Cannot detach child when no such relationship exists')
1421
+ # dissolve relationship
1422
+ rule.child_rule_id = None # type: ignore
1423
+ # remove expiration date
1424
+ rule.expires_at = None # type: ignore
1425
+ else:
1426
+ query = select(
1427
+ models.ReplicationRule
1428
+ ).where(
1429
+ models.ReplicationRule.id == child_id
1430
+ )
1431
+ child_rule = session.execute(query).scalar_one()
1432
+ if rule.scope != child_rule.scope or rule.name != child_rule.name:
1433
+ raise InputValidationError('Parent and child rule must be set on the same dataset.')
1434
+ if rule.id == options[key]:
1435
+ raise InputValidationError('Self-referencing parent/child-relationship.')
1436
+ if child_rule.state != RuleState.OK:
1437
+ rule.child_rule_id = child_id # type: ignore
1438
+
1439
+ elif key == 'meta':
1440
+ # Need to json.dump the metadata
1441
+ rule.meta = json.dumps(options[key])
1442
+
1443
+ else:
1444
+ setattr(rule, key, options[key])
1445
+
1446
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1447
+
1448
+ # `boost_rule` should run after `stuck`, so lets not include it in the loop since the arguments are unordered
1449
+ if 'boost_rule' in options:
1450
+ query = select(
1451
+ models.ReplicaLock
1452
+ ).where(
1453
+ models.ReplicaLock.rule_id == rule.id,
1454
+ models.ReplicaLock.state == LockState.STUCK
1455
+ )
1456
+ for lock in session.execute(query).scalars().all():
1457
+ lock['updated_at'] -= timedelta(days=1)
1458
+
1459
+ rule['updated_at'] -= timedelta(days=1)
1460
+
1461
+ insert_rule_history(
1462
+ rule,
1463
+ recent=True,
1464
+ longterm=False,
1465
+ session=session
1466
+ )
1467
+
1468
+ except IntegrityError as error:
1469
+ if match('.*ORA-00001.*', str(error.args[0])) \
1470
+ or match('.*IntegrityError.*UNIQUE constraint failed.*', str(error.args[0])) \
1471
+ or match('.*1062.*Duplicate entry.*for key.*', str(error.args[0])) \
1472
+ or match('.*IntegrityError.*columns? .*not unique.*', str(error.args[0])):
1473
+ raise DuplicateRule(error.args[0])
1474
+ else:
1475
+ raise error
1476
+ except NoResultFound:
1477
+ raise RuleNotFound('No rule with the id %s found' % (rule_id))
1478
+ except StatementError as e:
1479
+ raise RucioException(f"A StatementError occurred while processing rule {rule_id}") from e
1480
+
1481
+
1482
+ @transactional_session
1483
+ def reduce_rule(rule_id, copies, exclude_expression=None, *, session: "Session"):
1484
+ """
1485
+ Reduce the number of copies for a rule by atomically replacing the rule.
1486
+
1487
+ :param rule_id: Rule to be reduced.
1488
+ :param copies: Number of copies of the new rule.
1489
+ :param exclude_expression: RSE Expression of RSEs to exclude.
1490
+ :param session: The DB Session.
1491
+ :raises: RuleReplaceFailed, RuleNotFound
1492
+ """
1493
+ try:
1494
+ rule = session.query(models.ReplicationRule).filter_by(id=rule_id).one()
1495
+
1496
+ if copies >= rule.copies:
1497
+ raise RuleReplaceFailed('Copies of the new rule must be smaller than the old rule.')
1498
+
1499
+ if rule.state != RuleState.OK:
1500
+ raise RuleReplaceFailed('The source rule must be in state OK.')
1501
+
1502
+ if exclude_expression:
1503
+ rse_expression = '(' + rule.rse_expression + ')' + '\\' + '(' + exclude_expression + ')'
1504
+ else:
1505
+ rse_expression = rule.rse_expression
1506
+
1507
+ grouping = {RuleGrouping.ALL: 'ALL', RuleGrouping.NONE: 'NONE'}.get(rule.grouping, 'DATASET')
1508
+
1509
+ if rule.expires_at:
1510
+ lifetime = (rule.expires_at - datetime.utcnow()).days * 24 * 3600 + (rule.expires_at - datetime.utcnow()).seconds
1511
+ else:
1512
+ lifetime = None
1513
+
1514
+ notify = {RuleNotification.YES: 'Y', RuleNotification.CLOSE: 'C', RuleNotification.PROGRESS: 'P'}.get(rule.notification, 'N')
1515
+
1516
+ new_rule_id = add_rule(dids=[{'scope': rule.scope, 'name': rule.name}],
1517
+ account=rule.account,
1518
+ copies=copies,
1519
+ rse_expression=rse_expression,
1520
+ grouping=grouping,
1521
+ weight=rule.weight,
1522
+ lifetime=lifetime,
1523
+ locked=rule.locked,
1524
+ subscription_id=rule.subscription_id,
1525
+ source_replica_expression=rule.source_replica_expression,
1526
+ activity=rule.activity,
1527
+ notify=notify,
1528
+ purge_replicas=rule.purge_replicas,
1529
+ ignore_availability=rule.ignore_availability,
1530
+ session=session)
1531
+
1532
+ session.flush()
1533
+
1534
+ new_rule = session.query(models.ReplicationRule).filter_by(id=new_rule_id[0]).one()
1535
+
1536
+ if new_rule.state != RuleState.OK:
1537
+ raise RuleReplaceFailed('The replacement of the rule failed.')
1538
+
1539
+ delete_rule(rule_id=rule_id,
1540
+ session=session)
1541
+
1542
+ return new_rule_id[0]
1543
+
1544
+ except NoResultFound:
1545
+ raise RuleNotFound('No rule with the id %s found' % (rule_id))
1546
+
1547
+
1548
+ @transactional_session
1549
+ def move_rule(rule_id: str, rse_expression: str, override: Optional[dict[str, Any]] = None, *, session: "Session"):
1550
+ """
1551
+ Move a replication rule to another RSE and, once done, delete the original one.
1552
+
1553
+ :param rule_id: Rule to be moved.
1554
+ :param rse_expression: RSE expression of the new rule.
1555
+ :param override: Configurations to update for the new rule.
1556
+ :param session: The DB Session.
1557
+ :raises: RuleNotFound, RuleReplaceFailed, InvalidRSEExpression
1558
+ """
1559
+ override = override or {}
1560
+
1561
+ try:
1562
+ rule = session.query(models.ReplicationRule).filter_by(id=rule_id).one()
1563
+
1564
+ if rule.child_rule_id:
1565
+ raise RuleReplaceFailed('The rule must not have a child rule.')
1566
+
1567
+ grouping = {RuleGrouping.ALL: 'ALL', RuleGrouping.NONE: 'NONE'}.get(rule.grouping, 'DATASET')
1568
+
1569
+ if rule.expires_at:
1570
+ lifetime = (rule.expires_at - datetime.utcnow()).days * 24 * 3600 + (rule.expires_at - datetime.utcnow()).seconds
1571
+ else:
1572
+ lifetime = None
1573
+
1574
+ notify = {RuleNotification.YES: 'Y', RuleNotification.CLOSE: 'C', RuleNotification.PROGRESS: 'P'}.get(rule.notification, 'N')
1575
+
1576
+ options = {
1577
+ 'dids': [{'scope': rule.scope, 'name': rule.name}],
1578
+ 'account': rule.account,
1579
+ 'copies': rule.copies,
1580
+ 'rse_expression': rse_expression,
1581
+ 'grouping': grouping,
1582
+ 'weight': rule.weight,
1583
+ 'lifetime': lifetime,
1584
+ 'locked': rule.locked,
1585
+ 'subscription_id': rule.subscription_id,
1586
+ 'source_replica_expression': rule.source_replica_expression,
1587
+ 'activity': rule.activity,
1588
+ 'notify': notify,
1589
+ 'purge_replicas': rule.purge_replicas,
1590
+ 'ignore_availability': rule.ignore_availability,
1591
+ 'comment': rule.comments,
1592
+ 'session': session,
1593
+ }
1594
+
1595
+ for key in override:
1596
+ if key in ['dids', 'session']:
1597
+ raise UnsupportedOperation('Not allowed to override option %s' % key)
1598
+ elif key not in options:
1599
+ raise UnsupportedOperation('Non-valid override option %s' % key)
1600
+ else:
1601
+ options[key] = override[key]
1602
+
1603
+ new_rule_id = add_rule(**options)
1604
+
1605
+ session.flush()
1606
+
1607
+ update_rule(rule_id=rule_id, options={'child_rule_id': new_rule_id[0], 'lifetime': 0}, session=session)
1608
+
1609
+ return new_rule_id[0]
1610
+
1611
+ except StatementError:
1612
+ raise RucioException('Badly formatted rule id (%s)' % (rule_id))
1613
+ except NoResultFound:
1614
+ raise RuleNotFound('No rule with the id %s found' % (rule_id))
1615
+
1616
+
1617
+ @transactional_session
1618
+ def re_evaluate_did(scope, name, rule_evaluation_action, *, session: "Session"):
1619
+ """
1620
+ Re-Evaluates a did.
1621
+
1622
+ :param scope: The scope of the did to be re-evaluated.
1623
+ :param name: The name of the did to be re-evaluated.
1624
+ :param rule_evaluation_action: The Rule evaluation action.
1625
+ :param session: The database session in use.
1626
+ :raises: DataIdentifierNotFound
1627
+ """
1628
+
1629
+ try:
1630
+ did = session.query(models.DataIdentifier).filter(models.DataIdentifier.scope == scope,
1631
+ models.DataIdentifier.name == name).one()
1632
+ except NoResultFound:
1633
+ raise DataIdentifierNotFound()
1634
+
1635
+ if rule_evaluation_action == DIDReEvaluation.ATTACH:
1636
+ __evaluate_did_attach(did, session=session)
1637
+ else:
1638
+ __evaluate_did_detach(did, session=session)
1639
+
1640
+ # Update size and length of did
1641
+ if session.bind.dialect.name == 'oracle':
1642
+ stmt = session.query(func.sum(models.DataIdentifierAssociation.bytes),
1643
+ func.count(1)).\
1644
+ with_hint(models.DataIdentifierAssociation,
1645
+ "index(CONTENTS CONTENTS_PK)", 'oracle').\
1646
+ filter(models.DataIdentifierAssociation.scope == scope,
1647
+ models.DataIdentifierAssociation.name == name)
1648
+ for bytes_, length in stmt:
1649
+ did.bytes = bytes_
1650
+ did.length = length
1651
+
1652
+ # Add an updated_col_rep
1653
+ if did.did_type == DIDType.DATASET:
1654
+ models.UpdatedCollectionReplica(scope=scope,
1655
+ name=name,
1656
+ did_type=did.did_type).save(session=session)
1657
+
1658
+
1659
+ @read_session
1660
+ def get_updated_dids(total_workers, worker_number, limit=100, blocked_dids=[], *, session: "Session"):
1661
+ """
1662
+ Get updated dids.
1663
+
1664
+ :param total_workers: Number of total workers.
1665
+ :param worker_number: id of the executing worker.
1666
+ :param limit: Maximum number of dids to return.
1667
+ :param blocked_dids: Blocked dids to filter.
1668
+ :param session: Database session in use.
1669
+ """
1670
+ query = session.query(models.UpdatedDID.id,
1671
+ models.UpdatedDID.scope,
1672
+ models.UpdatedDID.name,
1673
+ models.UpdatedDID.rule_evaluation_action)
1674
+
1675
+ query = filter_thread_work(session=session, query=query, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
1676
+
1677
+ # Remove blocked dids from query, but only do the first 30 ones, not to overload the query
1678
+ if blocked_dids:
1679
+ chunk = list(chunks(blocked_dids, 30))[0]
1680
+ query = query.filter(tuple_(models.UpdatedDID.scope, models.UpdatedDID.name).notin_(chunk))
1681
+
1682
+ if limit:
1683
+ fetched_dids = query.order_by(models.UpdatedDID.created_at).limit(limit).all()
1684
+ filtered_dids = [did for did in fetched_dids if (did.scope, did.name) not in blocked_dids]
1685
+ if len(fetched_dids) == limit and not filtered_dids:
1686
+ return get_updated_dids(total_workers=total_workers,
1687
+ worker_number=worker_number,
1688
+ limit=None,
1689
+ blocked_dids=blocked_dids,
1690
+ session=session)
1691
+ else:
1692
+ return filtered_dids
1693
+ else:
1694
+ return [did for did in query.order_by(models.UpdatedDID.created_at).all() if (did.scope, did.name) not in blocked_dids]
1695
+
1696
+
1697
+ @read_session
1698
+ def get_rules_beyond_eol(date_check, worker_number, total_workers, *, session: "Session"):
1699
+ """
1700
+ Get rules which have eol_at before a certain date.
1701
+
1702
+ :param date_check: The reference date that should be compared to eol_at.
1703
+ :param worker_number: id of the executing worker.
1704
+ :param total_workers: Number of total workers.
1705
+ :param session: Database session in use.
1706
+ """
1707
+ query = session.query(models.ReplicationRule.scope,
1708
+ models.ReplicationRule.name,
1709
+ models.ReplicationRule.rse_expression,
1710
+ models.ReplicationRule.locked,
1711
+ models.ReplicationRule.id,
1712
+ models.ReplicationRule.eol_at,
1713
+ models.ReplicationRule.expires_at,
1714
+ models.ReplicationRule.account).\
1715
+ filter(models.ReplicationRule.eol_at < date_check)
1716
+
1717
+ query = filter_thread_work(session=session, query=query, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
1718
+ return [rule for rule in query.all()]
1719
+
1720
+
1721
+ @read_session
1722
+ def get_expired_rules(total_workers, worker_number, limit=100, blocked_rules=[], *, session: "Session"):
1723
+ """
1724
+ Get expired rules.
1725
+
1726
+ :param total_workers: Number of total workers.
1727
+ :param worker_number: id of the executing worker.
1728
+ :param limit: Maximum number of rules to return.
1729
+ :param blocked_rules: List of blocked rules.
1730
+ :param session: Database session in use.
1731
+ """
1732
+
1733
+ query = session.query(models.ReplicationRule.id, models.ReplicationRule.rse_expression).filter(models.ReplicationRule.expires_at < datetime.utcnow(),
1734
+ models.ReplicationRule.locked == false(),
1735
+ models.ReplicationRule.child_rule_id == None).\
1736
+ with_hint(models.ReplicationRule, "index(rules RULES_EXPIRES_AT_IDX)", 'oracle').\
1737
+ order_by(models.ReplicationRule.expires_at) # NOQA
1738
+
1739
+ query = filter_thread_work(session=session, query=query, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
1740
+
1741
+ if limit:
1742
+ fetched_rules = query.limit(limit).all()
1743
+ filtered_rules = [rule for rule in fetched_rules if rule[0] not in blocked_rules]
1744
+ if len(fetched_rules) == limit and not filtered_rules:
1745
+ return get_expired_rules(total_workers=total_workers,
1746
+ worker_number=worker_number,
1747
+ limit=None,
1748
+ blocked_rules=blocked_rules,
1749
+ session=session)
1750
+ else:
1751
+ return filtered_rules
1752
+ else:
1753
+ return [rule for rule in query.all() if rule[0] not in blocked_rules]
1754
+
1755
+
1756
+ @read_session
1757
+ def get_injected_rules(total_workers, worker_number, limit=100, blocked_rules=[], *, session: "Session"):
1758
+ """
1759
+ Get rules to be injected.
1760
+
1761
+ :param total_workers: Number of total workers.
1762
+ :param worker_number: id of the executing worker.
1763
+ :param limit: Maximum number of rules to return.
1764
+ :param blocked_rules: Blocked rules not to include.
1765
+ :param session: Database session in use.
1766
+ """
1767
+
1768
+ query = session.query(models.ReplicationRule.id).\
1769
+ with_hint(models.ReplicationRule, "index(rules RULES_STATE_IDX)", 'oracle').\
1770
+ filter(models.ReplicationRule.state == RuleState.INJECT).\
1771
+ order_by(models.ReplicationRule.created_at).\
1772
+ filter(models.ReplicationRule.created_at <= datetime.utcnow())
1773
+
1774
+ query = filter_thread_work(session=session, query=query, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
1775
+
1776
+ if limit:
1777
+ fetched_rules = query.limit(limit).all()
1778
+ filtered_rules = [rule for rule in fetched_rules if rule[0] not in blocked_rules]
1779
+ if len(fetched_rules) == limit and not filtered_rules:
1780
+ return get_injected_rules(total_workers=total_workers,
1781
+ worker_number=worker_number,
1782
+ limit=None,
1783
+ blocked_rules=blocked_rules,
1784
+ session=session)
1785
+ else:
1786
+ return filtered_rules
1787
+ else:
1788
+ return [rule for rule in query.all() if rule[0] not in blocked_rules]
1789
+
1790
+
1791
+ @read_session
1792
+ def get_stuck_rules(total_workers, worker_number, delta=600, limit=10, blocked_rules=[], *, session: "Session"):
1793
+ """
1794
+ Get stuck rules.
1795
+
1796
+ :param total_workers: Number of total workers.
1797
+ :param worker_number: id of the executing worker.
1798
+ :param delta: Delta in seconds to select rules in.
1799
+ :param limit: Maximum number of rules to select.
1800
+ :param blocked_rules: Blocked rules to filter out.
1801
+ :param session: Database session in use.
1802
+ """
1803
+ query = session.query(models.ReplicationRule.id).\
1804
+ with_hint(models.ReplicationRule, "index(rules RULES_STATE_IDX)", 'oracle').\
1805
+ filter(models.ReplicationRule.state == RuleState.STUCK).\
1806
+ filter(models.ReplicationRule.updated_at < datetime.utcnow() - timedelta(seconds=delta)).\
1807
+ filter(or_(models.ReplicationRule.expires_at == null(),
1808
+ models.ReplicationRule.expires_at > datetime.utcnow(),
1809
+ models.ReplicationRule.locked == true())).\
1810
+ order_by(models.ReplicationRule.updated_at)
1811
+
1812
+ query = filter_thread_work(session=session, query=query, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
1813
+
1814
+ if limit:
1815
+ fetched_rules = query.limit(limit).all()
1816
+ filtered_rules = [rule for rule in fetched_rules if rule[0] not in blocked_rules]
1817
+ if len(fetched_rules) == limit and not filtered_rules:
1818
+ return get_stuck_rules(total_workers=total_workers,
1819
+ worker_number=worker_number,
1820
+ delta=delta,
1821
+ limit=None,
1822
+ blocked_rules=blocked_rules,
1823
+ session=session)
1824
+ else:
1825
+ return filtered_rules
1826
+ else:
1827
+ return [rule for rule in query.all() if rule[0] not in blocked_rules]
1828
+
1829
+
1830
+ @transactional_session
1831
+ def delete_updated_did(id_, *, session: "Session"):
1832
+ """
1833
+ Delete an updated_did by id.
1834
+
1835
+ :param id_: Id of the row not to delete.
1836
+ :param session: The database session in use.
1837
+ """
1838
+ session.query(models.UpdatedDID).filter(models.UpdatedDID.id == id_).delete()
1839
+
1840
+
1841
+ @transactional_session
1842
+ def update_rules_for_lost_replica(scope, name, rse_id, nowait=False, *, session: "Session", logger=logging.log):
1843
+ """
1844
+ Update rules if a file replica is lost.
1845
+
1846
+ :param scope: Scope of the replica.
1847
+ :param name: Name of the replica.
1848
+ :param rse_id: RSE id of the replica.
1849
+ :param nowait: Nowait parameter for the FOR UPDATE statement.
1850
+ :param session: The database session in use.
1851
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
1852
+ """
1853
+
1854
+ locks = session.query(models.ReplicaLock).filter(models.ReplicaLock.scope == scope, models.ReplicaLock.name == name, models.ReplicaLock.rse_id == rse_id).with_for_update(nowait=nowait).all()
1855
+ replica = session.query(models.RSEFileAssociation).filter(models.RSEFileAssociation.scope == scope, models.RSEFileAssociation.name == name, models.RSEFileAssociation.rse_id == rse_id).with_for_update(nowait=nowait).one()
1856
+ requests = session.query(models.Request).filter(models.Request.scope == scope, models.Request.name == name, models.Request.dest_rse_id == rse_id).with_for_update(nowait=nowait).all()
1857
+
1858
+ rse = get_rse_name(rse_id, session=session)
1859
+
1860
+ datasets = []
1861
+ parent_dids = rucio.core.did.list_parent_dids(scope=scope, name=name, session=session)
1862
+ for parent in parent_dids:
1863
+ if {'name': parent['name'], 'scope': parent['scope']} not in datasets:
1864
+ datasets.append({'name': parent['name'], 'scope': parent['scope']})
1865
+
1866
+ for request in requests:
1867
+ session.delete(request)
1868
+
1869
+ for lock in locks:
1870
+ rule = session.query(models.ReplicationRule).filter(models.ReplicationRule.id == lock.rule_id).with_for_update(nowait=nowait).one()
1871
+ rule_state_before = rule.state
1872
+ replica.lock_cnt -= 1
1873
+ if lock.state == LockState.OK:
1874
+ rule.locks_ok_cnt -= 1
1875
+ elif lock.state == LockState.REPLICATING:
1876
+ rule.locks_replicating_cnt -= 1
1877
+ elif lock.state == LockState.STUCK:
1878
+ rule.locks_stuck_cnt -= 1
1879
+ account_counter.decrease(rse_id=rse_id, account=rule.account, files=1, bytes_=lock.bytes, session=session)
1880
+ if rule.state == RuleState.SUSPENDED:
1881
+ pass
1882
+ elif rule.state == RuleState.STUCK:
1883
+ pass
1884
+ elif rule.locks_replicating_cnt == 0 and rule.locks_stuck_cnt == 0:
1885
+ rule.state = RuleState.OK
1886
+ if rule.grouping != RuleGrouping.NONE:
1887
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.OK})
1888
+ session.flush()
1889
+ if rule_state_before != RuleState.OK:
1890
+ generate_rule_notifications(rule=rule, session=session)
1891
+ generate_email_for_rule_ok_notification(rule=rule, session=session)
1892
+ # Try to release potential parent rules
1893
+ release_parent_rule(child_rule_id=rule.id, session=session)
1894
+ # Insert rule history
1895
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1896
+
1897
+ session.delete(lock)
1898
+
1899
+ if replica.lock_cnt != 0:
1900
+ logger(logging.ERROR, 'Replica for did %s:%s with lock_cnt = %s. This should never happen. Update lock_cnt', scope, name, replica.lock_cnt)
1901
+ replica.lock_cnt = 0
1902
+
1903
+ replica.tombstone = OBSOLETE
1904
+ replica.state = ReplicaState.UNAVAILABLE
1905
+ session.query(models.DataIdentifier).filter_by(scope=scope, name=name).update({'availability': DIDAvailability.LOST})
1906
+ session.query(models.BadReplicas).filter_by(state=BadFilesStatus.BAD, rse_id=rse_id, scope=scope, name=name).update({'state': BadFilesStatus.LOST, 'updated_at': datetime.utcnow()})
1907
+ for dts in datasets:
1908
+ logger(logging.INFO, 'File %s:%s bad at site %s is completely lost from dataset %s:%s. Will be marked as LOST and detached', scope, name, rse, dts['scope'], dts['name'])
1909
+ rucio.core.did.detach_dids(scope=dts['scope'], name=dts['name'], dids=[{'scope': scope, 'name': name}], session=session)
1910
+
1911
+ message = {'scope': scope.external,
1912
+ 'name': name,
1913
+ 'dataset_name': dts['name'],
1914
+ 'dataset_scope': dts['scope'].external}
1915
+ if scope.vo != 'def':
1916
+ message['vo'] = scope.vo
1917
+
1918
+ add_message('LOST', message, session=session)
1919
+
1920
+
1921
+ @transactional_session
1922
+ def update_rules_for_bad_replica(scope, name, rse_id, nowait=False, *, session: "Session", logger=logging.log):
1923
+ """
1924
+ Update rules if a file replica is bad and has to be recreated.
1925
+
1926
+ :param scope: Scope of the replica.
1927
+ :param name: Name of the replica.
1928
+ :param rse_id: RSE id of the replica.
1929
+ :param nowait: Nowait parameter for the FOR UPDATE statement.
1930
+ :param session: The database session in use.
1931
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
1932
+ """
1933
+
1934
+ locks = session.query(models.ReplicaLock).filter(models.ReplicaLock.scope == scope, models.ReplicaLock.name == name, models.ReplicaLock.rse_id == rse_id).with_for_update(nowait=nowait).all()
1935
+ replica = session.query(models.RSEFileAssociation).filter(models.RSEFileAssociation.scope == scope, models.RSEFileAssociation.name == name, models.RSEFileAssociation.rse_id == rse_id).with_for_update(nowait=nowait).one()
1936
+
1937
+ nlock = 0
1938
+ datasets = []
1939
+ for lock in locks:
1940
+ nlock += 1
1941
+ rule = session.query(models.ReplicationRule).filter(models.ReplicationRule.id == lock.rule_id).with_for_update(nowait=nowait).one()
1942
+ # If source replica expression exists, we remove it
1943
+ if rule.source_replica_expression:
1944
+ rule.source_replica_expression = None
1945
+ # Get the affected datasets
1946
+ ds_scope = rule.scope
1947
+ ds_name = rule.name
1948
+ dataset = '%s:%s' % (ds_scope, ds_name)
1949
+ if dataset not in datasets:
1950
+ datasets.append(dataset)
1951
+ logger(logging.INFO, 'Recovering file %s:%s from dataset %s:%s at site %s', scope, name, ds_scope, ds_name, get_rse_name(rse_id=rse_id, session=session))
1952
+ # Insert a new row in the UpdateCollectionReplica table
1953
+ models.UpdatedCollectionReplica(scope=ds_scope,
1954
+ name=ds_name,
1955
+ did_type=rule.did_type,
1956
+ rse_id=lock.rse_id).save(flush=False, session=session)
1957
+ # Set the lock counters
1958
+ if lock.state == LockState.OK:
1959
+ rule.locks_ok_cnt -= 1
1960
+ elif lock.state == LockState.REPLICATING:
1961
+ rule.locks_replicating_cnt -= 1
1962
+ elif lock.state == LockState.STUCK:
1963
+ rule.locks_stuck_cnt -= 1
1964
+ rule.locks_replicating_cnt += 1
1965
+ # Generate the request
1966
+ try:
1967
+ request_core.get_request_by_did(scope, name, rse_id, session=session)
1968
+ except RequestNotFound:
1969
+ bytes_ = replica.bytes
1970
+ md5 = replica.md5
1971
+ adler32 = replica.adler32
1972
+ request_core.queue_requests(requests=[create_transfer_dict(dest_rse_id=rse_id,
1973
+ request_type=RequestType.TRANSFER,
1974
+ scope=scope, name=name, rule=rule, lock=lock, bytes_=bytes_, md5=md5, adler32=adler32,
1975
+ ds_scope=ds_scope, ds_name=ds_name, copy_pin_lifetime=None, activity='Recovery', session=session)], session=session)
1976
+ lock.state = LockState.REPLICATING
1977
+ if rule.state == RuleState.SUSPENDED:
1978
+ pass
1979
+ elif rule.state == RuleState.STUCK:
1980
+ pass
1981
+ else:
1982
+ rule.state = RuleState.REPLICATING
1983
+ if rule.grouping != RuleGrouping.NONE:
1984
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.REPLICATING})
1985
+ # Insert rule history
1986
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1987
+ if nlock:
1988
+ session.query(models.RSEFileAssociation).filter(models.RSEFileAssociation.scope == scope, models.RSEFileAssociation.name == name, models.RSEFileAssociation.rse_id == rse_id).update({'state': ReplicaState.COPYING})
1989
+ else:
1990
+ logger(logging.INFO, 'File %s:%s at site %s has no locks. Will be deleted now.', scope, name, get_rse_name(rse_id=rse_id, session=session))
1991
+ tombstone = OBSOLETE
1992
+ session.query(models.RSEFileAssociation).filter(models.RSEFileAssociation.scope == scope, models.RSEFileAssociation.name == name, models.RSEFileAssociation.rse_id == rse_id).update({'state': ReplicaState.UNAVAILABLE, 'tombstone': tombstone})
1993
+
1994
+
1995
+ @transactional_session
1996
+ def generate_rule_notifications(rule, replicating_locks_before=None, *, session: "Session"):
1997
+ """
1998
+ Generate (If necessary) a callback for a rule (DATASETLOCK_OK, RULE_OK, DATASETLOCK_PROGRESS)
1999
+
2000
+ :param rule: The rule object.
2001
+ :param replicating_locks_before: Amount of replicating locks before the current state change.
2002
+ :param session: The Database session
2003
+ """
2004
+
2005
+ session.flush()
2006
+ total_locks = rule.locks_replicating_cnt + rule.locks_ok_cnt
2007
+
2008
+ if rule.state == RuleState.OK:
2009
+ # Only notify when rule is in state OK
2010
+
2011
+ # RULE_OK RULE_PROGRESS NOTIFICATIONS:
2012
+ if rule.notification == RuleNotification.YES:
2013
+ payload = {'scope': rule.scope.external,
2014
+ 'name': rule.name,
2015
+ 'rule_id': rule.id}
2016
+ if rule.scope.vo != 'def':
2017
+ payload['vo'] = rule.scope.vo
2018
+
2019
+ add_message(event_type='RULE_OK', payload=payload, session=session)
2020
+
2021
+ elif rule.notification in [RuleNotification.CLOSE, RuleNotification.PROGRESS]:
2022
+ try:
2023
+ did = rucio.core.did.get_did(scope=rule.scope, name=rule.name, session=session)
2024
+ if not did['open']:
2025
+ payload = {'scope': rule.scope.external,
2026
+ 'name': rule.name,
2027
+ 'rule_id': rule.id}
2028
+ if rule.scope.vo != 'def':
2029
+ payload['vo'] = rule.scope.vo
2030
+
2031
+ add_message(event_type='RULE_OK', payload=payload, session=session)
2032
+
2033
+ if rule.notification == RuleNotification.PROGRESS:
2034
+ payload = {'scope': rule.scope.external,
2035
+ 'name': rule.name,
2036
+ 'rule_id': rule.id,
2037
+ 'progress': __progress_class(rule.locks_replicating_cnt, total_locks)}
2038
+ if rule.scope.vo != 'def':
2039
+ payload['vo'] = rule.scope.vo
2040
+
2041
+ add_message(event_type='RULE_PROGRESS', payload=payload, session=session)
2042
+
2043
+ except DataIdentifierNotFound:
2044
+ pass
2045
+
2046
+ # DATASETLOCK_OK NOTIFICATIONS:
2047
+ if rule.grouping != RuleGrouping.NONE:
2048
+ # Only send DATASETLOCK_OK callbacks for ALL/DATASET grouped rules
2049
+ if rule.notification == RuleNotification.YES:
2050
+ dataset_locks = session.query(models.DatasetLock).filter_by(rule_id=rule.id).all()
2051
+ for dataset_lock in dataset_locks:
2052
+ payload = {'scope': dataset_lock.scope.external,
2053
+ 'name': dataset_lock.name,
2054
+ 'rse': get_rse_name(rse_id=dataset_lock.rse_id, session=session),
2055
+ 'rse_id': dataset_lock.rse_id,
2056
+ 'rule_id': rule.id}
2057
+ if dataset_lock.scope.vo != 'def':
2058
+ payload['vo'] = dataset_lock.scope.vo
2059
+
2060
+ add_message(event_type='DATASETLOCK_OK', payload=payload, session=session)
2061
+
2062
+ elif rule.notification == RuleNotification.CLOSE:
2063
+ dataset_locks = session.query(models.DatasetLock).filter_by(rule_id=rule.id).all()
2064
+ for dataset_lock in dataset_locks:
2065
+ try:
2066
+ did = rucio.core.did.get_did(scope=dataset_lock.scope, name=dataset_lock.name, session=session)
2067
+ if not did['open']:
2068
+ if did['length'] is None:
2069
+ return
2070
+ if did['length'] * rule.copies == rule.locks_ok_cnt:
2071
+ payload = {'scope': dataset_lock.scope.external,
2072
+ 'name': dataset_lock.name,
2073
+ 'rse': get_rse_name(rse_id=dataset_lock.rse_id, session=session),
2074
+ 'rse_id': dataset_lock.rse_id,
2075
+ 'rule_id': rule.id}
2076
+ if dataset_lock.scope.vo != 'def':
2077
+ payload['vo'] = dataset_lock.scope.vo
2078
+
2079
+ add_message(event_type='DATASETLOCK_OK', payload=payload, session=session)
2080
+
2081
+ except DataIdentifierNotFound:
2082
+ pass
2083
+
2084
+ elif rule.state == RuleState.REPLICATING and rule.notification == RuleNotification.PROGRESS and replicating_locks_before:
2085
+ # For RuleNotification PROGRESS rules, also notifiy when REPLICATING thresholds are passed
2086
+ if __progress_class(replicating_locks_before, total_locks) != __progress_class(rule.locks_replicating_cnt, total_locks):
2087
+ try:
2088
+ did = rucio.core.did.get_did(scope=rule.scope, name=rule.name, session=session)
2089
+ if not did['open']:
2090
+ payload = {'scope': rule.scope.external,
2091
+ 'name': rule.name,
2092
+ 'rule_id': rule.id,
2093
+ 'progress': __progress_class(rule.locks_replicating_cnt, total_locks)}
2094
+ if rule.scope.vo != 'def':
2095
+ payload['vo'] = rule.scope.vo
2096
+
2097
+ add_message(event_type='RULE_PROGRESS', payload=payload, session=session)
2098
+
2099
+ except DataIdentifierNotFound:
2100
+ pass
2101
+
2102
+
2103
+ @transactional_session
2104
+ def generate_email_for_rule_ok_notification(rule, *, session: "Session", logger=logging.log):
2105
+ """
2106
+ Generate (If necessary) an eMail for a rule with notification mode Y.
2107
+
2108
+ :param rule: The rule object.
2109
+ :param session: The Database session
2110
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
2111
+ """
2112
+
2113
+ session.flush()
2114
+
2115
+ if rule.state == RuleState.OK and rule.notification == RuleNotification.YES:
2116
+ try:
2117
+ template_path = '%s/rule_ok_notification.tmpl' % config_get('common', 'mailtemplatedir')
2118
+ except NoOptionError as ex:
2119
+ logger(logging.ERROR, "Missing configuration option 'mailtemplatedir'.", exc_info=ex)
2120
+ return
2121
+
2122
+ try:
2123
+ with open(template_path, 'r') as templatefile:
2124
+ template = Template(templatefile.read())
2125
+ except IOError as ex:
2126
+ logger(logging.ERROR, "Couldn't open file '%s'", template_path, exc_info=ex)
2127
+ return
2128
+
2129
+ email = get_account(account=rule.account, session=session).email
2130
+ if not email:
2131
+ logger(logging.INFO, 'No email associated with rule ID %s.', rule.id)
2132
+ return
2133
+
2134
+ try:
2135
+ email_body = template.safe_substitute({'rule_id': str(rule.id),
2136
+ 'created_at': str(rule.created_at),
2137
+ 'expires_at': str(rule.expires_at),
2138
+ 'rse_expression': rule.rse_expression,
2139
+ 'comment': rule.comments,
2140
+ 'scope': rule.scope.external,
2141
+ 'name': rule.name,
2142
+ 'did_type': rule.did_type})
2143
+ except ValueError as ex:
2144
+ logger(logging.ERROR, "Invalid mail template.", exc_info=ex)
2145
+ return
2146
+
2147
+ add_message(event_type='email',
2148
+ payload={'body': email_body,
2149
+ 'to': [email],
2150
+ 'subject': '[RUCIO] Replication rule %s has been succesfully transferred' % (str(rule.id))},
2151
+ session=session)
2152
+
2153
+
2154
+ @transactional_session
2155
+ def insert_rule_history(rule, recent=True, longterm=False, *, session: "Session"):
2156
+ """
2157
+ Insert rule history to recent/longterm history.
2158
+
2159
+ :param rule: The rule object.
2160
+ :param recent: Insert to recent table.
2161
+ :param longterm: Insert to longterm table.
2162
+ :param session: The Database session.
2163
+ """
2164
+ if recent:
2165
+ models.ReplicationRuleHistoryRecent(id=rule.id, subscription_id=rule.subscription_id, account=rule.account, scope=rule.scope, name=rule.name,
2166
+ did_type=rule.did_type, state=rule.state, error=rule.error, rse_expression=rule.rse_expression, copies=rule.copies,
2167
+ expires_at=rule.expires_at, weight=rule.weight, locked=rule.locked, locks_ok_cnt=rule.locks_ok_cnt,
2168
+ locks_replicating_cnt=rule.locks_replicating_cnt, locks_stuck_cnt=rule.locks_stuck_cnt, source_replica_expression=rule.source_replica_expression,
2169
+ activity=rule.activity, grouping=rule.grouping, notification=rule.notification, stuck_at=rule.stuck_at, purge_replicas=rule.purge_replicas,
2170
+ ignore_availability=rule.ignore_availability, ignore_account_limit=rule.ignore_account_limit, comments=rule.comments, created_at=rule.created_at,
2171
+ updated_at=rule.updated_at, child_rule_id=rule.child_rule_id, eol_at=rule.eol_at,
2172
+ split_container=rule.split_container, meta=rule.meta).save(session=session)
2173
+ if longterm:
2174
+ models.ReplicationRuleHistory(id=rule.id, subscription_id=rule.subscription_id, account=rule.account, scope=rule.scope, name=rule.name,
2175
+ did_type=rule.did_type, state=rule.state, error=rule.error, rse_expression=rule.rse_expression, copies=rule.copies,
2176
+ expires_at=rule.expires_at, weight=rule.weight, locked=rule.locked, locks_ok_cnt=rule.locks_ok_cnt,
2177
+ locks_replicating_cnt=rule.locks_replicating_cnt, locks_stuck_cnt=rule.locks_stuck_cnt, source_replica_expression=rule.source_replica_expression,
2178
+ activity=rule.activity, grouping=rule.grouping, notification=rule.notification, stuck_at=rule.stuck_at, purge_replicas=rule.purge_replicas,
2179
+ ignore_availability=rule.ignore_availability, ignore_account_limit=rule.ignore_account_limit, comments=rule.comments, created_at=rule.created_at,
2180
+ updated_at=rule.updated_at, child_rule_id=rule.child_rule_id, eol_at=rule.eol_at,
2181
+ split_container=rule.split_container, meta=rule.meta).save(session=session)
2182
+
2183
+
2184
+ @transactional_session
2185
+ def approve_rule(rule_id, approver=None, notify_approvers=True, *, session: "Session"):
2186
+ """
2187
+ Approve a specific replication rule.
2188
+
2189
+ :param rule_id: The rule_id to approve.
2190
+ :param approver: The account which is approving the rule.
2191
+ :param notify_approvers: Notify the other approvers.
2192
+ :param session: The database session in use.
2193
+ :raises: RuleNotFound if no Rule can be found.
2194
+ """
2195
+
2196
+ try:
2197
+ rule = session.query(models.ReplicationRule).filter_by(id=rule_id).one()
2198
+ if rule.state == RuleState.WAITING_APPROVAL:
2199
+ rule.ignore_account_limit = True
2200
+ rule.state = RuleState.INJECT
2201
+ if approver:
2202
+ approver_email = get_account(account=approver, session=session).email
2203
+ if approver_email:
2204
+ approver = '%s (%s)' % (approver, approver_email)
2205
+ else:
2206
+ approver = 'AUTOMATIC'
2207
+ with open('%s/rule_approved_user.tmpl' % config_get('common', 'mailtemplatedir'), 'r') as templatefile:
2208
+ template = Template(templatefile.read())
2209
+ email = get_account(account=rule.account, session=session).email
2210
+ if email:
2211
+ text = template.safe_substitute({'rule_id': str(rule.id),
2212
+ 'expires_at': str(rule.expires_at),
2213
+ 'rse_expression': rule.rse_expression,
2214
+ 'comment': rule.comments,
2215
+ 'scope': rule.scope.external,
2216
+ 'name': rule.name,
2217
+ 'did_type': rule.did_type,
2218
+ 'approver': approver})
2219
+ add_message(event_type='email',
2220
+ payload={'body': text,
2221
+ 'to': [email],
2222
+ 'subject': '[RUCIO] Replication rule %s has been approved' % (str(rule.id))},
2223
+ session=session)
2224
+ # Also notify the other approvers
2225
+ if notify_approvers:
2226
+ with open('%s/rule_approved_admin.tmpl' % config_get('common', 'mailtemplatedir'), 'r') as templatefile:
2227
+ template = Template(templatefile.read())
2228
+ text = template.safe_substitute({'rule_id': str(rule.id),
2229
+ 'approver': approver})
2230
+ vo = rule.account.vo
2231
+ recipients = _create_recipients_list(rse_expression=rule.rse_expression, filter_={'vo': vo}, session=session)
2232
+ for recipient in recipients:
2233
+ add_message(event_type='email',
2234
+ payload={'body': text,
2235
+ 'to': [recipient[0]],
2236
+ 'subject': 'Re: [RUCIO] Request to approve replication rule %s' % (str(rule.id))},
2237
+ session=session)
2238
+ except NoResultFound:
2239
+ raise RuleNotFound('No rule with the id %s found' % (rule_id))
2240
+ except StatementError:
2241
+ raise RucioException('Badly formatted rule id (%s)' % (rule_id))
2242
+
2243
+
2244
+ @transactional_session
2245
+ def deny_rule(rule_id, approver=None, reason=None, *, session: "Session"):
2246
+ """
2247
+ Deny a specific replication rule.
2248
+
2249
+ :param rule_id: The rule_id to approve.
2250
+ :param approver: The account which is denying the rule.
2251
+ :param reason: The reason why the rule was denied.
2252
+ :param session: The database session in use.
2253
+ :raises: RuleNotFound if no Rule can be found.
2254
+ """
2255
+
2256
+ try:
2257
+ rule = session.query(models.ReplicationRule).filter_by(id=rule_id).one()
2258
+ if rule.state == RuleState.WAITING_APPROVAL:
2259
+ with open('%s/rule_denied_user.tmpl' % config_get('common', 'mailtemplatedir'), 'r') as templatefile:
2260
+ template = Template(templatefile.read())
2261
+ email = get_account(account=rule.account, session=session).email
2262
+ if approver:
2263
+ approver_email = get_account(account=approver, session=session).email
2264
+ if approver_email:
2265
+ approver = '%s (%s)' % (approver, approver_email)
2266
+ else:
2267
+ approver = 'AUTOMATIC'
2268
+ if email:
2269
+ email_body = template.safe_substitute({'rule_id': str(rule.id),
2270
+ 'rse_expression': rule.rse_expression,
2271
+ 'comment': rule.comments,
2272
+ 'scope': rule.scope.external,
2273
+ 'name': rule.name,
2274
+ 'did_type': rule.did_type,
2275
+ 'approver': approver,
2276
+ 'reason': reason})
2277
+ add_message(event_type='email',
2278
+ payload={'body': email_body,
2279
+ 'to': [email],
2280
+ 'subject': '[RUCIO] Replication rule %s has been denied' % (str(rule.id))},
2281
+ session=session)
2282
+ delete_rule(rule_id=rule_id, ignore_rule_lock=True, session=session)
2283
+ # Also notify the other approvers
2284
+ with open('%s/rule_denied_admin.tmpl' % config_get('common', 'mailtemplatedir'), 'r') as templatefile:
2285
+ template = Template(templatefile.read())
2286
+ email_body = template.safe_substitute({'rule_id': str(rule.id),
2287
+ 'approver': approver,
2288
+ 'reason': reason})
2289
+ vo = rule.account.vo
2290
+ recipients = _create_recipients_list(rse_expression=rule.rse_expression, filter_={'vo': vo}, session=session)
2291
+ for recipient in recipients:
2292
+ add_message(event_type='email',
2293
+ payload={'body': email_body,
2294
+ 'to': [recipient[0]],
2295
+ 'subject': 'Re: [RUCIO] Request to approve replication rule %s' % (str(rule.id))},
2296
+ session=session)
2297
+ except NoResultFound:
2298
+ raise RuleNotFound('No rule with the id %s found' % rule_id)
2299
+ except StatementError:
2300
+ raise RucioException('Badly formatted rule id (%s)' % rule_id)
2301
+
2302
+
2303
+ @transactional_session
2304
+ def examine_rule(rule_id, *, session: "Session"):
2305
+ """
2306
+ Examine a replication rule for transfer errors.
2307
+
2308
+ :param rule_id: Replication rule id
2309
+ :param session: Session of the db.
2310
+ :returns: Dictionary of informations
2311
+ """
2312
+ result = {'rule_error': None,
2313
+ 'transfers': []}
2314
+
2315
+ try:
2316
+ rule = session.query(models.ReplicationRule).filter_by(id=rule_id).one()
2317
+ if rule.state == RuleState.OK:
2318
+ result['rule_error'] = 'This replication rule is OK'
2319
+ elif rule.state == RuleState.REPLICATING:
2320
+ result['rule_error'] = 'This replication rule is currently REPLICATING'
2321
+ elif rule.state == RuleState.SUSPENDED:
2322
+ result['rule_error'] = 'This replication rule is SUSPENDED'
2323
+ else:
2324
+ result['rule_error'] = rule.error
2325
+ # Get the stuck locks
2326
+ stuck_locks = session.query(models.ReplicaLock).filter_by(rule_id=rule_id, state=LockState.STUCK).all()
2327
+ for lock in stuck_locks:
2328
+ # Get the count of requests in the request_history for each lock
2329
+ transfers = session.query(models.RequestHistory).filter_by(scope=lock.scope, name=lock.name, dest_rse_id=lock.rse_id).order_by(models.RequestHistory.created_at.desc()).all() # pylint: disable=no-member
2330
+ transfer_cnt = len(transfers)
2331
+ # Get the error of the last request that has been tried and also the SOURCE used for the last request
2332
+ last_error, last_source, last_time, sources = None, None, None, []
2333
+ if transfers:
2334
+ last_request = transfers[0]
2335
+ last_error = last_request.state
2336
+ last_time = last_request.created_at
2337
+ last_source = None if last_request.source_rse_id is None else get_rse_name(rse_id=last_request.source_rse_id, session=session)
2338
+ available_replicas = session.query(models.RSEFileAssociation).filter_by(scope=lock.scope, name=lock.name, state=ReplicaState.AVAILABLE).all()
2339
+
2340
+ for replica in available_replicas:
2341
+ sources.append((get_rse_name(rse_id=replica.rse_id, session=session),
2342
+ True if get_rse(rse_id=replica.rse_id, session=session)['availability_read'] else False))
2343
+
2344
+ result['transfers'].append({'scope': lock.scope,
2345
+ 'name': lock.name,
2346
+ 'rse_id': lock.rse_id,
2347
+ 'rse': get_rse_name(rse_id=lock.rse_id, session=session),
2348
+ 'attempts': transfer_cnt,
2349
+ 'last_error': str(last_error),
2350
+ 'last_source': last_source,
2351
+ 'sources': sources,
2352
+ 'last_time': last_time})
2353
+ return result
2354
+ except NoResultFound:
2355
+ raise RuleNotFound('No rule with the id %s found' % (rule_id))
2356
+ except StatementError:
2357
+ raise RucioException('Badly formatted rule id (%s)' % (rule_id))
2358
+
2359
+
2360
+ @transactional_session
2361
+ def get_evaluation_backlog(expiration_time=600, *, session: "Session"):
2362
+ """
2363
+ Counts the number of entries in the rule evaluation backlog.
2364
+ (Number of files to be evaluated)
2365
+
2366
+ :returns: Tuple (Count, Datetime of oldest entry)
2367
+ """
2368
+
2369
+ result = REGION.get('rule_evaluation_backlog', expiration_time=expiration_time)
2370
+ if result is NO_VALUE:
2371
+ result = session.query(func.count(models.UpdatedDID.created_at), func.min(models.UpdatedDID.created_at)).one()
2372
+ REGION.set('rule_evaluation_backlog', result)
2373
+ return result
2374
+
2375
+
2376
+ @transactional_session
2377
+ def release_parent_rule(child_rule_id, remove_parent_expiration=False, *, session: "Session"):
2378
+ """
2379
+ Release a potential parent rule, because the child_rule is OK.
2380
+
2381
+ :param child_rule_id: The child rule id.
2382
+ :param remove_parant_expiration: If true, removes the expiration of the parent rule.
2383
+ :param session: The Database session
2384
+ """
2385
+
2386
+ session.flush()
2387
+
2388
+ parent_rules = session.query(models.ReplicationRule).filter_by(child_rule_id=child_rule_id).\
2389
+ with_hint(models.ReplicationRule, "index(RULES RULES_CHILD_RULE_ID_IDX)", 'oracle').all()
2390
+ for rule in parent_rules:
2391
+ if remove_parent_expiration:
2392
+ rule.expires_at = None
2393
+ rule.child_rule_id = None
2394
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
2395
+
2396
+
2397
+ @transactional_session
2398
+ def __find_missing_locks_and_create_them(datasetfiles, locks, replicas, source_replicas, rseselector, rule, source_rses, *, session: "Session", logger=logging.log):
2399
+ """
2400
+ Find missing locks for a rule and create them.
2401
+
2402
+ :param datasetfiles: Dict holding all datasets and files.
2403
+ :param locks: Dict holding locks.
2404
+ :param replicas: Dict holding replicas.
2405
+ :param source_replicas: Dict holding source replicas.
2406
+ :param rseselector: The RSESelector to be used.
2407
+ :param rule: The rule.
2408
+ :param source_rses: RSE ids for eglible source RSEs.
2409
+ :param session: Session of the db.
2410
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
2411
+ :raises: InsufficientAccountLimit, IntegrityError, InsufficientTargetRSEs
2412
+ :attention: This method modifies the contents of the locks and replicas input parameters.
2413
+ """
2414
+
2415
+ logger(logging.DEBUG, "Finding missing locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
2416
+
2417
+ mod_datasetfiles = [] # List of Datasets and their files in the Tree [{'scope':, 'name':, 'files': []}]
2418
+ # Files are in the format [{'scope':, 'name':, 'bytes':, 'md5':, 'adler32':}]
2419
+
2420
+ for dataset in datasetfiles:
2421
+ mod_files = []
2422
+ preferred_rse_ids = []
2423
+ for file in dataset['files']:
2424
+ if len([lock for lock in locks[(file['scope'], file['name'])] if lock.rule_id == rule.id]) < rule.copies:
2425
+ mod_files.append(file)
2426
+ else:
2427
+ preferred_rse_ids = [lock.rse_id for lock in locks[(file['scope'], file['name'])] if lock.rule_id == rule.id]
2428
+ if mod_files:
2429
+ logger(logging.DEBUG, 'Found missing locks for rule %s, creating them now', str(rule.id))
2430
+ mod_datasetfiles.append({'scope': dataset['scope'], 'name': dataset['name'], 'files': mod_files})
2431
+ __create_locks_replicas_transfers(datasetfiles=mod_datasetfiles,
2432
+ locks=locks,
2433
+ replicas=replicas,
2434
+ source_replicas=source_replicas,
2435
+ rseselector=rseselector,
2436
+ rule=rule,
2437
+ preferred_rse_ids=preferred_rse_ids,
2438
+ source_rses=source_rses,
2439
+ session=session)
2440
+
2441
+ logger(logging.DEBUG, "Finished finding missing locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
2442
+
2443
+
2444
+ @transactional_session
2445
+ def __find_surplus_locks_and_remove_them(datasetfiles, locks, replicas, source_replicas, rseselector, rule, source_rses, *, session: "Session", logger=logging.log):
2446
+ """
2447
+ Find surplocks locks for a rule and delete them.
2448
+
2449
+ :param datasetfiles: Dict holding all datasets and files.
2450
+ :param locks: Dict holding locks.
2451
+ :param replicas: Dict holding replicas.
2452
+ :param source_replicas: Dict holding all source replicas.
2453
+ :param rseselector: The RSESelector to be used.
2454
+ :param rule: The rule.
2455
+ :param source_rses: RSE ids for eglible source RSEs.
2456
+ :param session: Session of the db.
2457
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
2458
+ :raises: InsufficientAccountLimit, IntegrityError, InsufficientTargetRSEs
2459
+ :attention: This method modifies the contents of the locks and replicas input parameters.
2460
+ """
2461
+
2462
+ logger(logging.DEBUG, "Finding surplus locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
2463
+
2464
+ account_counter_decreases = {} # {'rse_id': [file_size, file_size, file_size]}
2465
+
2466
+ # Put all the files in one dictionary
2467
+ files = {}
2468
+ for ds in datasetfiles:
2469
+ for file in ds['files']:
2470
+ files[(file['scope'], file['name'])] = True
2471
+
2472
+ for key in locks:
2473
+ if key not in files:
2474
+ # The lock needs to be removed
2475
+ for lock in deepcopy(locks[key]):
2476
+ if lock.rule_id == rule.id:
2477
+ __delete_lock_and_update_replica(lock=lock, purge_replicas=rule.purge_replicas, nowait=True, session=session)
2478
+ if lock.rse_id not in account_counter_decreases:
2479
+ account_counter_decreases[lock.rse_id] = []
2480
+ account_counter_decreases[lock.rse_id].append(lock.bytes)
2481
+ if lock.state == LockState.OK:
2482
+ rule.locks_ok_cnt -= 1
2483
+ elif lock.state == LockState.REPLICATING:
2484
+ rule.locks_replicating_cnt -= 1
2485
+ elif lock.state == LockState.STUCK:
2486
+ rule.locks_stuck_cnt -= 1
2487
+ locks[key].remove(lock)
2488
+
2489
+ logger(logging.DEBUG, "Finished finding surplus locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
2490
+
2491
+
2492
+ @transactional_session
2493
+ def __find_stuck_locks_and_repair_them(datasetfiles, locks, replicas, source_replicas, rseselector, rule, source_rses, *, session: "Session", logger=logging.log):
2494
+ """
2495
+ Find stuck locks for a rule and repair them.
2496
+
2497
+ :param datasetfiles: Dict holding all datasets and files.
2498
+ :param locks: Dict holding locks.
2499
+ :param replicas: Dict holding replicas.
2500
+ :param source_replicas: Dict holding source replicas.
2501
+ :param rseselector: The RSESelector to be used.
2502
+ :param rule: The rule.
2503
+ :param source_rses: RSE ids of eglible source RSEs.
2504
+ :param session: Session of the db.
2505
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
2506
+ :raises: InsufficientAccountLimit, IntegrityError, InsufficientTargetRSEs
2507
+ :attention: This method modifies the contents of the locks and replicas input parameters.
2508
+ """
2509
+
2510
+ logger(logging.DEBUG, "Finding and repairing stuck locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
2511
+
2512
+ replicas_to_create, locks_to_create, transfers_to_create,\
2513
+ locks_to_delete = repair_stuck_locks_and_apply_rule_grouping(datasetfiles=datasetfiles,
2514
+ locks=locks,
2515
+ replicas=replicas,
2516
+ source_replicas=source_replicas,
2517
+ rseselector=rseselector,
2518
+ rule=rule,
2519
+ source_rses=source_rses,
2520
+ session=session)
2521
+ # Add the replicas
2522
+ session.add_all([item for sublist in replicas_to_create.values() for item in sublist])
2523
+ session.flush()
2524
+
2525
+ # Add the locks
2526
+ session.add_all([item for sublist in locks_to_create.values() for item in sublist])
2527
+ session.flush()
2528
+
2529
+ # Increase rse_counters
2530
+ for rse_id in replicas_to_create.keys():
2531
+ rse_counter.increase(rse_id=rse_id, files=len(replicas_to_create[rse_id]), bytes_=sum([replica.bytes for replica in replicas_to_create[rse_id]]), session=session)
2532
+
2533
+ # Increase account_counters
2534
+ for rse_id in locks_to_create.keys():
2535
+ account_counter.increase(rse_id=rse_id, account=rule.account, files=len(locks_to_create[rse_id]), bytes_=sum([lock.bytes for lock in locks_to_create[rse_id]]), session=session)
2536
+
2537
+ # Decrease account_counters
2538
+ for rse_id in locks_to_delete:
2539
+ account_counter.decrease(rse_id=rse_id, account=rule.account, files=len(locks_to_delete[rse_id]), bytes_=sum([lock.bytes for lock in locks_to_delete[rse_id]]), session=session)
2540
+
2541
+ # Delete the locks:
2542
+ for lock in [item for sublist in locks_to_delete.values() for item in sublist]:
2543
+ session.delete(lock)
2544
+
2545
+ # Add the transfers
2546
+ request_core.queue_requests(requests=transfers_to_create, session=session)
2547
+ session.flush()
2548
+ logger(logging.DEBUG, "Finished finding and repairing stuck locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
2549
+
2550
+
2551
+ @transactional_session
2552
+ def __evaluate_did_detach(eval_did, *, session: "Session", logger=logging.log):
2553
+ """
2554
+ Evaluate a parent did which has children removed.
2555
+
2556
+ :param eval_did: The did object in use.
2557
+ :param session: The database session in use.
2558
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
2559
+ """
2560
+
2561
+ logger(logging.INFO, "Re-Evaluating did %s:%s for DETACH", eval_did.scope, eval_did.name)
2562
+ force_epoch = config_get('rules', 'force_epoch_when_detach', default=False, session=session)
2563
+
2564
+ with METRICS.timer('evaluate_did_detach.total'):
2565
+ # Get all parent DID's
2566
+ parent_dids = rucio.core.did.list_all_parent_dids(scope=eval_did.scope, name=eval_did.name, session=session)
2567
+
2568
+ # Get all RR from parents and eval_did
2569
+ rules = session.query(models.ReplicationRule).filter_by(scope=eval_did.scope, name=eval_did.name).with_for_update(nowait=True).all()
2570
+ for did in parent_dids:
2571
+ rules.extend(session.query(models.ReplicationRule).filter_by(scope=did['scope'], name=did['name']).with_for_update(nowait=True).all())
2572
+
2573
+ # Iterate rules and delete locks
2574
+ transfers_to_delete = [] # [{'scope': , 'name':, 'rse_id':}]
2575
+ account_counter_decreases = {} # {'rse_id': [file_size, file_size, file_size]}
2576
+ for rule in rules:
2577
+ # Get all the files covering this rule
2578
+ files = {}
2579
+ for file in rucio.core.did.list_files(scope=rule.scope, name=rule.name, session=session):
2580
+ files[(file['scope'], file['name'])] = True
2581
+ logger(logging.DEBUG, "Removing locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
2582
+ rule_locks_ok_cnt_before = rule.locks_ok_cnt
2583
+ query = session.query(models.ReplicaLock).filter_by(rule_id=rule.id)
2584
+ for lock in query:
2585
+ if (lock.scope, lock.name) not in files:
2586
+ if __delete_lock_and_update_replica(lock=lock, purge_replicas=force_epoch or rule.purge_replicas, nowait=True, session=session):
2587
+ transfers_to_delete.append({'scope': lock.scope, 'name': lock.name, 'rse_id': lock.rse_id})
2588
+ if lock.rse_id not in account_counter_decreases:
2589
+ account_counter_decreases[lock.rse_id] = []
2590
+ account_counter_decreases[lock.rse_id].append(lock.bytes)
2591
+ if lock.state == LockState.OK:
2592
+ rule.locks_ok_cnt -= 1
2593
+ elif lock.state == LockState.REPLICATING:
2594
+ rule.locks_replicating_cnt -= 1
2595
+ elif lock.state == LockState.STUCK:
2596
+ rule.locks_stuck_cnt -= 1
2597
+ logger(logging.DEBUG, "Finished removing locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
2598
+
2599
+ if eval_did.did_type == DIDType.CONTAINER:
2600
+ # Get all datasets of eval_did
2601
+ child_datasets = {}
2602
+ for ds in rucio.core.did.list_child_datasets(scope=rule.scope, name=rule.name, session=session):
2603
+ child_datasets[(ds['scope'], ds['name'])] = True
2604
+ logger(logging.DEBUG, "Removing dataset_locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
2605
+ query = session.query(models.DatasetLock).filter_by(rule_id=rule.id)
2606
+ for ds_lock in query:
2607
+ if (ds_lock.scope, ds_lock.name) not in child_datasets:
2608
+ ds_lock.delete(flush=False, session=session)
2609
+ logger(logging.DEBUG, "Finished removing dataset_locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
2610
+
2611
+ if rule.state == RuleState.SUSPENDED:
2612
+ pass
2613
+ elif rule.state == RuleState.STUCK:
2614
+ pass
2615
+ elif rule.locks_replicating_cnt == 0 and rule.locks_stuck_cnt == 0:
2616
+ rule.state = RuleState.OK
2617
+ if rule.grouping != RuleGrouping.NONE:
2618
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.OK})
2619
+ session.flush()
2620
+ if rule_locks_ok_cnt_before != rule.locks_ok_cnt:
2621
+ generate_rule_notifications(rule=rule, session=session)
2622
+ generate_email_for_rule_ok_notification(rule=rule, session=session)
2623
+ # Try to release potential parent rules
2624
+ release_parent_rule(child_rule_id=rule.id, session=session)
2625
+
2626
+ # Insert rule history
2627
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
2628
+
2629
+ session.flush()
2630
+
2631
+ # Decrease account_counters
2632
+ for rse_id in account_counter_decreases:
2633
+ account_counter.decrease(rse_id=rse_id, account=rule.account, files=len(account_counter_decreases[rse_id]), bytes_=sum(account_counter_decreases[rse_id]), session=session)
2634
+
2635
+ for transfer in transfers_to_delete:
2636
+ transfers_to_cancel = request_core.cancel_request_did(scope=transfer['scope'], name=transfer['name'], dest_rse_id=transfer['rse_id'], session=session)
2637
+ transfer_core.cancel_transfers(transfers_to_cancel)
2638
+
2639
+
2640
+ @transactional_session
2641
+ def __oldest_file_under(scope, name, *, session: "Session"):
2642
+ """
2643
+ Finds oldest file in oldest container/dataset in the container or the dataset, recursively.
2644
+ Oldest means attached to its parent first.
2645
+
2646
+ :param scope: dataset or container scope
2647
+ :param name: dataset or container name
2648
+ :returns: tuple (scope, name) or None
2649
+ """
2650
+ children = session.query(models.DataIdentifierAssociation) \
2651
+ .filter(models.DataIdentifierAssociation.scope == scope,
2652
+ models.DataIdentifierAssociation.name == name) \
2653
+ .order_by(models.DataIdentifierAssociation.created_at)
2654
+ for child in children:
2655
+ if child.child_type == DIDType.FILE:
2656
+ return child.child_scope, child.child_name
2657
+ elif child.child_type in (DIDType.DATASET, DIDType.CONTAINER):
2658
+ out = __oldest_file_under(child.child_scope, child.child_name, session=session)
2659
+ if out:
2660
+ return out
2661
+ return None
2662
+
2663
+
2664
+ @transactional_session
2665
+ def __evaluate_did_attach(eval_did, *, session: "Session", logger=logging.log):
2666
+ """
2667
+ Evaluate a parent did which has new childs
2668
+
2669
+ :param eval_did: The did object in use.
2670
+ :param session: The database session in use.
2671
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
2672
+ :raises: ReplicationRuleCreationTemporaryFailed
2673
+ """
2674
+
2675
+ logger(logging.INFO, "Re-Evaluating did %s:%s for ATTACH", eval_did.scope, eval_did.name)
2676
+
2677
+ with METRICS.timer('evaluate_did_attach.total'):
2678
+ # Get all parent DID's
2679
+ with METRICS.timer('evaluate_did_attach.list_parent_dids'):
2680
+ parent_dids = rucio.core.did.list_all_parent_dids(scope=eval_did.scope, name=eval_did.name, session=session)
2681
+
2682
+ # Get immediate new child DID's
2683
+ with METRICS.timer('evaluate_did_attach.list_new_child_dids'):
2684
+ new_child_dids = session.query(models.DataIdentifierAssociation)\
2685
+ .with_hint(models.DataIdentifierAssociation, "INDEX_RS_ASC(contents contents_pk)", 'oracle')\
2686
+ .filter(models.DataIdentifierAssociation.scope == eval_did.scope,
2687
+ models.DataIdentifierAssociation.name == eval_did.name,
2688
+ models.DataIdentifierAssociation.rule_evaluation == True).all() # noqa
2689
+
2690
+ if new_child_dids:
2691
+ # Get all unsuspended RR from parents and eval_did
2692
+ with METRICS.timer('evaluate_did_attach.get_rules'):
2693
+ rule_clauses = []
2694
+ for did in parent_dids:
2695
+ rule_clauses.append(and_(models.ReplicationRule.scope == did['scope'],
2696
+ models.ReplicationRule.name == did['name']))
2697
+ rule_clauses.append(and_(models.ReplicationRule.scope == eval_did.scope,
2698
+ models.ReplicationRule.name == eval_did.name))
2699
+ rules = session.query(models.ReplicationRule).filter(
2700
+ or_(*rule_clauses),
2701
+ models.ReplicationRule.state != RuleState.SUSPENDED,
2702
+ models.ReplicationRule.state != RuleState.WAITING_APPROVAL,
2703
+ models.ReplicationRule.state != RuleState.INJECT).with_for_update(nowait=True).all()
2704
+
2705
+ if rules:
2706
+ # Resolve the new_child_dids to its locks
2707
+ with METRICS.timer('evaluate_did_attach.resolve_did_to_locks_and_replicas'):
2708
+ # Resolve the rules to possible target rses:
2709
+ possible_rses = []
2710
+ source_rses = []
2711
+ for rule in rules:
2712
+ try:
2713
+ vo = rule.account.vo
2714
+ if rule.source_replica_expression:
2715
+ source_rses.extend(parse_expression(rule.source_replica_expression, filter_={'vo': vo}, session=session))
2716
+
2717
+ # if rule.ignore_availability:
2718
+ possible_rses.extend(parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session))
2719
+ # else:
2720
+ # possible_rses.extend(parse_expression(rule.rse_expression, filter={'availability_write': True}, session=session))
2721
+ except (InvalidRSEExpression, RSEWriteBlocked):
2722
+ possible_rses = []
2723
+ break
2724
+
2725
+ source_rses = list(set([rse['id'] for rse in source_rses]))
2726
+ possible_rses = list(set([rse['id'] for rse in possible_rses]))
2727
+
2728
+ datasetfiles, locks, replicas, source_replicas = __resolve_dids_to_locks_and_replicas(dids=new_child_dids,
2729
+ nowait=True,
2730
+ restrict_rses=possible_rses,
2731
+ source_rses=source_rses,
2732
+ session=session)
2733
+
2734
+ # Evaluate the replication rules
2735
+ with METRICS.timer('evaluate_did_attach.evaluate_rules'):
2736
+ for rule in rules:
2737
+ rule_locks_ok_cnt_before = rule.locks_ok_cnt
2738
+
2739
+ # 1. Resolve the rse_expression into a list of RSE-ids
2740
+ try:
2741
+ vo = rule.account.vo
2742
+ if rule.ignore_availability:
2743
+ rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
2744
+ else:
2745
+ rses = parse_expression(rule.rse_expression, filter_={'vo': vo, 'availability_write': True}, session=session)
2746
+ source_rses = []
2747
+ if rule.source_replica_expression:
2748
+ source_rses = parse_expression(rule.source_replica_expression, filter_={'vo': vo}, session=session)
2749
+ except (InvalidRSEExpression, RSEWriteBlocked) as error:
2750
+ rule.state = RuleState.STUCK
2751
+ rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
2752
+ rule.save(session=session)
2753
+ # Insert rule history
2754
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
2755
+ # Try to update the DatasetLocks
2756
+ if rule.grouping != RuleGrouping.NONE:
2757
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.STUCK})
2758
+ continue
2759
+
2760
+ # 2. Create the RSE Selector
2761
+ try:
2762
+ rseselector = RSESelector(account=rule.account,
2763
+ rses=rses,
2764
+ weight=rule.weight,
2765
+ copies=rule.copies,
2766
+ ignore_account_limit=rule.ignore_account_limit,
2767
+ session=session)
2768
+ except (InvalidRuleWeight, InsufficientTargetRSEs, InsufficientAccountLimit, RSEOverQuota) as error:
2769
+ rule.state = RuleState.STUCK
2770
+ rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
2771
+ rule.save(session=session)
2772
+ # Insert rule history
2773
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
2774
+ # Try to update the DatasetLocks
2775
+ if rule.grouping != RuleGrouping.NONE:
2776
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.STUCK})
2777
+ continue
2778
+
2779
+ # 3. Apply the Replication rule to the Files
2780
+ preferred_rse_ids = []
2781
+ brother_scope_name = None
2782
+
2783
+ if rule.grouping == RuleGrouping.ALL:
2784
+ # get oldest file, recursively, in the rule owner
2785
+ brother_scope_name = __oldest_file_under(rule["scope"], rule["name"], session=session)
2786
+
2787
+ elif rule.grouping == RuleGrouping.DATASET and new_child_dids[0].child_type == DIDType.FILE:
2788
+ # get oldest file in the dataset being evaluated
2789
+ brother_scope_name = __oldest_file_under(eval_did.scope, eval_did.name, session=session)
2790
+
2791
+ if brother_scope_name:
2792
+ scope, name = brother_scope_name
2793
+ file_locks = rucio.core.lock.get_replica_locks(scope=scope, name=name, nowait=True, session=session)
2794
+ preferred_rse_ids = [
2795
+ lock['rse_id']
2796
+ for lock in file_locks
2797
+ if lock['rse_id'] in [rse['id'] for rse in rses] and lock['rule_id'] == rule.id
2798
+ ]
2799
+
2800
+ locks_stuck_before = rule.locks_stuck_cnt
2801
+ try:
2802
+ __create_locks_replicas_transfers(datasetfiles=datasetfiles,
2803
+ locks=locks,
2804
+ replicas=replicas,
2805
+ source_replicas=source_replicas,
2806
+ rseselector=rseselector,
2807
+ rule=rule,
2808
+ preferred_rse_ids=preferred_rse_ids,
2809
+ source_rses=[rse['id'] for rse in source_rses],
2810
+ session=session)
2811
+ except (InsufficientAccountLimit, InsufficientTargetRSEs, RSEOverQuota) as error:
2812
+ rule.state = RuleState.STUCK
2813
+ rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
2814
+ rule.save(session=session)
2815
+ # Insert rule history
2816
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
2817
+ # Try to update the DatasetLocks
2818
+ if rule.grouping != RuleGrouping.NONE:
2819
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.STUCK})
2820
+ continue
2821
+
2822
+ # 4. Update the Rule State
2823
+ if rule.state == RuleState.STUCK:
2824
+ pass
2825
+ elif rule.locks_stuck_cnt > 0:
2826
+ if locks_stuck_before != rule.locks_stuck_cnt:
2827
+ rule.state = RuleState.STUCK
2828
+ rule.error = 'MissingSourceReplica'
2829
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.STUCK})
2830
+ elif rule.locks_replicating_cnt > 0:
2831
+ rule.state = RuleState.REPLICATING
2832
+ if rule.grouping != RuleGrouping.NONE:
2833
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.REPLICATING})
2834
+ elif rule.locks_replicating_cnt == 0 and rule.locks_stuck_cnt == 0:
2835
+ rule.state = RuleState.OK
2836
+ if rule.grouping != RuleGrouping.NONE:
2837
+ session.query(models.DatasetLock).filter_by(rule_id=rule.id).update({'state': LockState.OK})
2838
+ session.flush()
2839
+ if rule_locks_ok_cnt_before < rule.locks_ok_cnt:
2840
+ generate_rule_notifications(rule=rule, session=session)
2841
+ generate_email_for_rule_ok_notification(rule=rule, session=session)
2842
+
2843
+ # Insert rule history
2844
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
2845
+
2846
+ # Unflage the dids
2847
+ with METRICS.timer('evaluate_did_attach.update_did'):
2848
+ for did in new_child_dids:
2849
+ did.rule_evaluation = None
2850
+
2851
+ session.flush()
2852
+
2853
+
2854
+ @transactional_session
2855
+ def __resolve_did_to_locks_and_replicas(did, nowait=False, restrict_rses=None, source_rses=None, only_stuck=False, *, session: "Session"):
2856
+ """
2857
+ Resolves a did to its constituent childs and reads the locks and replicas of all the constituent files.
2858
+
2859
+ :param did: The db object of the did the rule is applied on.
2860
+ :param nowait: Nowait parameter for the FOR UPDATE statement.
2861
+ :param restrict_rses: Possible rses of the rule, so only these replica/locks should be considered.
2862
+ :param source_rses: Source rses for this rule. These replicas are not row-locked.
2863
+ :param only_stuck: Get results only for STUCK locks, if True.
2864
+ :param session: Session of the db.
2865
+ :returns: (datasetfiles, locks, replicas)
2866
+ """
2867
+
2868
+ datasetfiles = [] # List of Datasets and their files in the Tree [{'scope':, 'name':, 'files': []}]
2869
+ # Files are in the format [{'scope':, 'name':, 'bytes':, 'md5':, 'adler32':}]
2870
+ locks = {} # {(scope,name): [SQLAlchemy]}
2871
+ replicas = {} # {(scope, name): [SQLAlchemy]}
2872
+ source_replicas = {} # {(scope, name): [rse_id]
2873
+
2874
+ if did.did_type == DIDType.FILE:
2875
+ datasetfiles = [{'scope': None,
2876
+ 'name': None,
2877
+ 'files': [{'scope': did.scope,
2878
+ 'name': did.name,
2879
+ 'bytes': did.bytes,
2880
+ 'md5': did.md5,
2881
+ 'adler32': did.adler32}]}]
2882
+ locks[(did.scope, did.name)] = rucio.core.lock.get_replica_locks(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, session=session)
2883
+ replicas[(did.scope, did.name)] = rucio.core.replica.get_and_lock_file_replicas(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, session=session)
2884
+ if source_rses:
2885
+ source_replicas[(did.scope, did.name)] = rucio.core.replica.get_source_replicas(scope=did.scope, name=did.name, source_rses=source_rses, session=session)
2886
+
2887
+ elif did.did_type == DIDType.DATASET and only_stuck:
2888
+ files = []
2889
+ locks = rucio.core.lock.get_files_and_replica_locks_of_dataset(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, only_stuck=True, session=session)
2890
+ for file in locks:
2891
+ file_did = rucio.core.did.get_did(scope=file[0], name=file[1], session=session)
2892
+ files.append({'scope': file[0], 'name': file[1], 'bytes': file_did['bytes'], 'md5': file_did['md5'], 'adler32': file_did['adler32']})
2893
+ replicas[(file[0], file[1])] = rucio.core.replica.get_and_lock_file_replicas(scope=file[0], name=file[1], nowait=nowait, restrict_rses=restrict_rses, session=session)
2894
+ if source_rses:
2895
+ source_replicas[(file[0], file[1])] = rucio.core.replica.get_source_replicas(scope=file[0], name=file[1], source_rses=source_rses, session=session)
2896
+ datasetfiles = [{'scope': did.scope,
2897
+ 'name': did.name,
2898
+ 'files': files}]
2899
+
2900
+ elif did.did_type == DIDType.DATASET:
2901
+ files, replicas = rucio.core.replica.get_and_lock_file_replicas_for_dataset(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, session=session)
2902
+ if source_rses:
2903
+ source_replicas = rucio.core.replica.get_source_replicas_for_dataset(scope=did.scope, name=did.name, source_rses=source_rses, session=session)
2904
+ datasetfiles = [{'scope': did.scope,
2905
+ 'name': did.name,
2906
+ 'files': files}]
2907
+ locks = rucio.core.lock.get_files_and_replica_locks_of_dataset(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, session=session)
2908
+
2909
+ elif did.did_type == DIDType.CONTAINER and only_stuck:
2910
+
2911
+ for dataset in rucio.core.did.list_child_datasets(scope=did.scope, name=did.name, session=session):
2912
+ files = []
2913
+ tmp_locks = rucio.core.lock.get_files_and_replica_locks_of_dataset(scope=dataset['scope'], name=dataset['name'], nowait=nowait, restrict_rses=restrict_rses, only_stuck=True, session=session)
2914
+ locks = dict(list(locks.items()) + list(tmp_locks.items()))
2915
+ for file in tmp_locks:
2916
+ file_did = rucio.core.did.get_did(scope=file[0], name=file[1], session=session)
2917
+ files.append({'scope': file[0], 'name': file[1], 'bytes': file_did['bytes'], 'md5': file_did['md5'], 'adler32': file_did['adler32']})
2918
+ replicas[(file[0], file[1])] = rucio.core.replica.get_and_lock_file_replicas(scope=file[0], name=file[1], nowait=nowait, restrict_rses=restrict_rses, session=session)
2919
+ if source_rses:
2920
+ source_replicas[(file[0], file[1])] = rucio.core.replica.get_source_replicas(scope=file[0], name=file[1], source_rses=source_rses, session=session)
2921
+ datasetfiles.append({'scope': dataset['scope'],
2922
+ 'name': dataset['name'],
2923
+ 'files': files})
2924
+
2925
+ elif did.did_type == DIDType.CONTAINER:
2926
+
2927
+ for dataset in rucio.core.did.list_child_datasets(scope=did.scope, name=did.name, session=session):
2928
+ files, tmp_replicas = rucio.core.replica.get_and_lock_file_replicas_for_dataset(scope=dataset['scope'], name=dataset['name'], nowait=nowait, restrict_rses=restrict_rses, session=session)
2929
+ if source_rses:
2930
+ tmp_source_replicas = rucio.core.replica.get_source_replicas_for_dataset(scope=dataset['scope'], name=dataset['name'], source_rses=source_rses, session=session)
2931
+ source_replicas = dict(list(source_replicas.items()) + list(tmp_source_replicas.items()))
2932
+ tmp_locks = rucio.core.lock.get_files_and_replica_locks_of_dataset(scope=dataset['scope'], name=dataset['name'], nowait=nowait, restrict_rses=restrict_rses, session=session)
2933
+ datasetfiles.append({'scope': dataset['scope'],
2934
+ 'name': dataset['name'],
2935
+ 'files': files})
2936
+ replicas = dict(list(replicas.items()) + list(tmp_replicas.items()))
2937
+ locks = dict(list(locks.items()) + list(tmp_locks.items()))
2938
+
2939
+ # order datasetfiles for deterministic result
2940
+ try:
2941
+ datasetfiles = sorted(datasetfiles, key=lambda x: "%s%s" % (x['scope'], x['name']))
2942
+ except Exception:
2943
+ pass
2944
+
2945
+ else:
2946
+ raise InvalidReplicationRule('The did \"%s:%s\" has been deleted.' % (did.scope, did.name))
2947
+
2948
+ return datasetfiles, locks, replicas, source_replicas
2949
+
2950
+
2951
+ @transactional_session
2952
+ def __resolve_dids_to_locks_and_replicas(dids, nowait=False, restrict_rses=[], source_rses=None, *, session: "Session"):
2953
+ """
2954
+ Resolves a list of dids to its constituent childs and reads the locks and replicas of all the constituent files.
2955
+
2956
+ :param dids: The list of DIDAssociation objects.
2957
+ :param nowait: Nowait parameter for the FOR UPDATE statement.
2958
+ :param restrict_rses: Possible rses of the rule, so only these replica/locks should be considered.
2959
+ :param source_rses: Source rses for this rule. These replicas are not row-locked.
2960
+ :param session: Session of the db.
2961
+ :returns: (datasetfiles, locks, replicas)
2962
+ """
2963
+
2964
+ datasetfiles = [] # List of Datasets and their files in the Tree [{'scope':, 'name':, 'files': []}]
2965
+ # Files are in the format [{'scope':, 'name':, 'bytes':, 'md5':, 'adler32':}]
2966
+ locks = {} # {(scope,name): [SQLAlchemy]}
2967
+ replicas = {} # {(scope, name): [SQLAlchemy]}
2968
+ source_replicas = {} # {(scope, name): [rse_id]
2969
+
2970
+ if dids[0].child_type == DIDType.FILE:
2971
+ # All the dids will be files!
2972
+ # Prepare the datasetfiles
2973
+ files = []
2974
+ for did in dids:
2975
+ files.append({'scope': did.child_scope,
2976
+ 'name': did.child_name,
2977
+ 'bytes': did.bytes,
2978
+ 'md5': did.md5,
2979
+ 'adler32': did.adler32})
2980
+ locks[(did.child_scope, did.child_name)] = []
2981
+ replicas[(did.child_scope, did.child_name)] = []
2982
+ source_replicas[(did.child_scope, did.child_name)] = []
2983
+ datasetfiles = [{'scope': dids[0].scope, 'name': dids[0].name, 'files': files}]
2984
+
2985
+ # Prepare the locks and files
2986
+ lock_clauses = []
2987
+ replica_clauses = []
2988
+ for did in dids:
2989
+ lock_clauses.append(and_(models.ReplicaLock.scope == did.child_scope,
2990
+ models.ReplicaLock.name == did.child_name))
2991
+ replica_clauses.append(and_(models.RSEFileAssociation.scope == did.child_scope,
2992
+ models.RSEFileAssociation.name == did.child_name))
2993
+ lock_clause_chunks = [lock_clauses[x:x + 10] for x in range(0, len(lock_clauses), 10)]
2994
+ replica_clause_chunks = [replica_clauses[x:x + 10] for x in range(0, len(replica_clauses), 10)]
2995
+
2996
+ replicas_rse_clause = []
2997
+ source_replicas_rse_clause = []
2998
+ locks_rse_clause = []
2999
+ if restrict_rses:
3000
+ for rse_id in restrict_rses:
3001
+ replicas_rse_clause.append(models.RSEFileAssociation.rse_id == rse_id)
3002
+ locks_rse_clause.append(models.ReplicaLock.rse_id == rse_id)
3003
+ if source_rses:
3004
+ for rse_id in source_rses:
3005
+ source_replicas_rse_clause.append(models.RSEFileAssociation.rse_id == rse_id)
3006
+
3007
+ for lock_clause_chunk in lock_clause_chunks:
3008
+ if locks_rse_clause:
3009
+ tmp_locks = session.query(models.ReplicaLock).filter(or_(*lock_clause_chunk), or_(*locks_rse_clause))\
3010
+ .with_hint(models.ReplicaLock, "index(LOCKS LOCKS_PK)", 'oracle')\
3011
+ .with_for_update(nowait=nowait).all()
3012
+ else:
3013
+ tmp_locks = session.query(models.ReplicaLock).filter(or_(*lock_clause_chunk))\
3014
+ .with_hint(models.ReplicaLock, "index(LOCKS LOCKS_PK)", 'oracle')\
3015
+ .with_for_update(nowait=nowait).all()
3016
+ for lock in tmp_locks:
3017
+ if (lock.scope, lock.name) not in locks:
3018
+ locks[(lock.scope, lock.name)] = [lock]
3019
+ else:
3020
+ locks[(lock.scope, lock.name)].append(lock)
3021
+
3022
+ for replica_clause_chunk in replica_clause_chunks:
3023
+ if replicas_rse_clause:
3024
+ tmp_replicas = session.query(models.RSEFileAssociation).filter(or_(*replica_clause_chunk), or_(*replicas_rse_clause), models.RSEFileAssociation.state != ReplicaState.BEING_DELETED)\
3025
+ .with_hint(models.RSEFileAssociation, "index(REPLICAS REPLICAS_PK)", 'oracle')\
3026
+ .with_for_update(nowait=nowait).all()
3027
+ else:
3028
+ tmp_replicas = session.query(models.RSEFileAssociation).filter(or_(*replica_clause_chunk), models.RSEFileAssociation.state != ReplicaState.BEING_DELETED)\
3029
+ .with_hint(models.RSEFileAssociation, "index(REPLICAS REPLICAS_PK)", 'oracle')\
3030
+ .with_for_update(nowait=nowait).all()
3031
+ for replica in tmp_replicas:
3032
+ if (replica.scope, replica.name) not in replicas:
3033
+ replicas[(replica.scope, replica.name)] = [replica]
3034
+ else:
3035
+ replicas[(replica.scope, replica.name)].append(replica)
3036
+
3037
+ if source_rses:
3038
+ for replica_clause_chunk in replica_clause_chunks:
3039
+ tmp_source_replicas = session.query(models.RSEFileAssociation.scope, models.RSEFileAssociation.name, models.RSEFileAssociation.rse_id).\
3040
+ filter(or_(*replica_clause_chunk), or_(*source_replicas_rse_clause), models.RSEFileAssociation.state == ReplicaState.AVAILABLE)\
3041
+ .with_hint(models.RSEFileAssociation, "index(REPLICAS REPLICAS_PK)", 'oracle').all()
3042
+ for scope, name, rse_id in tmp_source_replicas:
3043
+ if (scope, name) not in source_replicas:
3044
+ source_replicas[(scope, name)] = [rse_id]
3045
+ else:
3046
+ source_replicas[(scope, name)].append(rse_id)
3047
+ else:
3048
+ # The evaluate_dids will be containers and/or datasets
3049
+ for did in dids:
3050
+ real_did = session.query(models.DataIdentifier).filter(models.DataIdentifier.scope == did.child_scope, models.DataIdentifier.name == did.child_name).one()
3051
+ tmp_datasetfiles, tmp_locks, tmp_replicas, tmp_source_replicas = __resolve_did_to_locks_and_replicas(did=real_did,
3052
+ nowait=nowait,
3053
+ restrict_rses=restrict_rses,
3054
+ source_rses=source_rses,
3055
+ session=session)
3056
+ datasetfiles.extend(tmp_datasetfiles)
3057
+ locks.update(tmp_locks)
3058
+ replicas.update(tmp_replicas)
3059
+ source_replicas.update(tmp_source_replicas)
3060
+ return datasetfiles, locks, replicas, source_replicas
3061
+
3062
+
3063
+ @transactional_session
3064
+ def __create_locks_replicas_transfers(datasetfiles, locks, replicas, source_replicas, rseselector, rule, preferred_rse_ids=[], source_rses=[], *, session: "Session", logger=logging.log):
3065
+ """
3066
+ Apply a created replication rule to a set of files
3067
+
3068
+ :param datasetfiles: Dict holding all datasets and files.
3069
+ :param locks: Dict holding locks.
3070
+ :param replicas: Dict holding replicas.
3071
+ :param source_replicas: Dict holding source replicas.
3072
+ :param rseselector: The RSESelector to be used.
3073
+ :param rule: The rule.
3074
+ :param preferred_rse_ids: Preferred RSE's to select.
3075
+ :param source_rses: RSE ids of eglible source replicas.
3076
+ :param session: Session of the db.
3077
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
3078
+ :raises: InsufficientAccountLimit, IntegrityError, InsufficientTargetRSEs, RSEOverQuota
3079
+ :attention: This method modifies the contents of the locks and replicas input parameters.
3080
+ """
3081
+
3082
+ logger(logging.DEBUG, "Creating locks and replicas for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
3083
+
3084
+ replicas_to_create, locks_to_create, transfers_to_create = apply_rule_grouping(datasetfiles=datasetfiles,
3085
+ locks=locks,
3086
+ replicas=replicas,
3087
+ source_replicas=source_replicas,
3088
+ rseselector=rseselector,
3089
+ rule=rule,
3090
+ preferred_rse_ids=preferred_rse_ids,
3091
+ source_rses=source_rses,
3092
+ session=session)
3093
+ # Add the replicas
3094
+ session.add_all([item for sublist in replicas_to_create.values() for item in sublist])
3095
+ session.flush()
3096
+
3097
+ # Add the locks
3098
+ session.add_all([item for sublist in locks_to_create.values() for item in sublist])
3099
+ session.flush()
3100
+
3101
+ # Increase rse_counters
3102
+ for rse_id in replicas_to_create.keys():
3103
+ rse_counter.increase(rse_id=rse_id, files=len(replicas_to_create[rse_id]), bytes_=sum([replica.bytes for replica in replicas_to_create[rse_id]]), session=session)
3104
+
3105
+ # Increase account_counters
3106
+ for rse_id in locks_to_create.keys():
3107
+ account_counter.increase(rse_id=rse_id, account=rule.account, files=len(locks_to_create[rse_id]), bytes_=sum([lock.bytes for lock in locks_to_create[rse_id]]), session=session)
3108
+
3109
+ # Add the transfers
3110
+ logger(logging.DEBUG, "Rule %s [%d/%d/%d] queued %d transfers", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt, len(transfers_to_create))
3111
+ request_core.queue_requests(requests=transfers_to_create, session=session)
3112
+ session.flush()
3113
+ logger(logging.DEBUG, "Finished creating locks and replicas for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
3114
+
3115
+
3116
+ @transactional_session
3117
+ def __delete_lock_and_update_replica(lock, purge_replicas=False, nowait=False, *, session: "Session", logger=logging.log):
3118
+ """
3119
+ Delete a lock and update the associated replica.
3120
+
3121
+ :param lock: SQLAlchemy lock object.
3122
+ :param purge_replicas: Purge setting of the rule.
3123
+ :param nowait: The nowait option of the FOR UPDATE statement.
3124
+ :param session: The database session in use.
3125
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
3126
+ :returns: True, if the lock was replicating and the associated transfer should be canceled; False otherwise.
3127
+ """
3128
+
3129
+ logger(logging.DEBUG, "Deleting lock %s:%s for rule %s", lock.scope, lock.name, str(lock.rule_id))
3130
+ lock.delete(session=session, flush=False)
3131
+ try:
3132
+ replica = session.query(models.RSEFileAssociation).filter(
3133
+ models.RSEFileAssociation.scope == lock.scope,
3134
+ models.RSEFileAssociation.name == lock.name,
3135
+ models.RSEFileAssociation.rse_id == lock.rse_id).with_for_update(nowait=nowait).one()
3136
+ replica.lock_cnt -= 1
3137
+ if replica.lock_cnt == 0:
3138
+ if purge_replicas:
3139
+ replica.tombstone = OBSOLETE
3140
+ elif replica.state == ReplicaState.UNAVAILABLE:
3141
+ replica.tombstone = OBSOLETE
3142
+ elif replica.accessed_at is not None:
3143
+ replica.tombstone = replica.accessed_at
3144
+ else:
3145
+ replica.tombstone = replica.created_at
3146
+ if lock.state == LockState.REPLICATING:
3147
+ replica.state = ReplicaState.UNAVAILABLE
3148
+ replica.tombstone = OBSOLETE
3149
+ return True
3150
+ except NoResultFound:
3151
+ logger(logging.ERROR, "Replica for lock %s:%s for rule %s on rse %s could not be found", lock.scope, lock.name, str(lock.rule_id), get_rse_name(rse_id=lock.rse_id, session=session))
3152
+ return False
3153
+
3154
+
3155
+ @transactional_session
3156
+ def __create_rule_approval_email(rule: "models.ReplicationRule", *, session: "Session"):
3157
+ """
3158
+ Create the rule notification email.
3159
+
3160
+ :param rule: The rule object.
3161
+ :param session: The database session in use.
3162
+ """
3163
+
3164
+ filepath = path.join(
3165
+ config_get('common', 'mailtemplatedir'), 'rule_approval_request.tmpl' # type: ignore
3166
+ )
3167
+ with open(filepath, 'r') as templatefile:
3168
+ template = Template(templatefile.read())
3169
+
3170
+ did = rucio.core.did.get_did(
3171
+ scope=rule.scope,
3172
+ name=rule.name,
3173
+ dynamic_depth=DIDType.FILE,
3174
+ session=session
3175
+ )
3176
+
3177
+ reps = rucio.core.replica.list_dataset_replicas(
3178
+ scope=rule.scope, name=rule.name, session=session
3179
+ )
3180
+ rses = [rep['rse_id'] for rep in reps if rep['state'] == ReplicaState.AVAILABLE]
3181
+
3182
+ # RSE occupancy
3183
+ vo = rule.account.vo
3184
+ target_rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
3185
+ if len(target_rses) > 1:
3186
+ target_rse = 'Multiple'
3187
+ free_space = 'undefined'
3188
+ free_space_after = 'undefined'
3189
+ else:
3190
+ target_rse = target_rses[0]['rse']
3191
+ target_rse_id = target_rses[0]['id']
3192
+ free_space = 'undefined'
3193
+ free_space_after = 'undefined'
3194
+
3195
+ try:
3196
+ for usage in get_rse_usage(rse_id=target_rse_id, session=session):
3197
+ if usage['source'] == 'storage':
3198
+ free_space = sizefmt(usage['free'])
3199
+ if did['bytes'] is None:
3200
+ free_space_after = 'undefined'
3201
+ else:
3202
+ free_space_after = sizefmt(usage['free'] - did['bytes'])
3203
+ except Exception:
3204
+ pass
3205
+
3206
+ # Resolve recipients:
3207
+ recipients = _create_recipients_list(rse_expression=rule.rse_expression, filter_={'vo': vo}, session=session)
3208
+
3209
+ for recipient in recipients:
3210
+ text = template.safe_substitute(
3211
+ {
3212
+ 'rule_id': str(rule.id),
3213
+ 'created_at': str(rule.created_at),
3214
+ 'expires_at': str(rule.expires_at),
3215
+ 'account': rule.account.external,
3216
+ 'email': get_account(account=rule.account, session=session).email,
3217
+ 'rse_expression': rule.rse_expression,
3218
+ 'comment': rule.comments,
3219
+ 'scope': rule.scope.external,
3220
+ 'name': rule.name,
3221
+ 'did_type': rule.did_type,
3222
+ 'length': '0' if did['length'] is None else str(did['length']),
3223
+ 'bytes': '0' if did['bytes'] is None else sizefmt(did['bytes']),
3224
+ 'open': did.get('open', 'Not Applicable'),
3225
+ 'complete_rses': ', '.join(rses),
3226
+ 'approvers': ','.join([r[0] for r in recipients]),
3227
+ 'approver': recipient[1],
3228
+ 'target_rse': target_rse,
3229
+ 'free_space': free_space,
3230
+ 'free_space_after': free_space_after
3231
+ }
3232
+ )
3233
+
3234
+ add_message(event_type='email',
3235
+ payload={'body': text,
3236
+ 'to': [recipient[0]],
3237
+ 'subject': '[RUCIO] Request to approve replication rule %s' % (str(rule.id))},
3238
+ session=session)
3239
+
3240
+
3241
+ @transactional_session
3242
+ def _create_recipients_list(rse_expression: str, filter_=None, *, session: "Session"):
3243
+ """
3244
+ Create a list of recipients for a notification email based on rse_expression.
3245
+
3246
+ :param rse_exoression: The rse_expression.
3247
+ :param session: The database session in use.
3248
+ """
3249
+
3250
+ recipients: list[tuple] = [] # (eMail, account)
3251
+
3252
+ # APPROVERS-LIST
3253
+ # If there are accounts in the approvers-list of any of the RSEs only these should be used
3254
+ for rse in parse_expression(rse_expression, filter_=filter_, session=session):
3255
+ rse_attr = list_rse_attributes(rse_id=rse['id'], session=session)
3256
+ if rse_attr.get('rule_approvers'):
3257
+ for account in rse_attr.get('rule_approvers').split(','):
3258
+ account = InternalAccount(account)
3259
+ try:
3260
+ email = get_account(account=account, session=session).email
3261
+ if email:
3262
+ recipients.append((email, account))
3263
+ except Exception:
3264
+ pass
3265
+
3266
+ # LOCALGROUPDISK/LOCALGROUPTAPE
3267
+ if not recipients:
3268
+ for rse in parse_expression(rse_expression, filter_=filter_, session=session):
3269
+ rse_attr = list_rse_attributes(rse_id=rse['id'], session=session)
3270
+ if rse_attr.get('type', '') in ('LOCALGROUPDISK', 'LOCALGROUPTAPE'):
3271
+
3272
+ query = select(
3273
+ models.AccountAttrAssociation.account
3274
+ ).where(
3275
+ models.AccountAttrAssociation.key == f'country-{rse_attr.get("country", "")}',
3276
+ models.AccountAttrAssociation.value == 'admin'
3277
+ )
3278
+
3279
+ for account in session.execute(query).scalars().all():
3280
+ try:
3281
+ email = get_account(account=account, session=session).email
3282
+ if email:
3283
+ recipients.append((email, account))
3284
+ except Exception:
3285
+ pass
3286
+
3287
+ # GROUPDISK
3288
+ if not recipients:
3289
+ for rse in parse_expression(rse_expression, filter_=filter_, session=session):
3290
+ rse_attr = list_rse_attributes(rse_id=rse['id'], session=session)
3291
+ if rse_attr.get('type', '') == 'GROUPDISK':
3292
+
3293
+ query = select(
3294
+ models.AccountAttrAssociation.account
3295
+ ).where(
3296
+ models.AccountAttrAssociation.key == f'group-{rse_attr.get("physgroup", "")}',
3297
+ models.AccountAttrAssociation.value == 'admin'
3298
+ )
3299
+
3300
+ for account in session.execute(query).scalars().all():
3301
+ try:
3302
+ email = get_account(account=account, session=session).email
3303
+ if email:
3304
+ recipients.append((email, account))
3305
+ except Exception:
3306
+ pass
3307
+
3308
+ # DDMADMIN as default
3309
+ if not recipients:
3310
+ default_mail_from = config_get(
3311
+ 'core', 'default_mail_from', raise_exception=False, default=None
3312
+ )
3313
+ if default_mail_from:
3314
+ recipients = [(default_mail_from, 'ddmadmin')]
3315
+
3316
+ return list(set(recipients))
3317
+
3318
+
3319
+ def __progress_class(replicating_locks, total_locks):
3320
+ """
3321
+ Returns the progress class (10%, 20%, ...) of currently replicating locks.
3322
+
3323
+ :param replicating_locks: Currently replicating locks.
3324
+ :param total_locks: Total locks.
3325
+ """
3326
+
3327
+ try:
3328
+ return int(float(total_locks - replicating_locks) / float(total_locks) * 10) * 10
3329
+ except Exception:
3330
+ return 0
3331
+
3332
+
3333
+ @policy_filter
3334
+ @transactional_session
3335
+ def archive_localgroupdisk_datasets(scope, name, *, session: "Session", logger=logging.log):
3336
+ """
3337
+ ATLAS policy to archive a dataset which has a replica on LOCALGROUPDISK
3338
+
3339
+ :param scope: Scope of the dataset.
3340
+ :param name: Name of the dataset.
3341
+ :param session: The database session in use.
3342
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
3343
+ """
3344
+
3345
+ rses_to_rebalance = []
3346
+
3347
+ archive = InternalScope('archive', vo=scope.vo)
3348
+ # Check if the archival dataset already exists
3349
+ try:
3350
+ rucio.core.did.get_did(scope=archive, name=name, session=session)
3351
+ return
3352
+ except DataIdentifierNotFound:
3353
+ pass
3354
+
3355
+ # Check if the dataset has a rule on a LOCALGROUPDISK
3356
+ for lock in rucio.core.lock.get_dataset_locks(scope=scope, name=name, session=session):
3357
+ if 'LOCALGROUPDISK' in lock['rse']:
3358
+ rses_to_rebalance.append({'rse_id': lock['rse_id'], 'rse': lock['rse'], 'account': lock['account']})
3359
+ # Remove duplicates from list
3360
+ rses_to_rebalance = [dict(t) for t in set([tuple(sorted(d.items())) for d in rses_to_rebalance])]
3361
+
3362
+ # There is at least one rule on LOCALGROUPDISK
3363
+ if rses_to_rebalance:
3364
+ content = [x for x in rucio.core.did.list_content(scope=scope, name=name, session=session)]
3365
+ if content:
3366
+ # Create the archival dataset
3367
+ did = rucio.core.did.get_did(scope=scope, name=name, session=session)
3368
+ meta = rucio.core.did.get_metadata(scope=scope, name=name, session=session)
3369
+ new_meta = {k: v for k, v in meta.items() if k in ['project', 'datatype', 'run_number', 'stream_name', 'prod_step', 'version', 'campaign', 'task_id', 'panda_id'] and v is not None}
3370
+ rucio.core.did.add_did(scope=archive,
3371
+ name=name,
3372
+ did_type=DIDType.DATASET,
3373
+ account=did['account'],
3374
+ statuses={},
3375
+ meta=new_meta,
3376
+ rules=[],
3377
+ lifetime=None,
3378
+ dids=[],
3379
+ rse_id=None,
3380
+ session=session)
3381
+ rucio.core.did.attach_dids(scope=archive, name=name, dids=content, account=did['account'], session=session)
3382
+ if not did['open']:
3383
+ rucio.core.did.set_status(scope=archive, name=name, open=False, session=session)
3384
+
3385
+ for rse in rses_to_rebalance:
3386
+ add_rule(dids=[{'scope': archive, 'name': name}],
3387
+ account=rse['account'],
3388
+ copies=1,
3389
+ rse_expression=rse['rse'],
3390
+ grouping='DATASET',
3391
+ weight=None,
3392
+ lifetime=None,
3393
+ locked=False,
3394
+ subscription_id=None,
3395
+ ignore_account_limit=True,
3396
+ ignore_availability=True,
3397
+ session=session)
3398
+ logger(logging.DEBUG, 'Re-Scoped %s:%s', scope, name)
3399
+
3400
+
3401
+ @policy_filter
3402
+ @read_session
3403
+ def get_scratch_policy(account, rses, lifetime, *, session: "Session"):
3404
+ """
3405
+ ATLAS policy for rules on SCRATCHDISK
3406
+
3407
+ :param account: Account of the rule.
3408
+ :param rses: List of RSEs.
3409
+ :param lifetime: Lifetime.
3410
+ :param session: The database session in use.
3411
+ """
3412
+
3413
+ scratchdisk_lifetime = get_scratchdisk_lifetime()
3414
+ # Check SCRATCHDISK Policy
3415
+ if not has_account_attribute(account=account, key='admin', session=session) and (lifetime is None or lifetime > 60 * 60 * 24 * scratchdisk_lifetime):
3416
+ # Check if one of the rses is a SCRATCHDISK:
3417
+ if [rse for rse in rses if list_rse_attributes(rse_id=rse['id'], session=session).get('type') == 'SCRATCHDISK']:
3418
+ lifetime = 60 * 60 * 24 * scratchdisk_lifetime - 1
3419
+ return lifetime