rucio 37.7.0__py3-none-any.whl → 38.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.
- rucio/alembicrevision.py +1 -1
- rucio/cli/bin_legacy/rucio.py +51 -107
- rucio/cli/bin_legacy/rucio_admin.py +26 -26
- rucio/cli/command.py +1 -0
- rucio/cli/did.py +2 -2
- rucio/cli/opendata.py +132 -0
- rucio/cli/replica.py +15 -5
- rucio/cli/rule.py +7 -2
- rucio/cli/scope.py +3 -2
- rucio/cli/utils.py +28 -4
- rucio/client/baseclient.py +9 -1
- rucio/client/client.py +2 -0
- rucio/client/diracclient.py +73 -12
- rucio/client/opendataclient.py +249 -0
- rucio/client/subscriptionclient.py +30 -0
- rucio/client/uploadclient.py +10 -13
- rucio/common/constants.py +4 -1
- rucio/common/exception.py +55 -0
- rucio/common/plugins.py +45 -8
- rucio/common/schema/generic.py +5 -3
- rucio/common/schema/generic_multi_vo.py +4 -2
- rucio/common/types.py +8 -7
- rucio/common/utils.py +176 -11
- rucio/core/dirac.py +5 -5
- rucio/core/opendata.py +744 -0
- rucio/core/rule.py +63 -8
- rucio/core/transfer.py +1 -1
- rucio/daemons/common.py +1 -1
- rucio/daemons/conveyor/finisher.py +2 -2
- rucio/daemons/conveyor/poller.py +2 -2
- rucio/daemons/conveyor/preparer.py +1 -1
- rucio/daemons/conveyor/submitter.py +2 -2
- rucio/daemons/conveyor/throttler.py +1 -1
- rucio/daemons/hermes/hermes.py +26 -17
- rucio/db/sqla/constants.py +6 -0
- rucio/db/sqla/migrate_repo/versions/a62db546a1f1_opendata_initial_model.py +85 -0
- rucio/db/sqla/models.py +69 -0
- rucio/db/sqla/session.py +8 -1
- rucio/db/sqla/util.py +2 -2
- rucio/gateway/dirac.py +1 -1
- rucio/gateway/opendata.py +190 -0
- rucio/gateway/subscription.py +5 -3
- rucio/rse/protocols/protocol.py +9 -5
- rucio/rse/translation.py +17 -6
- rucio/tests/common.py +64 -12
- rucio/transfertool/fts3.py +1 -0
- rucio/transfertool/fts3_plugins.py +6 -1
- rucio/vcsversion.py +4 -4
- rucio/web/rest/flaskapi/v1/auth.py +11 -2
- rucio/web/rest/flaskapi/v1/common.py +34 -14
- rucio/web/rest/flaskapi/v1/config.py +1 -1
- rucio/web/rest/flaskapi/v1/dids.py +447 -160
- rucio/web/rest/flaskapi/v1/heartbeats.py +1 -1
- rucio/web/rest/flaskapi/v1/identities.py +1 -1
- rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +1 -1
- rucio/web/rest/flaskapi/v1/locks.py +1 -1
- rucio/web/rest/flaskapi/v1/main.py +3 -7
- rucio/web/rest/flaskapi/v1/meta_conventions.py +1 -16
- rucio/web/rest/flaskapi/v1/nongrid_traces.py +1 -1
- rucio/web/rest/flaskapi/v1/opendata.py +391 -0
- rucio/web/rest/flaskapi/v1/opendata_public.py +146 -0
- rucio/web/rest/flaskapi/v1/requests.py +1 -1
- rucio/web/rest/flaskapi/v1/rses.py +1 -1
- rucio/web/rest/flaskapi/v1/rules.py +1 -1
- rucio/web/rest/flaskapi/v1/scopes.py +1 -1
- rucio/web/rest/flaskapi/v1/subscriptions.py +6 -9
- rucio/web/rest/flaskapi/v1/traces.py +1 -1
- rucio/web/rest/flaskapi/v1/vos.py +1 -1
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/etc/alembic.ini.template +1 -1
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/etc/alembic_offline.ini.template +1 -1
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/etc/rucio.cfg.template +2 -2
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/etc/rucio_multi_vo.cfg.template +3 -3
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/requirements.server.txt +6 -3
- rucio-38.0.0.data/data/rucio/tools/reset_database.py +87 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio +2 -1
- {rucio-37.7.0.dist-info → rucio-38.0.0.dist-info}/METADATA +37 -36
- {rucio-37.7.0.dist-info → rucio-38.0.0.dist-info}/RECORD +129 -123
- {rucio-37.7.0.dist-info → rucio-38.0.0.dist-info}/licenses/AUTHORS.rst +1 -0
- rucio/client/fileclient.py +0 -57
- rucio-37.7.0.data/data/rucio/tools/reset_database.py +0 -40
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/etc/globus-config.yml.template +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/etc/ldap.cfg.template +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/etc/mail_templates/rule_approval_request.tmpl +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/etc/mail_templates/rule_approved_user.tmpl +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/etc/mail_templates/rule_denied_user.tmpl +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/etc/rse-accounts.cfg.template +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/etc/rucio.cfg.atlas.client.template +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/tools/bootstrap.py +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/data/rucio/tools/merge_rucio_configs.py +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-abacus-account +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-abacus-collection-replica +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-abacus-rse +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-admin +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-atropos +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-auditor +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-automatix +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-bb8 +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-cache-client +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-cache-consumer +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-conveyor-finisher +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-conveyor-poller +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-conveyor-preparer +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-conveyor-receiver +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-conveyor-stager +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-conveyor-submitter +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-conveyor-throttler +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-dark-reaper +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-dumper +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-follower +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-hermes +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-judge-cleaner +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-judge-evaluator +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-judge-injector +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-judge-repairer +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-kronos +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-minos +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-minos-temporary-expiration +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-necromancer +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-oauth-manager +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-reaper +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-replica-recoverer +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-rse-decommissioner +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-storage-consistency-actions +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-transmogrifier +0 -0
- {rucio-37.7.0.data → rucio-38.0.0.data}/scripts/rucio-undertaker +0 -0
- {rucio-37.7.0.dist-info → rucio-38.0.0.dist-info}/WHEEL +0 -0
- {rucio-37.7.0.dist-info → rucio-38.0.0.dist-info}/licenses/LICENSE +0 -0
- {rucio-37.7.0.dist-info → rucio-38.0.0.dist-info}/top_level.txt +0 -0
|
@@ -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)
|
rucio/gateway/subscription.py
CHANGED
|
@@ -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
|
|
rucio/rse/protocols/protocol.py
CHANGED
|
@@ -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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
#
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
+
algorithm_callable = None
|
|
263
|
+
if algorithm == 'default' or algorithm == RSEDeterministicTranslation._DEFAULT_LFN2PFN:
|
|
255
264
|
algorithm = RSEDeterministicTranslation._DEFAULT_LFN2PFN
|
|
256
|
-
|
|
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
|
|
rucio/tests/common.py
CHANGED
|
@@ -50,23 +50,75 @@ skip_outside_gh_actions = pytest.mark.skipif(os.getenv("GITHUB_ACTIONS") != "tru
|
|
|
50
50
|
reason="Skipping tests outside GitHub Actions")
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
def is_influxdb_available(
|
|
54
|
-
|
|
53
|
+
def is_influxdb_available(
|
|
54
|
+
url: str = "http://influxdb:8086",
|
|
55
|
+
timeout: float = 2.0
|
|
56
|
+
) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Return True when InfluxDB is up and ready for queries, otherwise False.
|
|
59
|
+
|
|
60
|
+
Strategy:
|
|
61
|
+
1. Try /health → 200 + JSON["status"] == "pass"
|
|
62
|
+
2. Fallback to /ping → 204
|
|
63
|
+
"""
|
|
64
|
+
print(f"Checking InfluxDB availability at {url}")
|
|
65
|
+
try:
|
|
66
|
+
r = requests.get(f"{url}/health", timeout=timeout)
|
|
67
|
+
print(f"InfluxDB /health responded with {r.status_code} and body: {r.text}", r.status_code, r.text)
|
|
68
|
+
if r.status_code == 200 and r.json().get("status") == "pass":
|
|
69
|
+
return True
|
|
70
|
+
print(f"InfluxDB is not running healthy at {url}.")
|
|
71
|
+
return False
|
|
72
|
+
except requests.RequestException as e:
|
|
73
|
+
# /health failed or is not available (pre‑1.8)
|
|
74
|
+
print(f"Failed to query InfluxDB /health at {url}: {e}")
|
|
75
|
+
|
|
55
76
|
try:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
print(f"Falling back to /ping for InfluxDB at {url}")
|
|
78
|
+
r = requests.get(f"{url}/ping", timeout=timeout)
|
|
79
|
+
print(f"InfluxDB /ping responded with {r.status_code}")
|
|
80
|
+
return r.status_code == 204
|
|
81
|
+
except requests.RequestException as e:
|
|
82
|
+
print(f"InfluxDB is not reachable at {url}: {e}")
|
|
60
83
|
return False
|
|
61
84
|
|
|
62
85
|
|
|
63
|
-
def is_elasticsearch_available(
|
|
64
|
-
|
|
86
|
+
def is_elasticsearch_available(
|
|
87
|
+
url: str = "http://elasticsearch:9200",
|
|
88
|
+
timeout: float = 2.0,
|
|
89
|
+
min_status: str = 'green',
|
|
90
|
+
) -> bool:
|
|
91
|
+
"""
|
|
92
|
+
Return True when the Elasticsearch node is reachable **and**
|
|
93
|
+
cluster health is at least `min_status` ('red'<'yellow'<'green').
|
|
94
|
+
|
|
95
|
+
1. GET /_cluster/health → 200 + JSON["status"] meets threshold
|
|
96
|
+
2. Fallback: HEAD / → 200 (port open but health unknown)
|
|
97
|
+
"""
|
|
98
|
+
_status_level = {"red": 1, "yellow": 2, "green": 3}
|
|
99
|
+
|
|
100
|
+
print(f"Checking Elasticsearch availability at {url}")
|
|
101
|
+
try:
|
|
102
|
+
r = requests.get(f"{url}/_cluster/health", timeout=timeout)
|
|
103
|
+
print(f"Elasticsearch /_cluster/health responded with {r.status_code} and body: {r.text}")
|
|
104
|
+
if r.status_code == 200:
|
|
105
|
+
status = r.json().get("status")
|
|
106
|
+
if status and _status_level[status] >= _status_level[min_status]:
|
|
107
|
+
return True
|
|
108
|
+
print(f"Elasticsearch health is {status!r}, below threshold {min_status!r}.")
|
|
109
|
+
return False
|
|
110
|
+
except requests.RequestException as e:
|
|
111
|
+
# Either not reachable or /_cluster/health not yet available
|
|
112
|
+
print(f"Failed to query Elasticsearch /_cluster/health at {url}: {e}")
|
|
113
|
+
|
|
114
|
+
# Very old nodes or boot‑strapping clusters: fall back to a simple HEAD /
|
|
65
115
|
try:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
116
|
+
print(f"Falling back to HEAD request for Elasticsearch at {url}")
|
|
117
|
+
r = requests.head(url, timeout=timeout)
|
|
118
|
+
print(f"Elasticsearch HEAD / responded with {r.status_code}")
|
|
119
|
+
return r.status_code == 200
|
|
120
|
+
except requests.RequestException as e:
|
|
121
|
+
print(f"Elasticsearch is not reachable at {url}: {e}")
|
|
70
122
|
return False
|
|
71
123
|
|
|
72
124
|
|
rucio/transfertool/fts3.py
CHANGED
|
@@ -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
|
|
|
@@ -17,6 +17,7 @@ import sys
|
|
|
17
17
|
from typing import TYPE_CHECKING, Any, Optional, TypeVar
|
|
18
18
|
|
|
19
19
|
from rucio.common.config import config_get_int
|
|
20
|
+
from rucio.common.constants import DEFAULT_VO
|
|
20
21
|
from rucio.common.exception import InvalidRequest
|
|
21
22
|
from rucio.common.plugins import PolicyPackageAlgorithms
|
|
22
23
|
|
|
@@ -85,7 +86,11 @@ class FTS3TapeMetadataPlugin(PolicyPackageAlgorithms):
|
|
|
85
86
|
"""
|
|
86
87
|
return {"collocation_hints": collocation_func(**hints)}
|
|
87
88
|
|
|
88
|
-
def _default(self,
|
|
89
|
+
def _default(self, hint_dict: dict[str, Any]) -> dict:
|
|
90
|
+
vo = hint_dict['vo'] if 'vo' in hint_dict else DEFAULT_VO
|
|
91
|
+
default_algorithm = self._get_default_algorithm(self.ALGORITHM_NAME, vo=vo)
|
|
92
|
+
if default_algorithm is not None:
|
|
93
|
+
return default_algorithm(hint_dict)
|
|
89
94
|
return {}
|
|
90
95
|
|
|
91
96
|
def _verify_in_format(self, hint_dict: dict[str, Any]) -> None:
|
rucio/vcsversion.py
CHANGED
|
@@ -4,8 +4,8 @@ This file is automatically generated; Do not edit it. :)
|
|
|
4
4
|
'''
|
|
5
5
|
VERSION_INFO = {
|
|
6
6
|
'final': True,
|
|
7
|
-
'version': '
|
|
8
|
-
'branch_nick': '
|
|
9
|
-
'revision_id': '
|
|
10
|
-
'revno':
|
|
7
|
+
'version': '38.0.0',
|
|
8
|
+
'branch_nick': 'master',
|
|
9
|
+
'revision_id': 'f355202c14bffc0dff7c4f83c86c90fcce914472',
|
|
10
|
+
'revno': 13928
|
|
11
11
|
}
|
|
@@ -19,6 +19,7 @@ from typing import TYPE_CHECKING
|
|
|
19
19
|
from urllib.parse import urlparse
|
|
20
20
|
|
|
21
21
|
from flask import Blueprint, Flask, Response, redirect, render_template, request
|
|
22
|
+
from jinja2.exceptions import TemplateNotFound
|
|
22
23
|
from werkzeug.datastructures import Headers
|
|
23
24
|
|
|
24
25
|
from rucio.common.config import config_get
|
|
@@ -579,9 +580,17 @@ class CodeOIDC(ErrorHandlingMethodView):
|
|
|
579
580
|
return render_template('auth_crash.html', crashtype='no_result'), 401, headers
|
|
580
581
|
|
|
581
582
|
if 'fetchcode' in result:
|
|
582
|
-
|
|
583
|
+
try:
|
|
584
|
+
return render_template('auth_granted.html', authcode=result['fetchcode']), 200, headers
|
|
585
|
+
except TemplateNotFound:
|
|
586
|
+
headers.set('Content-Type', 'text/plain')
|
|
587
|
+
return 'auth_granted.html missing', 500, headers
|
|
583
588
|
elif 'polling' in result and result['polling'] is True:
|
|
584
|
-
|
|
589
|
+
try:
|
|
590
|
+
return render_template('auth_granted.html', authcode='allok'), 200, headers
|
|
591
|
+
except TemplateNotFound:
|
|
592
|
+
headers.set('Content-Type', 'text/plain')
|
|
593
|
+
return 'auth_granted.html missing', 500, headers
|
|
585
594
|
else:
|
|
586
595
|
headers.extend(error_headers('InvalidRequest', 'Cannot recognize and process your request'))
|
|
587
596
|
return render_template('auth_crash.html', crashtype='bad_request'), 400, headers
|
|
@@ -20,11 +20,12 @@ import re
|
|
|
20
20
|
from configparser import NoOptionError, NoSectionError
|
|
21
21
|
from functools import wraps
|
|
22
22
|
from time import time
|
|
23
|
-
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union
|
|
23
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union, cast
|
|
24
24
|
from urllib.parse import unquote_plus
|
|
25
25
|
|
|
26
26
|
import flask
|
|
27
27
|
from flask.views import MethodView
|
|
28
|
+
from typing_extensions import ParamSpec
|
|
28
29
|
from werkzeug.datastructures import Headers
|
|
29
30
|
from werkzeug.exceptions import HTTPException
|
|
30
31
|
from werkzeug.wrappers import Request, Response
|
|
@@ -183,25 +184,44 @@ def response_headers(response: ResponseTypeVar) -> ResponseTypeVar:
|
|
|
183
184
|
return response
|
|
184
185
|
|
|
185
186
|
|
|
186
|
-
|
|
187
|
-
|
|
187
|
+
P = ParamSpec('P')
|
|
188
|
+
R = TypeVar('R')
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def check_accept_header_wrapper_flask(
|
|
192
|
+
supported_content_types: 'Iterable[str]'
|
|
193
|
+
) -> 'Callable[[Callable[P, R]], Callable[P, R]]':
|
|
194
|
+
"""Decorator that refuses requests with an unsupported *Accept* header."""
|
|
195
|
+
|
|
196
|
+
def wrapper(
|
|
197
|
+
f: 'Callable[P, R]'
|
|
198
|
+
) -> 'Callable[P, R]':
|
|
199
|
+
"""Decorate *f* with an *Accept*-header check and return the new callable."""
|
|
188
200
|
|
|
189
|
-
def wrapper(f):
|
|
190
201
|
@wraps(f)
|
|
191
|
-
def decorated(*args, **kwargs):
|
|
202
|
+
def decorated(*args: 'P.args', **kwargs: 'P.kwargs') -> 'R':
|
|
203
|
+
"""Run the header check, then delegate to *f* (or return 406)."""
|
|
204
|
+
|
|
205
|
+
# 1. no Accept header → accept everything
|
|
192
206
|
if not flask.request.accept_mimetypes.provided:
|
|
193
|
-
# accept anything, if Accept header is not provided
|
|
194
207
|
return f(*args, **kwargs)
|
|
195
208
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
209
|
+
# 2. at least one acceptable media‑type → call the view
|
|
210
|
+
if any(s in flask.request.accept_mimetypes for s in supported_content_types):
|
|
211
|
+
return f(*args, **kwargs)
|
|
199
212
|
|
|
200
|
-
# none matched
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
213
|
+
# 3. none matched → 406 response
|
|
214
|
+
return cast(
|
|
215
|
+
'R',
|
|
216
|
+
generate_http_error_flask(
|
|
217
|
+
status_code=406,
|
|
218
|
+
exc=UnsupportedRequestedContentType.__name__,
|
|
219
|
+
exc_msg=(
|
|
220
|
+
f'The requested content type '
|
|
221
|
+
f'{flask.request.environ.get("HTTP_ACCEPT")} is not supported. '
|
|
222
|
+
f'Use {supported_content_types}.'
|
|
223
|
+
),
|
|
224
|
+
),
|
|
205
225
|
)
|
|
206
226
|
|
|
207
227
|
return decorated
|
|
@@ -281,7 +281,7 @@ class OptionSet(ErrorHandlingMethodView):
|
|
|
281
281
|
return generate_http_error_flask(500, error, f"Could not set value '{value}' for section '{section}' option '{option}'")
|
|
282
282
|
|
|
283
283
|
|
|
284
|
-
def blueprint():
|
|
284
|
+
def blueprint() -> AuthenticatedBlueprint:
|
|
285
285
|
bp = AuthenticatedBlueprint('config', __name__, url_prefix='/config')
|
|
286
286
|
|
|
287
287
|
option_set_view = OptionSet.as_view('option_set')
|