rucio 35.7.0__py3-none-any.whl → 37.0.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (266) hide show
  1. rucio/alembicrevision.py +1 -1
  2. rucio/{daemons/c3po/collectors → cli}/__init__.py +1 -0
  3. rucio/cli/account.py +216 -0
  4. rucio-35.7.0.data/scripts/rucio → rucio/cli/bin_legacy/rucio.py +769 -486
  5. rucio-35.7.0.data/scripts/rucio-admin → rucio/cli/bin_legacy/rucio_admin.py +476 -423
  6. rucio/cli/command.py +272 -0
  7. rucio/cli/config.py +72 -0
  8. rucio/cli/did.py +191 -0
  9. rucio/cli/download.py +128 -0
  10. rucio/cli/lifetime_exception.py +33 -0
  11. rucio/cli/replica.py +162 -0
  12. rucio/cli/rse.py +293 -0
  13. rucio/cli/rule.py +158 -0
  14. rucio/cli/scope.py +40 -0
  15. rucio/cli/subscription.py +73 -0
  16. rucio/cli/upload.py +60 -0
  17. rucio/cli/utils.py +226 -0
  18. rucio/client/accountclient.py +0 -1
  19. rucio/client/baseclient.py +33 -24
  20. rucio/client/client.py +45 -1
  21. rucio/client/didclient.py +5 -3
  22. rucio/client/downloadclient.py +6 -8
  23. rucio/client/replicaclient.py +0 -2
  24. rucio/client/richclient.py +317 -0
  25. rucio/client/rseclient.py +4 -4
  26. rucio/client/uploadclient.py +26 -12
  27. rucio/common/bittorrent.py +234 -0
  28. rucio/common/cache.py +66 -29
  29. rucio/common/checksum.py +168 -0
  30. rucio/common/client.py +122 -0
  31. rucio/common/config.py +22 -35
  32. rucio/common/constants.py +61 -3
  33. rucio/common/didtype.py +72 -24
  34. rucio/common/dumper/__init__.py +45 -38
  35. rucio/common/dumper/consistency.py +75 -30
  36. rucio/common/dumper/data_models.py +63 -19
  37. rucio/common/dumper/path_parsing.py +19 -8
  38. rucio/common/exception.py +65 -8
  39. rucio/common/extra.py +5 -10
  40. rucio/common/logging.py +13 -13
  41. rucio/common/pcache.py +8 -7
  42. rucio/common/plugins.py +59 -27
  43. rucio/common/policy.py +12 -3
  44. rucio/common/schema/__init__.py +84 -34
  45. rucio/common/schema/generic.py +0 -17
  46. rucio/common/schema/generic_multi_vo.py +0 -17
  47. rucio/common/test_rucio_server.py +12 -6
  48. rucio/common/types.py +132 -52
  49. rucio/common/utils.py +93 -643
  50. rucio/core/account_limit.py +14 -12
  51. rucio/core/authentication.py +2 -2
  52. rucio/core/config.py +23 -42
  53. rucio/core/credential.py +14 -15
  54. rucio/core/did.py +5 -1
  55. rucio/core/did_meta_plugins/elasticsearch_meta.py +407 -0
  56. rucio/core/did_meta_plugins/filter_engine.py +62 -3
  57. rucio/core/did_meta_plugins/json_meta.py +2 -2
  58. rucio/core/did_meta_plugins/mongo_meta.py +43 -30
  59. rucio/core/did_meta_plugins/postgres_meta.py +75 -39
  60. rucio/core/identity.py +6 -5
  61. rucio/core/importer.py +4 -3
  62. rucio/core/lifetime_exception.py +2 -2
  63. rucio/core/lock.py +8 -7
  64. rucio/core/message.py +6 -0
  65. rucio/core/monitor.py +30 -29
  66. rucio/core/naming_convention.py +2 -2
  67. rucio/core/nongrid_trace.py +2 -2
  68. rucio/core/oidc.py +11 -9
  69. rucio/core/permission/__init__.py +79 -37
  70. rucio/core/permission/generic.py +1 -7
  71. rucio/core/permission/generic_multi_vo.py +1 -7
  72. rucio/core/quarantined_replica.py +4 -3
  73. rucio/core/replica.py +464 -139
  74. rucio/core/replica_sorter.py +55 -59
  75. rucio/core/request.py +34 -32
  76. rucio/core/rse.py +301 -97
  77. rucio/core/rse_counter.py +1 -2
  78. rucio/core/rse_expression_parser.py +7 -7
  79. rucio/core/rse_selector.py +9 -7
  80. rucio/core/rule.py +41 -40
  81. rucio/core/rule_grouping.py +42 -40
  82. rucio/core/scope.py +5 -4
  83. rucio/core/subscription.py +26 -28
  84. rucio/core/topology.py +11 -11
  85. rucio/core/trace.py +2 -2
  86. rucio/core/transfer.py +29 -15
  87. rucio/core/volatile_replica.py +4 -3
  88. rucio/daemons/atropos/atropos.py +1 -1
  89. rucio/daemons/auditor/__init__.py +2 -2
  90. rucio/daemons/auditor/srmdumps.py +6 -6
  91. rucio/daemons/automatix/automatix.py +32 -21
  92. rucio/daemons/badreplicas/necromancer.py +2 -2
  93. rucio/daemons/bb8/nuclei_background_rebalance.py +1 -1
  94. rucio/daemons/bb8/t2_background_rebalance.py +1 -1
  95. rucio/daemons/common.py +15 -25
  96. rucio/daemons/conveyor/finisher.py +2 -2
  97. rucio/daemons/conveyor/poller.py +18 -28
  98. rucio/daemons/conveyor/receiver.py +2 -2
  99. rucio/daemons/conveyor/stager.py +1 -0
  100. rucio/daemons/conveyor/submitter.py +3 -3
  101. rucio/daemons/hermes/hermes.py +91 -30
  102. rucio/daemons/judge/evaluator.py +2 -2
  103. rucio/daemons/oauthmanager/oauthmanager.py +3 -3
  104. rucio/daemons/reaper/dark_reaper.py +7 -3
  105. rucio/daemons/reaper/reaper.py +12 -16
  106. rucio/daemons/rsedecommissioner/config.py +1 -1
  107. rucio/daemons/rsedecommissioner/profiles/generic.py +5 -4
  108. rucio/daemons/rsedecommissioner/profiles/types.py +7 -6
  109. rucio/daemons/rsedecommissioner/rse_decommissioner.py +1 -1
  110. rucio/daemons/storage/consistency/actions.py +8 -6
  111. rucio/daemons/tracer/kronos.py +4 -4
  112. rucio/db/sqla/constants.py +5 -0
  113. rucio/db/sqla/migrate_repo/versions/1677d4d803c8_split_rse_availability_into_multiple.py +4 -4
  114. rucio/db/sqla/migrate_repo/versions/30d5206e9cad_increase_oauthrequest_redirect_msg_.py +37 -0
  115. rucio/db/sqla/models.py +157 -154
  116. rucio/db/sqla/session.py +58 -27
  117. rucio/db/sqla/types.py +2 -2
  118. rucio/db/sqla/util.py +2 -2
  119. rucio/gateway/account.py +18 -12
  120. rucio/gateway/account_limit.py +137 -60
  121. rucio/gateway/authentication.py +18 -12
  122. rucio/gateway/config.py +30 -20
  123. rucio/gateway/credential.py +9 -10
  124. rucio/gateway/did.py +70 -53
  125. rucio/gateway/dirac.py +6 -4
  126. rucio/gateway/exporter.py +3 -2
  127. rucio/gateway/heartbeat.py +6 -4
  128. rucio/gateway/identity.py +36 -51
  129. rucio/gateway/importer.py +3 -2
  130. rucio/gateway/lifetime_exception.py +3 -2
  131. rucio/gateway/meta_conventions.py +17 -6
  132. rucio/gateway/permission.py +4 -1
  133. rucio/gateway/quarantined_replica.py +3 -2
  134. rucio/gateway/replica.py +31 -22
  135. rucio/gateway/request.py +27 -18
  136. rucio/gateway/rse.py +69 -37
  137. rucio/gateway/rule.py +46 -26
  138. rucio/gateway/scope.py +3 -2
  139. rucio/gateway/subscription.py +14 -11
  140. rucio/gateway/vo.py +12 -8
  141. rucio/rse/__init__.py +3 -3
  142. rucio/rse/protocols/bittorrent.py +11 -1
  143. rucio/rse/protocols/cache.py +0 -11
  144. rucio/rse/protocols/dummy.py +0 -11
  145. rucio/rse/protocols/gfal.py +14 -9
  146. rucio/rse/protocols/globus.py +1 -1
  147. rucio/rse/protocols/http_cache.py +1 -1
  148. rucio/rse/protocols/posix.py +2 -2
  149. rucio/rse/protocols/protocol.py +84 -317
  150. rucio/rse/protocols/rclone.py +2 -1
  151. rucio/rse/protocols/rfio.py +10 -1
  152. rucio/rse/protocols/ssh.py +2 -1
  153. rucio/rse/protocols/storm.py +2 -13
  154. rucio/rse/protocols/webdav.py +74 -30
  155. rucio/rse/protocols/xrootd.py +2 -1
  156. rucio/rse/rsemanager.py +170 -53
  157. rucio/rse/translation.py +260 -0
  158. rucio/tests/common.py +23 -13
  159. rucio/tests/common_server.py +26 -9
  160. rucio/transfertool/bittorrent.py +15 -14
  161. rucio/transfertool/bittorrent_driver.py +5 -7
  162. rucio/transfertool/bittorrent_driver_qbittorrent.py +9 -8
  163. rucio/transfertool/fts3.py +20 -16
  164. rucio/transfertool/mock.py +2 -3
  165. rucio/vcsversion.py +4 -4
  166. rucio/version.py +7 -0
  167. rucio/web/rest/flaskapi/v1/accounts.py +17 -3
  168. rucio/web/rest/flaskapi/v1/auth.py +5 -5
  169. rucio/web/rest/flaskapi/v1/credentials.py +3 -2
  170. rucio/web/rest/flaskapi/v1/dids.py +21 -15
  171. rucio/web/rest/flaskapi/v1/identities.py +33 -9
  172. rucio/web/rest/flaskapi/v1/redirect.py +5 -4
  173. rucio/web/rest/flaskapi/v1/replicas.py +12 -8
  174. rucio/web/rest/flaskapi/v1/rses.py +15 -4
  175. rucio/web/rest/flaskapi/v1/traces.py +56 -19
  176. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/etc/alembic.ini.template +1 -1
  177. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/etc/alembic_offline.ini.template +1 -1
  178. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/etc/rucio.cfg.atlas.client.template +3 -2
  179. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/etc/rucio.cfg.template +3 -19
  180. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/etc/rucio_multi_vo.cfg.template +1 -18
  181. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/requirements.server.txt +97 -68
  182. rucio-37.0.0.data/scripts/rucio +133 -0
  183. rucio-37.0.0.data/scripts/rucio-admin +97 -0
  184. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-atropos +2 -2
  185. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-auditor +2 -1
  186. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-automatix +2 -2
  187. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-cache-client +17 -10
  188. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-conveyor-receiver +1 -0
  189. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-kronos +1 -0
  190. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-minos +2 -2
  191. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-minos-temporary-expiration +2 -2
  192. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-necromancer +2 -2
  193. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-reaper +6 -6
  194. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-transmogrifier +2 -2
  195. rucio-37.0.0.dist-info/METADATA +92 -0
  196. {rucio-35.7.0.dist-info → rucio-37.0.0.dist-info}/RECORD +237 -243
  197. {rucio-35.7.0.dist-info → rucio-37.0.0.dist-info}/licenses/AUTHORS.rst +3 -0
  198. rucio/common/schema/atlas.py +0 -413
  199. rucio/common/schema/belleii.py +0 -408
  200. rucio/common/schema/domatpc.py +0 -401
  201. rucio/common/schema/escape.py +0 -426
  202. rucio/common/schema/icecube.py +0 -406
  203. rucio/core/permission/atlas.py +0 -1348
  204. rucio/core/permission/belleii.py +0 -1077
  205. rucio/core/permission/escape.py +0 -1078
  206. rucio/daemons/c3po/algorithms/__init__.py +0 -13
  207. rucio/daemons/c3po/algorithms/simple.py +0 -134
  208. rucio/daemons/c3po/algorithms/t2_free_space.py +0 -128
  209. rucio/daemons/c3po/algorithms/t2_free_space_only_pop.py +0 -130
  210. rucio/daemons/c3po/algorithms/t2_free_space_only_pop_with_network.py +0 -294
  211. rucio/daemons/c3po/c3po.py +0 -371
  212. rucio/daemons/c3po/collectors/agis.py +0 -108
  213. rucio/daemons/c3po/collectors/free_space.py +0 -81
  214. rucio/daemons/c3po/collectors/jedi_did.py +0 -57
  215. rucio/daemons/c3po/collectors/mock_did.py +0 -51
  216. rucio/daemons/c3po/collectors/network_metrics.py +0 -71
  217. rucio/daemons/c3po/collectors/workload.py +0 -112
  218. rucio/daemons/c3po/utils/__init__.py +0 -13
  219. rucio/daemons/c3po/utils/dataset_cache.py +0 -50
  220. rucio/daemons/c3po/utils/expiring_dataset_cache.py +0 -56
  221. rucio/daemons/c3po/utils/expiring_list.py +0 -62
  222. rucio/daemons/c3po/utils/popularity.py +0 -85
  223. rucio/daemons/c3po/utils/timeseries.py +0 -89
  224. rucio/rse/protocols/gsiftp.py +0 -92
  225. rucio-35.7.0.data/scripts/rucio-c3po +0 -85
  226. rucio-35.7.0.dist-info/METADATA +0 -72
  227. /rucio/{daemons/c3po → cli/bin_legacy}/__init__.py +0 -0
  228. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/etc/globus-config.yml.template +0 -0
  229. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/etc/ldap.cfg.template +0 -0
  230. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/etc/mail_templates/rule_approval_request.tmpl +0 -0
  231. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +0 -0
  232. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/etc/mail_templates/rule_approved_user.tmpl +0 -0
  233. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +0 -0
  234. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/etc/mail_templates/rule_denied_user.tmpl +0 -0
  235. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +0 -0
  236. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/etc/rse-accounts.cfg.template +0 -0
  237. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/tools/bootstrap.py +0 -0
  238. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/tools/merge_rucio_configs.py +0 -0
  239. {rucio-35.7.0.data → rucio-37.0.0.data}/data/rucio/tools/reset_database.py +0 -0
  240. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-abacus-account +0 -0
  241. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-abacus-collection-replica +0 -0
  242. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-abacus-rse +0 -0
  243. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-bb8 +0 -0
  244. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-cache-consumer +0 -0
  245. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-conveyor-finisher +0 -0
  246. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-conveyor-poller +0 -0
  247. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-conveyor-preparer +0 -0
  248. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-conveyor-stager +0 -0
  249. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-conveyor-submitter +0 -0
  250. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-conveyor-throttler +0 -0
  251. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-dark-reaper +0 -0
  252. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-dumper +0 -0
  253. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-follower +0 -0
  254. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-hermes +0 -0
  255. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-judge-cleaner +0 -0
  256. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-judge-evaluator +0 -0
  257. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-judge-injector +0 -0
  258. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-judge-repairer +0 -0
  259. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-oauth-manager +0 -0
  260. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-replica-recoverer +0 -0
  261. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-rse-decommissioner +0 -0
  262. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-storage-consistency-actions +0 -0
  263. {rucio-35.7.0.data → rucio-37.0.0.data}/scripts/rucio-undertaker +0 -0
  264. {rucio-35.7.0.dist-info → rucio-37.0.0.dist-info}/WHEEL +0 -0
  265. {rucio-35.7.0.dist-info → rucio-37.0.0.dist-info}/licenses/LICENSE +0 -0
  266. {rucio-35.7.0.dist-info → rucio-37.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,407 @@
1
+ # Copyright European Organization for Nuclear Research (CERN) since 2012
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ '''
17
+ Elasticsearch based metadata plugin.
18
+ '''
19
+
20
+ import datetime
21
+ import operator
22
+ from typing import TYPE_CHECKING, Any, Literal, Optional, Union
23
+
24
+ from elasticsearch import Elasticsearch
25
+ from elasticsearch import exceptions as elastic_exceptions
26
+
27
+ from rucio.common import config, exception
28
+ from rucio.core.did_meta_plugins.did_meta_plugin_interface import DidMetaPlugin
29
+ from rucio.core.did_meta_plugins.filter_engine import FilterEngine
30
+
31
+ if TYPE_CHECKING:
32
+ from collections.abc import Iterator
33
+
34
+ from sqlalchemy.orm import Session
35
+
36
+ from rucio.common.types import InternalScope
37
+
38
+ IMMUTABLE_KEYS = [
39
+ 'scope', # generated on insert
40
+ 'name', # generated on insert
41
+ 'vo' # generated on insert
42
+ ]
43
+
44
+
45
+ class ElasticDidMeta(DidMetaPlugin):
46
+ def __init__(
47
+ self,
48
+ hosts: Optional[list[str]] = None,
49
+ user: Optional[str] = None,
50
+ password: Optional[str] = None,
51
+ index: Optional[str] = None,
52
+ archive_index: Optional[str] = None,
53
+ use_ssl: Optional[bool] = False,
54
+ verify_certs: bool = True,
55
+ ca_certs: Optional[str] = None,
56
+ client_cert: Optional[str] = None,
57
+ client_key: Optional[str] = None,
58
+ request_timeout: int = 100,
59
+ max_retries: int = 3,
60
+ retry_on_timeout: bool = False
61
+ ) -> None:
62
+ super(ElasticDidMeta, self).__init__()
63
+ hosts = hosts or [config.config_get('metadata', 'elastic_service_hosts')]
64
+ user = user or config.config_get('metadata', 'elastic_user', False, None)
65
+ password = password or config.config_get('metadata', 'elastic_password', False, None)
66
+ self.index = index or config.config_get('metadata', 'meta_index', False, 'rucio_did_meta')
67
+ self.archive_index = archive_index or config.config_get('metadata', 'archive_index', False, 'archive_meta')
68
+ use_ssl = use_ssl or config.config_get_bool('metadata', 'use_ssl', False, False)
69
+ ca_certs = ca_certs or config.config_get('metadata', 'ca_certs', False, None)
70
+ client_cert = client_cert or config.config_get('metadata', 'client_cert', False, None)
71
+ client_key = client_key or config.config_get('metadata', 'client_key', False, None)
72
+
73
+ self.es_config = {
74
+ 'hosts': hosts,
75
+ 'timeout': request_timeout,
76
+ 'max_retries': max_retries,
77
+ 'retry_on_timeout': retry_on_timeout
78
+ }
79
+ if user and password:
80
+ self.es_config['basic_auth'] = (user, password)
81
+
82
+ if use_ssl:
83
+ self.es_config.update({
84
+ 'ca_certs': ca_certs,
85
+ 'verify_certs': verify_certs,
86
+ })
87
+ if client_cert and client_key:
88
+ self.es_config.update({
89
+ 'client_cert': client_cert,
90
+ 'client_key': client_key
91
+ })
92
+
93
+ self.client = Elasticsearch(**self.es_config)
94
+ self.plugin_name = "ELASTIC"
95
+
96
+ def drop_index(self) -> None:
97
+ self.client.indices.delete(index=self.index)
98
+
99
+ def get_metadata(
100
+ self,
101
+ scope: "InternalScope",
102
+ name: str,
103
+ *,
104
+ session: "Optional[Session]" = None
105
+ ) -> dict[str, Any]:
106
+ """
107
+ Get data identifier metadata.
108
+
109
+ :param scope: The scope name
110
+ :param name: The data identifier name
111
+ :param session: The database session in use
112
+ :returns: The metadata for the did
113
+ :raises DataIdentifierNotFound: If the DID metadata is not found.
114
+ :raises RucioException: If another error occurs during the process.
115
+ """
116
+
117
+ doc_id = f"{scope.internal}{name}"
118
+ try:
119
+ doc = self.client.get(index=self.index, id=doc_id)["_source"]
120
+ except elastic_exceptions.NotFoundError as err:
121
+ raise exception.DataIdentifierNotFound(f"No metadata found for DID '{scope}:{name}' not found") from err
122
+ except Exception as err:
123
+ raise exception.RucioException(err)
124
+ return doc
125
+
126
+ def set_metadata(
127
+ self,
128
+ scope: "InternalScope",
129
+ name: str,
130
+ key: str,
131
+ value: str,
132
+ recursive: bool = False,
133
+ *,
134
+ session: "Optional[Session]" = None
135
+ ) -> None:
136
+ """
137
+ Set single metadata key.
138
+
139
+ :param scope: the scope of did
140
+ :param name: the name of the did
141
+ :param key: the key to be added
142
+ :param value: the value of the key to be added
143
+ :param recursive: recurse into DIDs (not supported)
144
+ :param session: The database session in use
145
+ :raises DataIdentifierNotFound: If the DID is not found.
146
+ :raises RucioException: If an error occurs while setting the metadata.
147
+ """
148
+ self.set_metadata_bulk(scope=scope, name=name, meta={key: value}, recursive=recursive, session=session)
149
+
150
+ def set_metadata_bulk(
151
+ self,
152
+ scope: "InternalScope",
153
+ name: str,
154
+ meta: dict[str, Any],
155
+ recursive: bool = False,
156
+ *,
157
+ session: "Optional[Session]" = None
158
+ ) -> None:
159
+ """
160
+ Bulk set metadata keys.
161
+
162
+ :param scope: the scope of did
163
+ :param name: the name of the did
164
+ :param meta: dictionary of metadata keypairs to be added
165
+ :param recursive: recurse into DIDs (not supported)
166
+ :param session: The database session in use
167
+ :raises DataIdentifierNotFound: If the DID is not found.
168
+ :raises UnsupportedOperation: If recursive inserts are requested (currently unsupported).
169
+ :raises RucioException: If an error occurs while setting the metadata.
170
+ """
171
+ doc_id = f"{scope.internal}{name}"
172
+ try:
173
+ # Try to get existing metadata
174
+ existing_meta = self.get_metadata(scope, name)
175
+ except exception.DataIdentifierNotFound:
176
+ existing_meta = {
177
+ 'scope': str(scope.external),
178
+ 'name': name,
179
+ 'vo': scope.vo
180
+ }
181
+ for key, value in meta.items():
182
+ if key not in IMMUTABLE_KEYS:
183
+ existing_meta[key] = value
184
+
185
+ try:
186
+ self.client.index(index=self.index, body=existing_meta, id=doc_id, refresh="true")
187
+ except Exception as err:
188
+ raise exception.RucioException(err)
189
+
190
+ if recursive:
191
+ raise exception.UnsupportedOperation(f"'{self.plugin_name.lower()}' metadata module does not currently support recursive inserts of metadata")
192
+
193
+ def delete_metadata(
194
+ self,
195
+ scope: "InternalScope",
196
+ name: str,
197
+ key: str,
198
+ *,
199
+ session: "Optional[Session]" = None
200
+ ) -> None:
201
+ """
202
+ Delete a key from metadata.
203
+
204
+ :param scope: the scope of did
205
+ :param name: the name of the did
206
+ :param key: the key to be deleted
207
+ :raises DataIdentifierNotFound: If the DID is not found.
208
+ :raises RucioException: If an error occurs while setting the metadata.
209
+ """
210
+ doc_id = f"{scope.internal}{name}"
211
+ try:
212
+ # First, get the current document
213
+ doc = self.client.get(index=self.index, id=doc_id)
214
+
215
+ # Check if the key exists in the document
216
+ if key in doc['_source']:
217
+ # Use script to remove the field
218
+ script = {
219
+ "script": {
220
+ "source": f"ctx._source.remove('{key}')",
221
+ "lang": "painless"
222
+ }
223
+ }
224
+ self.client.update(index=self.index, id=doc_id, body=script)
225
+ except elastic_exceptions.NotFoundError as err:
226
+ raise exception.DataIdentifierNotFound(f"No metadata found for DID '{scope}:{name}' not found") from err
227
+ except Exception as err:
228
+ raise exception.RucioException(err)
229
+
230
+ def list_dids(
231
+ self,
232
+ scope: "InternalScope",
233
+ filters: Union[list[dict[str, Any]], dict[str, Any]],
234
+ did_type: Literal['all', 'collection', 'dataset', 'container', 'file'] = 'collection',
235
+ ignore_case: bool = False,
236
+ limit: Optional[int] = None,
237
+ offset: Optional[int] = None,
238
+ long: bool = False,
239
+ recursive: bool = False,
240
+ ignore_dids: Optional[list] = None,
241
+ *,
242
+ session: "Optional[Session]" = None
243
+ ) -> "Iterator[dict[str, Any]]":
244
+ """
245
+ List DIDs (Data Identifier).
246
+
247
+ :param scope: The scope of the DIDs to search.
248
+ :param filters: The filters to apply to the DID search.
249
+ :param did_type: The type of DID (default is 'collection').
250
+ :param ignore_case: Whether to ignore case (default is False).
251
+ :param limit: The maximum number of DIDs to return.
252
+ :param offset: The starting point for the search (used for pagination).
253
+ :param long: Whether to return extended information (scope, name, did_type, bytes, length) (default is False).
254
+ :param recursive: Whether to search recursively (currently unsupported).
255
+ :param ignore_dids: A list of DIDs to ignore (default is an empty list).
256
+ :param session: The database session in use.
257
+ :returns: A generator yielding DIDs as strings (when `long` is False) or dictionaries (when `long` is True).
258
+ :raises UnsupportedOperation: If recursive searches are requested (currently unsupported).
259
+ :raises RucioException: If an error occurs during the search.
260
+ """
261
+
262
+ if not ignore_dids:
263
+ ignore_dids = []
264
+
265
+ # backwards compatibility for filters as single {}.
266
+ if isinstance(filters, dict):
267
+ filters = [filters]
268
+
269
+ # Create Elasticsearch query
270
+ fe = FilterEngine(filters, model_class=None, strict_coerce=False)
271
+ elastic_query_str = fe.create_elastic_query(
272
+ additional_filters=[
273
+ ('scope', operator.eq, str(scope.external)),
274
+ ('vo', operator.eq, scope.vo)
275
+ ]
276
+ )
277
+ pit = self.client.open_point_in_time(index=self.index, keep_alive="2m")
278
+ pit_id = pit["id"]
279
+ # Base query with point in time (pit) paramter.
280
+ # sort is needed for search_after, so we use scope sort (random choice)
281
+ query = {
282
+ "query": elastic_query_str,
283
+ "sort": [{"scope.keyword": "asc"}],
284
+ "_source": ["scope", "name"] if not long else ["scope", "name", "did_type", "bytes", "length"],
285
+ "pit": {"id": pit_id, "keep_alive": "2m"}
286
+ }
287
+
288
+ # Add sorting and pagination
289
+ if offset:
290
+ query["from"] = offset
291
+ size = limit if limit else 10000
292
+ query["size"] = size
293
+ search_after = None
294
+ total_processed = 0
295
+ try:
296
+ while True:
297
+ if search_after:
298
+ query["search_after"] = search_after
299
+ query.pop("from", None)
300
+ # Execute search
301
+ results = self.client.search(body=query)
302
+ hits = results['hits']['hits']
303
+ if not hits:
304
+ break
305
+
306
+ for hit in hits:
307
+ did_full = f"{hit['_source']['scope']}:{hit['_source']['name']}"
308
+ if did_full not in ignore_dids:
309
+ ignore_dids.append(did_full)
310
+ if long:
311
+ yield {
312
+ 'scope': (hit['_source']['scope']),
313
+ 'name': hit['_source']['name'],
314
+ 'did_type': hit['_source'].get('did_type', 'N/A'),
315
+ 'bytes': hit['_source'].get('bytes', 'N/A'),
316
+ 'length': hit['_source'].get('length', 'N/A')
317
+ }
318
+ else:
319
+ yield hit['_source']['name']
320
+
321
+ total_processed += 1
322
+ if limit and total_processed >= limit:
323
+ break
324
+
325
+ # Update search_after for the next iteration
326
+ search_after = hits[-1]["sort"]
327
+
328
+ finally:
329
+ # Always delete the point in time when done
330
+ self.client.close_point_in_time(body={"id": pit_id})
331
+
332
+ if recursive:
333
+ raise exception.UnsupportedOperation(f"'{self.plugin_name.lower()}' metadata module does not currently support recursive searches")
334
+
335
+ def on_delete(
336
+ self,
337
+ scope: "InternalScope",
338
+ name: str,
339
+ archive: bool = False,
340
+ session: "Optional[Session]" = None
341
+ ) -> None:
342
+ """
343
+ Delete a document and optionally archive it.
344
+
345
+ :param scope: The scope of the document
346
+ :param name: The name of the document
347
+ :param archive: Whether to archive the document before deletion
348
+ :raises DataIdentifierNotFound: If the DID is not found.
349
+ :raises RucioException: If an error occurs while setting the metadata.
350
+ """
351
+ doc_id = f"{scope}{name}"
352
+
353
+ try:
354
+ doc = self.client.get(index=self.index, id=doc_id)
355
+
356
+ if archive:
357
+ archived_doc = doc['_source']
358
+ archived_doc['deleted_at'] = datetime.datetime.now(datetime.timezone.utc).isoformat()
359
+ self.client.index(index=self.archive_index, id=doc_id, body=archived_doc)
360
+
361
+ self.client.delete(index=self.index, id=doc_id)
362
+
363
+ except elastic_exceptions.NotFoundError as err:
364
+ raise exception.DataIdentifierNotFound(f"No metadata found for DID '{scope}:{name}' not found") from err
365
+ except Exception as err:
366
+ raise exception.RucioException(err)
367
+
368
+ def get_metadata_archived(
369
+ self,
370
+ scope: "InternalScope",
371
+ name: str,
372
+ session: "Optional[Session]" = None
373
+ ) -> None:
374
+ """
375
+ Retrieve archived metadata for a given scope and name.
376
+
377
+ :param scope: The scope of the document
378
+ :param name: The name of the document
379
+ :return: The archived metadata or None if not found
380
+ :raises DataIdentifierNotFound: If the DID is not found.
381
+ :raises RucioException: If an error occurs while setting the metadata.
382
+ """
383
+ doc_id = f"{scope}{name}"
384
+
385
+ try:
386
+ doc = self.client.get(index=self.archive_index, id=doc_id)["_source"]
387
+ return doc
388
+ except elastic_exceptions.NotFoundError as err:
389
+ raise exception.DataIdentifierNotFound(f"No metadata found for DID '{scope}:{name}' not found") from err
390
+ except Exception as err:
391
+ raise exception.RucioException(err)
392
+
393
+ def manages_key(
394
+ self,
395
+ key: str,
396
+ *,
397
+ session: "Optional[Session]" = None
398
+ ) -> bool:
399
+ return True
400
+
401
+ def get_plugin_name(self) -> str:
402
+ """
403
+ Returns a unique identifier for this plugin. This can be later used for filtering down results to this plugin only.
404
+
405
+ :returns: The name of the plugin
406
+ """
407
+ return self.plugin_name
@@ -49,6 +49,15 @@ OPERATORS_CONVERSION_LUT = {
49
49
  "": operator.eq
50
50
  }
51
51
 
52
+ ELASTIC_OP_MAP = {
53
+ operator.eq: "=",
54
+ operator.ne: "!",
55
+ operator.gt: "gt",
56
+ operator.lt: "lt",
57
+ operator.ge: "gte",
58
+ operator.le: "lte"
59
+ }
60
+
52
61
  # lookup table converting pythonic operators to oracle operators
53
62
  ORACLE_OP_MAP = {
54
63
  operator.eq: "==",
@@ -348,6 +357,56 @@ class FilterEngine:
348
357
 
349
358
  return query_str
350
359
 
360
+ def create_elastic_query(
361
+ self,
362
+ additional_filters: Optional["Iterable[FilterTuple]"] = None
363
+ ) -> dict[str, Any]:
364
+ """
365
+ Returns a single elastic query dictionary describing the filters expression.
366
+
367
+ :param additional_filters: additional filters to be applied to all clauses.
368
+ :returns: an elastic query dictionary describing the filters expression.
369
+ """
370
+
371
+ additional_filters = additional_filters or []
372
+ for or_group in self._filters:
373
+ for _filter in additional_filters:
374
+ or_group.append(list(_filter)) # type: ignore
375
+
376
+ should_clauses = []
377
+ for or_group in self._filters:
378
+ bool_query = {
379
+ "must": [],
380
+ "must_not": []
381
+ }
382
+
383
+ for and_group in or_group:
384
+ key, oper, value = and_group
385
+ key = str(key)
386
+
387
+ if isinstance(value, str) and any(char in value for char in ['*', '%']):
388
+ if value in ('*', '%', '*', '%'):
389
+ bool_query["must"].append({"wildcard": {key: value}})
390
+ else:
391
+ wildcard_query = {"wildcard": {key: value}}
392
+ if oper == operator.eq:
393
+ bool_query["must"].append(wildcard_query)
394
+ elif oper == operator.ne:
395
+ bool_query["must_not"].append(wildcard_query)
396
+ else:
397
+ if oper in [operator.lt, operator.gt, operator.ge, operator.le]:
398
+ elsop = ELASTIC_OP_MAP[oper]
399
+ bool_query["must"].append({"range": {key: {elsop: value}}})
400
+ elif oper == operator.eq:
401
+ bool_query["must"].append({"term": {key: value}})
402
+ elif oper == operator.ne:
403
+ bool_query["must_not"].append({"term": {key: value}})
404
+
405
+ should_clauses.append({"bool": bool_query})
406
+
407
+ query_expression = {"bool": {"should": should_clauses}}
408
+ return query_expression
409
+
351
410
  def create_postgres_query(
352
411
  self,
353
412
  additional_filters: Optional["Iterable[FilterTuple]"] = None,
@@ -551,7 +610,7 @@ class FilterEngine:
551
610
  """
552
611
  A (more) human readable format of <filters>.
553
612
  """
554
- operators_conversion_LUT_inv = {op2: op1 for op1, op2 in OPERATORS_CONVERSION_LUT.items()}
613
+ operators_conversion_lut_inv = {op2: op1 for op1, op2 in OPERATORS_CONVERSION_LUT.items()}
555
614
 
556
615
  filters = '\n'
557
616
  for or_group in self._filters:
@@ -559,10 +618,10 @@ class FilterEngine:
559
618
  key, oper, value = and_group
560
619
  if isinstance(key, InstrumentedAttribute):
561
620
  key = and_group[0].key
562
- if operators_conversion_LUT_inv[oper] == "":
621
+ if operators_conversion_lut_inv[oper] == "":
563
622
  oper = "eq"
564
623
  else:
565
- oper = operators_conversion_LUT_inv[oper]
624
+ oper = operators_conversion_lut_inv[oper]
566
625
  if isinstance(value, InstrumentedAttribute):
567
626
  value = and_group[2].key # type: ignore
568
627
  elif isinstance(value, DIDType):
@@ -100,9 +100,9 @@ class JSONDidMeta(DidMetaPlugin):
100
100
  if row_did_meta.meta:
101
101
  if session.bind.dialect.name in ['oracle', 'sqlite']:
102
102
  # Oracle and sqlite returns a string instead of a dict
103
- existing_meta = json_lib.loads(cast(str, row_did_meta.meta))
103
+ existing_meta = json_lib.loads(cast("str", row_did_meta.meta))
104
104
  else:
105
- existing_meta = cast(dict[str, Any], row_did_meta.meta)
105
+ existing_meta = cast("dict[str, Any]", row_did_meta.meta)
106
106
 
107
107
  for key, value in metadata.items():
108
108
  existing_meta[key] = value
@@ -28,10 +28,10 @@ if TYPE_CHECKING:
28
28
  from sqlalchemy.orm import Session
29
29
 
30
30
  IMMUTABLE_KEYS = [
31
- '_id', # index key
32
- 'scope', # generated on insert
33
- 'name', # generated on insert
34
- 'vo' # generated on insert
31
+ '_id', # index key
32
+ 'scope', # generated on insert
33
+ 'name', # generated on insert
34
+ 'vo' # generated on insert
35
35
  ]
36
36
 
37
37
 
@@ -46,28 +46,40 @@ class MongoDidMeta(DidMetaPlugin):
46
46
  password: "Optional[str]" = None,
47
47
  ):
48
48
  super(MongoDidMeta, self).__init__()
49
- if host is None:
50
- host = config.config_get('metadata', 'mongo_service_host')
51
- if port is None:
52
- port = config.config_get_int('metadata', 'mongo_service_port')
53
- if db is None:
54
- db = config.config_get('metadata', 'mongo_db')
55
- if collection is None:
56
- collection = config.config_get('metadata', 'mongo_collection')
57
-
58
- if user is None and config.config_has_section("metadata"):
59
- user = config.config_get("metadata", "mongo_user", default=None)
60
-
61
- if user is not None:
62
- if password is None:
63
- password = config.config_get("metadata", "mongo_password")
64
- auth = "{user}:{password}@".format(user=user, password=password)
65
- else:
66
- auth = ""
67
49
 
68
- self.client = pymongo.MongoClient("mongodb://{auth}{host}:{port}/".format(auth=auth, host=host, port=port))
69
- self.db = self.client[db]
70
- self.col = self.db[collection]
50
+ # Validate required parameters.
51
+ con_params = {
52
+ 'mongo_service_host': host,
53
+ 'mongo_service_port': port,
54
+ 'mongo_db': db,
55
+ 'mongo_collection': collection,
56
+ }
57
+
58
+ for param in con_params:
59
+ if con_params[param] is None:
60
+ if config.config_has_option('metadata', param):
61
+ con_params[param] = (
62
+ config.config_get_int('metadata', param)
63
+ if param == 'mongo_service_port'
64
+ else config.config_get('metadata', param)
65
+ )
66
+ else:
67
+ raise exception.ConnectionParameterNotFound(param)
68
+
69
+ if user is None and config.config_has_option('metadata', 'mongo_user'):
70
+ user = config.config_get('metadata', 'mongo_user', default=None)
71
+
72
+ if password is None and config.config_has_option('metadata', 'mongo_password'):
73
+ password = config.config_get('metadata', 'mongo_password')
74
+
75
+ # Set the auth (fallback to an anonymous connection if either user or password is not defined).
76
+ auth = "" if not user or not password else f"{user}:{password}@"
77
+
78
+ self.client = pymongo.MongoClient(
79
+ f"mongodb://{auth}{con_params['mongo_service_host']}:{con_params['mongo_service_port']}/"
80
+ )
81
+ self.db = self.client[con_params['mongo_db']]
82
+ self.col = self.db[con_params['mongo_collection']]
71
83
 
72
84
  self.plugin_name = "MONGO"
73
85
 
@@ -175,9 +187,10 @@ class MongoDidMeta(DidMetaPlugin):
175
187
 
176
188
  if recursive:
177
189
  # TODO: possible, but requires retrieving the results of a concurrent sqla query to call list_content on for datasets and containers
178
- raise exception.UnsupportedOperation("'{}' metadata module does not currently support recursive searches".format(
179
- self.plugin_name.lower()
180
- ))
190
+ raise exception.UnsupportedOperation(
191
+ "'{}' metadata module does not currently support recursive searches".format(
192
+ self.plugin_name.lower()
193
+ ))
181
194
 
182
195
  if long:
183
196
  query_result = self.col.find(mongo_query_str)
@@ -185,7 +198,7 @@ class MongoDidMeta(DidMetaPlugin):
185
198
  query_result = query_result.limit(limit)
186
199
  for did in query_result:
187
200
  did_full = "{}:{}".format(did['scope'], did['name'])
188
- if did_full not in ignore_dids: # aggregating recursive queries may contain duplicate DIDs
201
+ if did_full not in ignore_dids: # aggregating recursive queries may contain duplicate DIDs
189
202
  ignore_dids.add(did_full)
190
203
  yield {
191
204
  'scope': InternalScope(did['scope']),
@@ -200,7 +213,7 @@ class MongoDidMeta(DidMetaPlugin):
200
213
  query_result = query_result.limit(limit)
201
214
  for did in query_result:
202
215
  did_full = "{}:{}".format(did['scope'], did['name'])
203
- if did_full not in ignore_dids: # aggregating recursive queries may contain duplicate DIDs
216
+ if did_full not in ignore_dids: # aggregating recursive queries may contain duplicate DIDs
204
217
  ignore_dids.add(did_full)
205
218
  yield did['name']
206
219