rucio 37.7.1__py3-none-any.whl → 38.0.0rc1__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 (121) hide show
  1. rucio/alembicrevision.py +1 -1
  2. rucio/cli/bin_legacy/rucio.py +51 -107
  3. rucio/cli/bin_legacy/rucio_admin.py +26 -26
  4. rucio/cli/command.py +1 -0
  5. rucio/cli/did.py +2 -2
  6. rucio/cli/opendata.py +132 -0
  7. rucio/cli/replica.py +15 -5
  8. rucio/cli/rule.py +7 -2
  9. rucio/cli/scope.py +3 -2
  10. rucio/cli/utils.py +28 -4
  11. rucio/client/baseclient.py +9 -1
  12. rucio/client/client.py +2 -0
  13. rucio/client/diracclient.py +73 -12
  14. rucio/client/opendataclient.py +249 -0
  15. rucio/client/subscriptionclient.py +30 -0
  16. rucio/client/uploadclient.py +10 -13
  17. rucio/common/constants.py +4 -1
  18. rucio/common/exception.py +55 -0
  19. rucio/common/plugins.py +45 -8
  20. rucio/common/schema/generic.py +5 -3
  21. rucio/common/schema/generic_multi_vo.py +4 -2
  22. rucio/common/types.py +8 -7
  23. rucio/common/utils.py +176 -11
  24. rucio/core/dirac.py +5 -5
  25. rucio/core/opendata.py +744 -0
  26. rucio/core/rule.py +63 -8
  27. rucio/core/transfer.py +1 -1
  28. rucio/db/sqla/constants.py +6 -0
  29. rucio/db/sqla/migrate_repo/versions/a62db546a1f1_opendata_initial_model.py +85 -0
  30. rucio/db/sqla/models.py +67 -0
  31. rucio/db/sqla/util.py +2 -2
  32. rucio/gateway/dirac.py +1 -1
  33. rucio/gateway/opendata.py +190 -0
  34. rucio/gateway/subscription.py +5 -3
  35. rucio/rse/protocols/protocol.py +9 -5
  36. rucio/rse/translation.py +17 -6
  37. rucio/transfertool/fts3.py +1 -0
  38. rucio/transfertool/fts3_plugins.py +6 -1
  39. rucio/vcsversion.py +4 -4
  40. rucio/web/rest/flaskapi/v1/common.py +34 -14
  41. rucio/web/rest/flaskapi/v1/config.py +1 -1
  42. rucio/web/rest/flaskapi/v1/dids.py +447 -160
  43. rucio/web/rest/flaskapi/v1/heartbeats.py +1 -1
  44. rucio/web/rest/flaskapi/v1/identities.py +1 -1
  45. rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +1 -1
  46. rucio/web/rest/flaskapi/v1/locks.py +1 -1
  47. rucio/web/rest/flaskapi/v1/main.py +3 -8
  48. rucio/web/rest/flaskapi/v1/meta_conventions.py +1 -16
  49. rucio/web/rest/flaskapi/v1/nongrid_traces.py +1 -1
  50. rucio/web/rest/flaskapi/v1/opendata.py +391 -0
  51. rucio/web/rest/flaskapi/v1/opendata_public.py +146 -0
  52. rucio/web/rest/flaskapi/v1/requests.py +1 -1
  53. rucio/web/rest/flaskapi/v1/rses.py +1 -1
  54. rucio/web/rest/flaskapi/v1/rules.py +1 -1
  55. rucio/web/rest/flaskapi/v1/scopes.py +1 -1
  56. rucio/web/rest/flaskapi/v1/subscriptions.py +6 -9
  57. rucio/web/rest/flaskapi/v1/traces.py +1 -1
  58. rucio/web/rest/flaskapi/v1/vos.py +1 -1
  59. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/etc/alembic.ini.template +1 -1
  60. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/etc/alembic_offline.ini.template +1 -1
  61. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/etc/rucio.cfg.template +2 -2
  62. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/etc/rucio_multi_vo.cfg.template +3 -3
  63. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/requirements.server.txt +6 -3
  64. rucio-38.0.0rc1.data/data/rucio/tools/reset_database.py +87 -0
  65. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio +2 -1
  66. {rucio-37.7.1.dist-info → rucio-38.0.0rc1.dist-info}/METADATA +36 -36
  67. {rucio-37.7.1.dist-info → rucio-38.0.0rc1.dist-info}/RECORD +119 -113
  68. rucio/client/fileclient.py +0 -57
  69. rucio-37.7.1.data/data/rucio/tools/reset_database.py +0 -40
  70. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/etc/globus-config.yml.template +0 -0
  71. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/etc/ldap.cfg.template +0 -0
  72. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/etc/mail_templates/rule_approval_request.tmpl +0 -0
  73. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +0 -0
  74. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/etc/mail_templates/rule_approved_user.tmpl +0 -0
  75. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +0 -0
  76. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/etc/mail_templates/rule_denied_user.tmpl +0 -0
  77. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +0 -0
  78. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/etc/rse-accounts.cfg.template +0 -0
  79. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/etc/rucio.cfg.atlas.client.template +0 -0
  80. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/tools/bootstrap.py +0 -0
  81. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/data/rucio/tools/merge_rucio_configs.py +0 -0
  82. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-abacus-account +0 -0
  83. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-abacus-collection-replica +0 -0
  84. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-abacus-rse +0 -0
  85. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-admin +0 -0
  86. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-atropos +0 -0
  87. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-auditor +0 -0
  88. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-automatix +0 -0
  89. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-bb8 +0 -0
  90. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-cache-client +0 -0
  91. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-cache-consumer +0 -0
  92. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-conveyor-finisher +0 -0
  93. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-conveyor-poller +0 -0
  94. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-conveyor-preparer +0 -0
  95. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-conveyor-receiver +0 -0
  96. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-conveyor-stager +0 -0
  97. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-conveyor-submitter +0 -0
  98. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-conveyor-throttler +0 -0
  99. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-dark-reaper +0 -0
  100. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-dumper +0 -0
  101. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-follower +0 -0
  102. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-hermes +0 -0
  103. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-judge-cleaner +0 -0
  104. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-judge-evaluator +0 -0
  105. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-judge-injector +0 -0
  106. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-judge-repairer +0 -0
  107. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-kronos +0 -0
  108. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-minos +0 -0
  109. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-minos-temporary-expiration +0 -0
  110. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-necromancer +0 -0
  111. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-oauth-manager +0 -0
  112. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-reaper +0 -0
  113. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-replica-recoverer +0 -0
  114. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-rse-decommissioner +0 -0
  115. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-storage-consistency-actions +0 -0
  116. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-transmogrifier +0 -0
  117. {rucio-37.7.1.data → rucio-38.0.0rc1.data}/scripts/rucio-undertaker +0 -0
  118. {rucio-37.7.1.dist-info → rucio-38.0.0rc1.dist-info}/WHEEL +0 -0
  119. {rucio-37.7.1.dist-info → rucio-38.0.0rc1.dist-info}/licenses/AUTHORS.rst +0 -0
  120. {rucio-37.7.1.dist-info → rucio-38.0.0rc1.dist-info}/licenses/LICENSE +0 -0
  121. {rucio-37.7.1.dist-info → rucio-38.0.0rc1.dist-info}/top_level.txt +0 -0
rucio/core/rule.py CHANGED
@@ -100,21 +100,22 @@ class AutoApprove(PolicyPackageAlgorithms):
100
100
 
101
101
  _algorithm_type = 'auto_approve'
102
102
 
103
- def __init__(self, rule: models.ReplicationRule, did: models.DataIdentifier, session: 'Session') -> None:
103
+ def __init__(self, rule: models.ReplicationRule, did: models.DataIdentifier, session: 'Session', vo: str = DEFAULT_VO) -> None:
104
104
  super().__init__()
105
105
  self.rule = rule
106
106
  self.did = did
107
107
  self.session = session
108
+ self.vo = vo
108
109
  self.register("default", self.default)
109
110
 
110
111
  def evaluate(self) -> bool:
111
112
  """
112
113
  Evaluate the auto-approve algorithm
113
114
  """
114
- return self.get_configured_algorithm()(self.rule, self.did, self.session)
115
+ return self.get_configured_algorithm(self.vo)(self.rule, self.did, self.session)
115
116
 
116
117
  @classmethod
117
- def get_configured_algorithm(cls: type[AutoApproveT]) -> "Callable[[models.ReplicationRule, models.DataIdentifier, Session], bool]":
118
+ def get_configured_algorithm(cls: type[AutoApproveT], vo: str) -> "Callable[[models.ReplicationRule, models.DataIdentifier, Session], bool]":
118
119
  """
119
120
  Get the configured auto-approve algorithm
120
121
  """
@@ -123,7 +124,12 @@ class AutoApprove(PolicyPackageAlgorithms):
123
124
  except (NoOptionError, NoSectionError, RuntimeError):
124
125
  configured_algorithm = 'default'
125
126
 
126
- return super()._get_one_algorithm(cls._algorithm_type, configured_algorithm)
127
+ result = None
128
+ if configured_algorithm == 'default':
129
+ result = super()._get_default_algorithm(cls._algorithm_type, vo)
130
+ if result is None:
131
+ result = super()._get_one_algorithm(cls._algorithm_type, configured_algorithm)
132
+ return result
127
133
 
128
134
  @classmethod
129
135
  def register(cls: type[AutoApproveT], name: str, fn_auto_approve: "Callable[[models.ReplicationRule, models.DataIdentifier, Session], bool]") -> None:
@@ -390,7 +396,7 @@ def add_rule(
390
396
  if ask_approval:
391
397
  new_rule.state = RuleState.WAITING_APPROVAL
392
398
  # Use the new rule as the argument here
393
- auto_approver = AutoApprove(new_rule, did, session=session)
399
+ auto_approver = AutoApprove(new_rule, did, session=session, vo=account.vo)
394
400
  if auto_approver.evaluate():
395
401
  logger(logging.DEBUG, "Auto approving rule %s", str(new_rule.id))
396
402
  logger(logging.DEBUG, "Created rule %s for injection", str(new_rule.id))
@@ -1261,6 +1267,7 @@ def repair_rule(
1261
1267
  # created.
1262
1268
  # (C) Transfers fail and mark locks (and the rule) as STUCK. All STUCK locks have to be repaired.
1263
1269
  # (D) Files are declared as BAD.
1270
+ # (E) Stuck locks are found on RSEs that do not belong to the target RSEs.
1264
1271
 
1265
1272
  # start_time = time.time()
1266
1273
  try:
@@ -1314,6 +1321,43 @@ def repair_rule(
1314
1321
  logger(logging.DEBUG, '%s while repairing rule %s', str(error), rule_id)
1315
1322
  return
1316
1323
 
1324
+ # Get all stuck locks for this rule ID
1325
+ stmt = select(
1326
+ models.ReplicaLock.rse_id,
1327
+ func.count().label('lock_count')
1328
+ ).where(
1329
+ and_(models.ReplicaLock.rule_id == rule.id,
1330
+ models.ReplicaLock.state == LockState.STUCK)
1331
+ ).group_by(
1332
+ models.ReplicaLock.rse_id
1333
+ )
1334
+ stuck_locks_by_rse = session.execute(stmt).all()
1335
+
1336
+ stuck_locks_on_nontarget_rses = []
1337
+
1338
+ # Check if any of the locks found are not on our target RSEs
1339
+ target_rse_ids = {rse['id'] for rse in target_rses}
1340
+ for stuck_lock in stuck_locks_by_rse:
1341
+ if stuck_lock.rse_id not in target_rse_ids:
1342
+ rse_name = get_rse_name(rse_id=stuck_lock.rse_id, session=session)
1343
+ stuck_locks_on_nontarget_rses.append({
1344
+ 'rse_id': stuck_lock.rse_id,
1345
+ 'rse_name': rse_name,
1346
+ 'lock_count': stuck_lock.lock_count
1347
+ })
1348
+
1349
+ # Add to rule error if found
1350
+ if stuck_locks_on_nontarget_rses:
1351
+ error_msg = "Found stuck locks on RSEs not matching target expression: "
1352
+ error_msg += ", ".join([f"{rse['rse_name']} ({rse['lock_count']})" for rse in stuck_locks_on_nontarget_rses])
1353
+
1354
+ if rule.error:
1355
+ error_msg = rule.error + '|' + error_msg
1356
+
1357
+ rule.error = (error_msg[:245] + '...') if len(error_msg) > 245 else error_msg
1358
+
1359
+ logger(logging.WARNING, "Rule %s: %s", str(rule.id), error_msg)
1360
+
1317
1361
  # Create the RSESelector
1318
1362
  try:
1319
1363
  rseselector = RSESelector(account=rule.account,
@@ -1324,7 +1368,12 @@ def repair_rule(
1324
1368
  session=session)
1325
1369
  except (InvalidRuleWeight, InsufficientTargetRSEs, InsufficientAccountLimit) as error:
1326
1370
  rule.state = RuleState.STUCK
1327
- rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
1371
+
1372
+ error_msg = str(error)
1373
+ if rule.error:
1374
+ error_msg = rule.error + '|' + error_msg
1375
+ rule.error = (error_msg[:245] + '...') if len(error_msg) > 245 else error_msg
1376
+
1328
1377
  rule.save(session=session)
1329
1378
  # Insert rule history
1330
1379
  insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
@@ -1410,7 +1459,10 @@ def repair_rule(
1410
1459
  session=session)
1411
1460
  except (InsufficientAccountLimit, InsufficientTargetRSEs) as error:
1412
1461
  rule.state = RuleState.STUCK
1413
- rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
1462
+ error_msg = str(error)
1463
+ if rule.error:
1464
+ error_msg = rule.error + '|' + error_msg
1465
+ rule.error = (error_msg[:245] + '...') if len(error_msg) > 245 else error_msg
1414
1466
  rule.save(session=session)
1415
1467
  # Insert rule history
1416
1468
  insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
@@ -1450,7 +1502,10 @@ def repair_rule(
1450
1502
  session=session)
1451
1503
  except (InsufficientAccountLimit, InsufficientTargetRSEs) as error:
1452
1504
  rule.state = RuleState.STUCK
1453
- rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
1505
+ error_msg = str(error)
1506
+ if rule.error:
1507
+ error_msg = rule.error + '|' + error_msg
1508
+ rule.error = (error_msg[:245] + '...') if len(error_msg) > 245 else error_msg
1454
1509
  rule.save(session=session)
1455
1510
  # Insert rule history
1456
1511
  insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
rucio/core/transfer.py CHANGED
@@ -239,7 +239,7 @@ class DirectTransferImplementation(DirectTransfer):
239
239
  # DQ2 path always starts with /, but prefix might not end with /
240
240
  naming_convention = dst.rse.attributes.get(RseAttr.NAMING_CONVENTION, None)
241
241
  if rws.scope.external is not None:
242
- dest_path = construct_non_deterministic_pfn(dsn, rws.scope.external, rws.name, naming_convention)
242
+ dest_path = construct_non_deterministic_pfn(dsn, rws.scope.external, rws.name, naming_convention, rws.scope.vo)
243
243
  if dst.rse.is_tape():
244
244
  if rws.retry_count or rws.activity == 'Recovery':
245
245
  dest_path = '%s_%i' % (dest_path, int(time.time()))
@@ -84,6 +84,12 @@ class DIDType(Enum):
84
84
  DELETED_CONTAINER = 'Z'
85
85
 
86
86
 
87
+ class OpenDataDIDState(Enum):
88
+ PUBLIC = 'P'
89
+ DRAFT = 'D'
90
+ SUSPENDED = 'S'
91
+
92
+
87
93
  class IdentityType(Enum):
88
94
  X509 = 'X509'
89
95
  GSS = 'GSS'
@@ -0,0 +1,85 @@
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
+ """Opendata initial model""" # noqa: D400, D415
16
+
17
+ import sqlalchemy as sa
18
+ from alembic import op
19
+
20
+ from rucio.common.schema import get_schema_value
21
+ from rucio.db.sqla.constants import OpenDataDIDState
22
+ from rucio.db.sqla.types import JSON
23
+
24
+ # Alembic revision identifiers
25
+ revision = 'a62db546a1f1'
26
+ down_revision = '30d5206e9cad'
27
+
28
+
29
+ def upgrade():
30
+ op.create_table(
31
+ 'dids_opendata',
32
+ sa.Column('scope', sa.String(length=get_schema_value('SCOPE_LENGTH')), nullable=False),
33
+ sa.Column('name', sa.String(length=get_schema_value('NAME_LENGTH')), nullable=False),
34
+ sa.Column('state', sa.Enum(OpenDataDIDState, name='DID_OPENDATA_STATE_CHK',
35
+ values_callable=lambda obj: [e.value for e in obj]), nullable=True,
36
+ server_default=OpenDataDIDState.DRAFT.value),
37
+ sa.Column('created_at', sa.DateTime(), nullable=True),
38
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
39
+ sa.PrimaryKeyConstraint('scope', 'name', name='OPENDATA_DID_PK'),
40
+ sa.ForeignKeyConstraint(['scope', 'name'], ['dids.scope', 'dids.name'],
41
+ ondelete='CASCADE', name='OPENDATA_DID_FK')
42
+ )
43
+ op.create_index('OPENDATA_DID_UPDATED_AT_IDX', 'dids_opendata', ['updated_at'])
44
+ op.create_index('OPENDATA_DID_CREATED_AT_IDX', 'dids_opendata', ['created_at'])
45
+ op.create_index('OPENDATA_DID_STATE_IDX', 'dids_opendata', ['state'])
46
+ op.create_index('OPENDATA_DID_STATE_UPDATED_AT_IDX', 'dids_opendata', ['state', 'updated_at'])
47
+
48
+ op.create_table(
49
+ 'dids_opendata_doi',
50
+ sa.Column('scope', sa.String(length=get_schema_value('SCOPE_LENGTH')), nullable=False),
51
+ sa.Column('name', sa.String(length=get_schema_value('NAME_LENGTH')), nullable=False),
52
+ sa.Column('doi', sa.String(length=255), nullable=False, unique=True),
53
+ sa.Column('created_at', sa.DateTime(), nullable=True),
54
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
55
+ sa.PrimaryKeyConstraint('scope', 'name', name='OPENDATA_DOI_PK'),
56
+ sa.ForeignKeyConstraint(['scope', 'name'], ['dids_opendata.scope', 'dids_opendata.name'],
57
+ ondelete='CASCADE', name='OPENDATA_DOI_FK')
58
+ )
59
+ op.create_index('OPENDATA_DOI_UPDATED_AT_IDX', 'dids_opendata_doi', ['updated_at'])
60
+ op.create_index('OPENDATA_DOI_CREATED_AT_IDX', 'dids_opendata_doi', ['created_at'])
61
+
62
+ op.create_table(
63
+ 'dids_opendata_meta',
64
+ sa.Column('scope', sa.String(length=get_schema_value('SCOPE_LENGTH')), nullable=False),
65
+ sa.Column('name', sa.String(length=get_schema_value('NAME_LENGTH')), nullable=False),
66
+ sa.Column('meta', JSON(), nullable=False),
67
+ sa.Column('created_at', sa.DateTime(), nullable=True),
68
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
69
+ sa.PrimaryKeyConstraint('scope', 'name', name='OPENDATA_META_PK'),
70
+ sa.ForeignKeyConstraint(['scope', 'name'], ['dids_opendata.scope', 'dids_opendata.name'],
71
+ ondelete='CASCADE', name='OPENDATA_META_FK')
72
+ )
73
+
74
+
75
+ def downgrade():
76
+ op.drop_table('dids_opendata_meta')
77
+ op.drop_table('dids_opendata_doi')
78
+ op.drop_index('OPENDATA_DID_STATE_UPDATED_AT_IDX', table_name='dids_opendata')
79
+ op.drop_index('OPENDATA_DID_STATE_IDX', table_name='dids_opendata')
80
+ op.drop_index('OPENDATA_DID_CREATED_AT_IDX', table_name='dids_opendata')
81
+ op.drop_index('OPENDATA_DID_UPDATED_AT_IDX', table_name='dids_opendata')
82
+ op.drop_table('dids_opendata')
83
+
84
+ # Drop enum if created in this migration
85
+ sa.Enum(name='DID_OPENDATA_STATE_CHK').drop(op.get_bind(), checkfirst=True)
rucio/db/sqla/models.py CHANGED
@@ -43,6 +43,7 @@ from rucio.db.sqla.constants import (
43
43
  KeyType,
44
44
  LifetimeExceptionsState,
45
45
  LockState,
46
+ OpenDataDIDState,
46
47
  ReplicaState,
47
48
  RequestState,
48
49
  RequestType,
@@ -468,6 +469,72 @@ class DataIdentifier(BASE, ModelBase):
468
469
  Index('DIDS_EXPIRED_AT_IDX', 'expired_at'))
469
470
 
470
471
 
472
+ class OpenDataDid(BASE, ModelBase):
473
+ """DIDs which are part of OpenData"""
474
+ __tablename__ = 'dids_opendata'
475
+
476
+ scope: Mapped[InternalScope] = mapped_column(InternalScopeString(common_schema.get_schema_value('SCOPE_LENGTH')))
477
+ name: Mapped[str] = mapped_column(String(common_schema.get_schema_value('NAME_LENGTH')))
478
+ state: Mapped[Optional[OpenDataDIDState]] = mapped_column(Enum(OpenDataDIDState, name='DID_OPENDATA_STATE_CHK',
479
+ create_constraint=True,
480
+ values_callable=lambda obj: [e.value for e in obj]),
481
+ default=OpenDataDIDState.DRAFT)
482
+
483
+ __table_args__ = (
484
+ PrimaryKeyConstraint('scope', 'name', name='OPENDATA_DID_PK'),
485
+ ForeignKeyConstraint(
486
+ ['scope', 'name'],
487
+ ['dids.scope', 'dids.name'],
488
+ name='OPENDATA_DID_FK',
489
+ ondelete='CASCADE',
490
+ ),
491
+ Index('OPENDATA_DID_UPDATED_AT_IDX', 'updated_at'),
492
+ Index('OPENDATA_DID_CREATED_AT_IDX', 'created_at'),
493
+ Index('OPENDATA_DID_STATE_IDX', 'state'),
494
+ Index('OPENDATA_DID_STATE_UPDATED_AT_IDX', 'state', 'updated_at'),
495
+ )
496
+
497
+
498
+ class OpenDataDOI(BASE, ModelBase):
499
+ """Mapping between OpenData DIDs and DOIs"""
500
+ __tablename__ = 'dids_opendata_doi'
501
+
502
+ scope: Mapped[InternalScope] = mapped_column(InternalScopeString(common_schema.get_schema_value('SCOPE_LENGTH')))
503
+ name: Mapped[str] = mapped_column(String(common_schema.get_schema_value('NAME_LENGTH')))
504
+ doi: Mapped[str] = mapped_column(String(255), unique=True)
505
+
506
+ __table_args__ = (
507
+ PrimaryKeyConstraint('scope', 'name', name='OPENDATA_DOI_PK'),
508
+ ForeignKeyConstraint(
509
+ ['scope', 'name'],
510
+ ['dids_opendata.scope', 'dids_opendata.name'],
511
+ name='OPENDATA_DOI_FK',
512
+ ondelete='CASCADE',
513
+ ),
514
+ # Not working on all DB, we add the constraint on insert
515
+ # CheckConstraint("doi ~* '^10\\.[0-9]{4,9}/[-._;()/:A-Z0-9]+$'", name='OPENDATA_DOI_FORMAT_CHK'),
516
+ Index('OPENDATA_DOI_UPDATED_AT_IDX', 'updated_at'),
517
+ Index('OPENDATA_DOI_CREATED_AT_IDX', 'created_at'),
518
+ )
519
+
520
+ class OpenDataMeta(BASE, ModelBase):
521
+ """Mapping between OpenData DIDs and DOIs"""
522
+ __tablename__ = 'dids_opendata_meta'
523
+
524
+ scope: Mapped[InternalScope] = mapped_column(InternalScopeString(common_schema.get_schema_value('SCOPE_LENGTH')))
525
+ name: Mapped[str] = mapped_column(String(common_schema.get_schema_value('NAME_LENGTH')))
526
+ meta = mapped_column(JSON(), nullable=False)
527
+
528
+ __table_args__ = (
529
+ PrimaryKeyConstraint('scope', 'name', name='OPENDATA_META_PK'),
530
+ ForeignKeyConstraint(
531
+ ['scope', 'name'],
532
+ ['dids_opendata.scope', 'dids_opendata.name'],
533
+ name='OPENDATA_META_FK',
534
+ ondelete='CASCADE',
535
+ ),
536
+ )
537
+
471
538
  class VirtualPlacements(BASE, ModelBase):
472
539
  """Represents virtual placements"""
473
540
  __tablename__ = 'virtual_placements'
rucio/db/sqla/util.py CHANGED
@@ -81,7 +81,7 @@ def dump_schema() -> None:
81
81
  models.register_models(engine)
82
82
 
83
83
 
84
- def destroy_database() -> None:
84
+ def drop_orm_tables() -> None:
85
85
  """ Removes the schema from the database. Only useful for test cases or malicious intents. """
86
86
  engine = get_engine()
87
87
 
@@ -91,7 +91,7 @@ def destroy_database() -> None:
91
91
  print('Cannot destroy schema -- assuming already gone, continuing:', e)
92
92
 
93
93
 
94
- def drop_everything() -> None:
94
+ def purge_db() -> None:
95
95
  """
96
96
  Pre-gather all named constraints and table names, and drop everything.
97
97
  This is better than using metadata.reflect(); metadata.drop_all()
rucio/gateway/dirac.py CHANGED
@@ -56,7 +56,7 @@ def add_files(
56
56
  dids = []
57
57
  rses = {}
58
58
  for lfn in lfns:
59
- scope, name = extract_scope(lfn['lfn'], scopes)
59
+ scope, name = extract_scope(lfn['lfn'], scopes, vo=vo)
60
60
  dids.append({'scope': scope, 'name': name})
61
61
  rse = lfn['rse']
62
62
  if rse not in rses:
@@ -0,0 +1,190 @@
1
+ # Copyright European Organization for Nuclear Research (CERN) since 2012
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ from typing import TYPE_CHECKING, Any, Optional
17
+
18
+ from rucio.common.constants import DEFAULT_VO
19
+ from rucio.common.types import InternalScope
20
+ from rucio.common.utils import gateway_update_return_dict
21
+ from rucio.core import opendata
22
+ from rucio.core.opendata import opendata_state_str_to_enum, validate_opendata_did_state
23
+ from rucio.db.sqla.constants import DatabaseOperationType
24
+ from rucio.db.sqla.session import db_session
25
+
26
+ if TYPE_CHECKING:
27
+ from rucio.common.constants import OPENDATA_DID_STATE_LITERAL
28
+
29
+
30
+ def list_opendata_dids(
31
+ *,
32
+ limit: Optional[int] = None,
33
+ offset: Optional[int] = None,
34
+ state: Optional["OPENDATA_DID_STATE_LITERAL"] = None,
35
+ ) -> dict[str, list[dict[str, Any]]]:
36
+ """
37
+ List Opendata DIDs from the Opendata catalog.
38
+
39
+ Parameters:
40
+ limit: Maximum number of DIDs to return.
41
+ offset: Number of DIDs to skip before starting to collect the result set.
42
+ state: Filter DIDs by their state.
43
+
44
+ Returns:
45
+ A dictionary with a list of DIDs matching the criteria.
46
+ """
47
+
48
+ state_enum = None
49
+ if state is not None:
50
+ state = validate_opendata_did_state(state)
51
+ state_enum = opendata_state_str_to_enum(state)
52
+ with db_session(DatabaseOperationType.READ) as session:
53
+ result = opendata.list_opendata_dids(limit=limit, offset=offset, state=state_enum, session=session)
54
+ return result
55
+
56
+
57
+ def get_opendata_did(
58
+ *,
59
+ scope: str,
60
+ name: str,
61
+ state: Optional["OPENDATA_DID_STATE_LITERAL"] = None,
62
+ include_files: bool = True,
63
+ include_metadata: bool = False,
64
+ include_doi: bool = True,
65
+ vo: str = DEFAULT_VO,
66
+ ) -> dict[str, Any]:
67
+ """
68
+ Retrieve a specific Opendata DID from the Opendata catalog.
69
+
70
+ Parameters:
71
+ scope: The scope of the DID.
72
+ name: The name of the DID.
73
+ state: Optional state to filter the DID.
74
+ include_files: Whether to include files in the result.
75
+ include_metadata: Whether to include metadata in the result.
76
+ include_doi: Whether to include DOI information in the result.
77
+ vo: The virtual organization.
78
+
79
+ Returns:
80
+ A dictionary containing the details of the requested DID.
81
+ """
82
+
83
+ internal_scope = InternalScope(scope, vo=vo)
84
+ state_enum = None
85
+ if state is not None:
86
+ state = validate_opendata_did_state(state)
87
+ state_enum = opendata_state_str_to_enum(state)
88
+
89
+ with db_session(DatabaseOperationType.READ) as session:
90
+ result = opendata.get_opendata_did(scope=internal_scope,
91
+ name=name,
92
+ state=state_enum,
93
+ include_files=include_files,
94
+ include_metadata=include_metadata,
95
+ include_doi=include_doi,
96
+ session=session)
97
+ return gateway_update_return_dict(result, session=session)
98
+
99
+
100
+ def add_opendata_did(
101
+ *,
102
+ scope: str,
103
+ name: str,
104
+ vo: str = DEFAULT_VO,
105
+ ) -> None:
106
+ """
107
+ Add a new Opendata DID to the Opendata catalog.
108
+
109
+ Parameters:
110
+ scope: The scope of the DID.
111
+ name: The name of the DID.
112
+ vo: The virtual organization.
113
+
114
+ Returns:
115
+ None
116
+ """
117
+
118
+ internal_scope = InternalScope(scope, vo=vo)
119
+ with db_session(DatabaseOperationType.WRITE) as session:
120
+ return opendata.add_opendata_did(scope=internal_scope, name=name, session=session)
121
+
122
+
123
+ def delete_opendata_did(
124
+ *,
125
+ scope: str,
126
+ name: str,
127
+ vo: str = DEFAULT_VO,
128
+ ) -> None:
129
+ """
130
+ Delete an Opendata DID from the Opendata catalog.
131
+
132
+ Parameters:
133
+ scope: The scope of the DID.
134
+ name: The name of the DID.
135
+ vo: The virtual organization.
136
+
137
+ Returns:
138
+ None
139
+ """
140
+
141
+ internal_scope = InternalScope(scope, vo=vo)
142
+ with db_session(DatabaseOperationType.WRITE) as session:
143
+ return opendata.delete_opendata_did(scope=internal_scope, name=name, session=session)
144
+
145
+
146
+ def update_opendata_did(
147
+ *,
148
+ scope: str,
149
+ name: str,
150
+ state: Optional["OPENDATA_DID_STATE_LITERAL"] = None,
151
+ meta: Optional[dict] = None,
152
+ doi: Optional[str] = None,
153
+ vo: str = DEFAULT_VO,
154
+ ) -> None:
155
+ """
156
+ Update an existing Opendata DID in the Opendata catalog.
157
+
158
+ Parameters:
159
+ scope: The scope of the DID.
160
+ name: The name of the DID.
161
+ state: Optional new state for the DID.
162
+ meta: Optional metadata dictionary or JSON string.
163
+ doi: Optional DOI string.
164
+ vo: The virtual organization.
165
+
166
+ Returns:
167
+ None
168
+
169
+ Raises:
170
+ ValueError: If meta is a string and cannot be parsed as valid JSON.
171
+ """
172
+
173
+ internal_scope = InternalScope(scope, vo=vo)
174
+ state_enum = None
175
+ if state is not None:
176
+ state = validate_opendata_did_state(state)
177
+ state_enum = opendata_state_str_to_enum(state)
178
+ if isinstance(meta, str):
179
+ try:
180
+ meta = json.loads(meta)
181
+ except ValueError as error:
182
+ raise ValueError(f"Invalid JSON: {error}")
183
+
184
+ with db_session(DatabaseOperationType.WRITE) as session:
185
+ return opendata.update_opendata_did(scope=internal_scope,
186
+ name=name,
187
+ state=state_enum,
188
+ meta=meta,
189
+ doi=doi,
190
+ session=session)
@@ -21,7 +21,7 @@ from rucio.common.exception import AccessDenied, InvalidObject
21
21
  from rucio.common.schema import validate_schema
22
22
  from rucio.common.types import InternalAccount, InternalScope
23
23
  from rucio.core import subscription
24
- from rucio.db.sqla.constants import DatabaseOperationType
24
+ from rucio.db.sqla.constants import DatabaseOperationType, SubscriptionState
25
25
  from rucio.db.sqla.session import db_session
26
26
  from rucio.gateway.permission import has_permission
27
27
 
@@ -110,7 +110,7 @@ def update_subscription(
110
110
 
111
111
  :param name: Name of the subscription
112
112
  :param account: Account identifier
113
- :param metadata: Dictionary of metadata to update. Supported keys : filter, replication_rules, comments, lifetime, retroactive, dry_run, priority, last_processed
113
+ :param metadata: Dictionary of metadata to update. Supported keys : filter, replication_rules, comments, lifetime, retroactive, dry_run, priority, last_processed, state
114
114
  :param issuer: The account issuing this operation.
115
115
  :param vo: The VO to act on.
116
116
  :raises: SubscriptionNotFound if subscription is not found
@@ -132,6 +132,9 @@ def update_subscription(
132
132
  else:
133
133
  for rule in metadata['replication_rules']:
134
134
  validate_schema(name='activity', obj=rule.get('activity', 'default'), vo=vo)
135
+ if 'state' in metadata and metadata['state'] is not None:
136
+ metadata['state'] = SubscriptionState(metadata['state'])
137
+
135
138
  except ValueError as error:
136
139
  raise TypeError(error)
137
140
 
@@ -148,7 +151,6 @@ def update_subscription(
148
151
  filter_[_key] = [_type(val, vo=vo).internal for val in filter_[_key]]
149
152
  else:
150
153
  filter_[_key] = _type(filter_[_key], vo=vo).internal
151
-
152
154
  return subscription.update_subscription(name=name, account=internal_account, metadata=metadata, session=session)
153
155
 
154
156
 
@@ -69,11 +69,15 @@ class RSEProtocol(ABC):
69
69
  self.rse = rse_settings
70
70
  self.logger = logger
71
71
  if self.rse['deterministic']:
72
- self.translator = RSEDeterministicTranslation(self.rse['rse'], rse_settings, self.attributes)
73
- if getattr(rsemanager, 'CLIENT_MODE', None) and \
74
- not RSEDeterministicTranslation.supports(self.rse.get('lfn2pfn_algorithm')):
75
- # Remote server has an algorithm we don't understand; always make the server do the lookup.
76
- setattr(self, 'lfns2pfns', self.__lfns2pfns_client)
72
+ if getattr(rsemanager, 'SERVER_MODE', None):
73
+ vo = get_rse_vo(self.rse['id'])
74
+ if getattr(rsemanager, 'CLIENT_MODE', None):
75
+ # assume client has only one VO policy package configured
76
+ vo = ''
77
+ if not RSEDeterministicTranslation.supports(self.rse.get('lfn2pfn_algorithm')):
78
+ # Remote server has an algorithm we don't understand; always make the server do the lookup.
79
+ setattr(self, 'lfns2pfns', self.__lfns2pfns_client)
80
+ self.translator = RSEDeterministicTranslation(self.rse['rse'], rse_settings, self.attributes, vo)
77
81
  else:
78
82
  if getattr(rsemanager, 'CLIENT_MODE', None):
79
83
  setattr(self, 'lfns2pfns', self.__lfns2pfns_client)
rucio/rse/translation.py CHANGED
@@ -50,7 +50,7 @@ class RSEDeterministicScopeTranslation(PolicyPackageAlgorithms):
50
50
  algorithm_name = "def"
51
51
  logger.debug("PFN2LFN: Falling back to %s algorithm.", 'default' if algorithm_name == 'def' else algorithm_name)
52
52
 
53
- self.parser = self.get_parser(algorithm_name)
53
+ self.parser = self.get_parser(algorithm_name, vo)
54
54
 
55
55
  @classmethod
56
56
  def _module_init_(cls) -> None:
@@ -60,8 +60,14 @@ class RSEDeterministicScopeTranslation(PolicyPackageAlgorithms):
60
60
  cls.register(cls._default, "def")
61
61
 
62
62
  @classmethod
63
- def get_parser(cls, algorithm_name: str) -> 'Callable[..., Any]':
64
- return super()._get_one_algorithm(cls._algorithm_type, algorithm_name)
63
+ def get_parser(cls, algorithm_name: str, vo: str) -> 'Callable[..., Any]':
64
+ result = None
65
+ if algorithm_name == vo:
66
+ # default algorithm for VO
67
+ result = super()._get_default_algorithm(RSEDeterministicScopeTranslation._algorithm_type, vo)
68
+ if result is None:
69
+ result = super()._get_one_algorithm(cls._algorithm_type, algorithm_name)
70
+ return result
65
71
 
66
72
  @classmethod
67
73
  def register(
@@ -111,7 +117,8 @@ class RSEDeterministicTranslation(PolicyPackageAlgorithms):
111
117
  self,
112
118
  rse: Optional[str] = None,
113
119
  rse_attributes: Optional["RSESettingsDict"] = None,
114
- protocol_attributes: Optional[dict[str, Any]] = None
120
+ protocol_attributes: Optional[dict[str, Any]] = None,
121
+ vo: str = DEFAULT_VO
115
122
  ):
116
123
  """
117
124
  Initialize a translator object from the RSE, its attributes, and the protocol-specific
@@ -125,6 +132,7 @@ class RSEDeterministicTranslation(PolicyPackageAlgorithms):
125
132
  self.rse = rse
126
133
  self.rse_attributes = rse_attributes if rse_attributes else {}
127
134
  self.protocol_attributes = protocol_attributes if protocol_attributes else {}
135
+ self.vo = vo
128
136
 
129
137
  @classmethod
130
138
  def supports(
@@ -251,9 +259,12 @@ class RSEDeterministicTranslation(PolicyPackageAlgorithms):
251
259
  :returns: RSE specific URI of the physical file
252
260
  """
253
261
  algorithm = self.rse_attributes.get(RseAttr.LFN2PFN_ALGORITHM, 'default')
254
- if algorithm == 'default':
262
+ algorithm_callable = None
263
+ if algorithm == 'default' or algorithm == RSEDeterministicTranslation._DEFAULT_LFN2PFN:
255
264
  algorithm = RSEDeterministicTranslation._DEFAULT_LFN2PFN
256
- algorithm_callable = super()._get_one_algorithm(RSEDeterministicTranslation._algorithm_type, algorithm)
265
+ algorithm_callable = super()._get_default_algorithm(RSEDeterministicTranslation._algorithm_type, self.vo)
266
+ if algorithm_callable is None:
267
+ algorithm_callable = super()._get_one_algorithm(RSEDeterministicTranslation._algorithm_type, algorithm)
257
268
  return algorithm_callable(scope, name, self.rse, self.rse_attributes, self.protocol_attributes)
258
269
 
259
270
 
@@ -1046,6 +1046,7 @@ class FTS3Transfertool(Transfertool):
1046
1046
  t_file['scitag'] = self.scitags_exp_id << 6 | activity_id
1047
1047
 
1048
1048
  if t_file['metadata']['dst_type'] == 'TAPE':
1049
+ t_file['metadata']['vo'] = rws.scope.vo
1049
1050
  for plugin in self.tape_metadata_plugins:
1050
1051
  t_file = deep_merge_dict(source=plugin.hints(t_file['metadata']), destination=t_file)
1051
1052