rucio-clients 37.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-clients might be problematic. Click here for more details.

Files changed (104) hide show
  1. rucio/__init__.py +17 -0
  2. rucio/alembicrevision.py +15 -0
  3. rucio/cli/__init__.py +14 -0
  4. rucio/cli/account.py +216 -0
  5. rucio/cli/bin_legacy/__init__.py +13 -0
  6. rucio/cli/bin_legacy/rucio.py +2825 -0
  7. rucio/cli/bin_legacy/rucio_admin.py +2500 -0
  8. rucio/cli/command.py +272 -0
  9. rucio/cli/config.py +72 -0
  10. rucio/cli/did.py +191 -0
  11. rucio/cli/download.py +128 -0
  12. rucio/cli/lifetime_exception.py +33 -0
  13. rucio/cli/replica.py +162 -0
  14. rucio/cli/rse.py +293 -0
  15. rucio/cli/rule.py +158 -0
  16. rucio/cli/scope.py +40 -0
  17. rucio/cli/subscription.py +73 -0
  18. rucio/cli/upload.py +60 -0
  19. rucio/cli/utils.py +226 -0
  20. rucio/client/__init__.py +15 -0
  21. rucio/client/accountclient.py +432 -0
  22. rucio/client/accountlimitclient.py +183 -0
  23. rucio/client/baseclient.py +983 -0
  24. rucio/client/client.py +120 -0
  25. rucio/client/configclient.py +126 -0
  26. rucio/client/credentialclient.py +59 -0
  27. rucio/client/didclient.py +868 -0
  28. rucio/client/diracclient.py +56 -0
  29. rucio/client/downloadclient.py +1783 -0
  30. rucio/client/exportclient.py +44 -0
  31. rucio/client/fileclient.py +50 -0
  32. rucio/client/importclient.py +42 -0
  33. rucio/client/lifetimeclient.py +90 -0
  34. rucio/client/lockclient.py +109 -0
  35. rucio/client/metaconventionsclient.py +140 -0
  36. rucio/client/pingclient.py +44 -0
  37. rucio/client/replicaclient.py +452 -0
  38. rucio/client/requestclient.py +125 -0
  39. rucio/client/richclient.py +317 -0
  40. rucio/client/rseclient.py +746 -0
  41. rucio/client/ruleclient.py +294 -0
  42. rucio/client/scopeclient.py +90 -0
  43. rucio/client/subscriptionclient.py +173 -0
  44. rucio/client/touchclient.py +82 -0
  45. rucio/client/uploadclient.py +969 -0
  46. rucio/common/__init__.py +13 -0
  47. rucio/common/bittorrent.py +234 -0
  48. rucio/common/cache.py +111 -0
  49. rucio/common/checksum.py +168 -0
  50. rucio/common/client.py +122 -0
  51. rucio/common/config.py +788 -0
  52. rucio/common/constants.py +217 -0
  53. rucio/common/constraints.py +17 -0
  54. rucio/common/didtype.py +237 -0
  55. rucio/common/exception.py +1208 -0
  56. rucio/common/extra.py +31 -0
  57. rucio/common/logging.py +420 -0
  58. rucio/common/pcache.py +1409 -0
  59. rucio/common/plugins.py +185 -0
  60. rucio/common/policy.py +93 -0
  61. rucio/common/schema/__init__.py +200 -0
  62. rucio/common/schema/generic.py +416 -0
  63. rucio/common/schema/generic_multi_vo.py +395 -0
  64. rucio/common/stomp_utils.py +423 -0
  65. rucio/common/stopwatch.py +55 -0
  66. rucio/common/test_rucio_server.py +154 -0
  67. rucio/common/types.py +483 -0
  68. rucio/common/utils.py +1688 -0
  69. rucio/rse/__init__.py +96 -0
  70. rucio/rse/protocols/__init__.py +13 -0
  71. rucio/rse/protocols/bittorrent.py +194 -0
  72. rucio/rse/protocols/cache.py +111 -0
  73. rucio/rse/protocols/dummy.py +100 -0
  74. rucio/rse/protocols/gfal.py +708 -0
  75. rucio/rse/protocols/globus.py +243 -0
  76. rucio/rse/protocols/http_cache.py +82 -0
  77. rucio/rse/protocols/mock.py +123 -0
  78. rucio/rse/protocols/ngarc.py +209 -0
  79. rucio/rse/protocols/posix.py +250 -0
  80. rucio/rse/protocols/protocol.py +361 -0
  81. rucio/rse/protocols/rclone.py +365 -0
  82. rucio/rse/protocols/rfio.py +145 -0
  83. rucio/rse/protocols/srm.py +338 -0
  84. rucio/rse/protocols/ssh.py +414 -0
  85. rucio/rse/protocols/storm.py +195 -0
  86. rucio/rse/protocols/webdav.py +594 -0
  87. rucio/rse/protocols/xrootd.py +302 -0
  88. rucio/rse/rsemanager.py +881 -0
  89. rucio/rse/translation.py +260 -0
  90. rucio/vcsversion.py +11 -0
  91. rucio/version.py +45 -0
  92. rucio_clients-37.0.0rc1.data/data/etc/rse-accounts.cfg.template +25 -0
  93. rucio_clients-37.0.0rc1.data/data/etc/rucio.cfg.atlas.client.template +43 -0
  94. rucio_clients-37.0.0rc1.data/data/etc/rucio.cfg.template +241 -0
  95. rucio_clients-37.0.0rc1.data/data/requirements.client.txt +19 -0
  96. rucio_clients-37.0.0rc1.data/data/rucio_client/merge_rucio_configs.py +144 -0
  97. rucio_clients-37.0.0rc1.data/scripts/rucio +133 -0
  98. rucio_clients-37.0.0rc1.data/scripts/rucio-admin +97 -0
  99. rucio_clients-37.0.0rc1.dist-info/METADATA +54 -0
  100. rucio_clients-37.0.0rc1.dist-info/RECORD +104 -0
  101. rucio_clients-37.0.0rc1.dist-info/WHEEL +5 -0
  102. rucio_clients-37.0.0rc1.dist-info/licenses/AUTHORS.rst +100 -0
  103. rucio_clients-37.0.0rc1.dist-info/licenses/LICENSE +201 -0
  104. rucio_clients-37.0.0rc1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,881 @@
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 copy
16
+ import logging
17
+ import random
18
+ from time import sleep
19
+ from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast
20
+ from urllib.parse import urlparse
21
+
22
+ from rucio.common import constants, exception, types, utils
23
+ from rucio.common.checksum import GLOBALLY_SUPPORTED_CHECKSUMS
24
+ from rucio.common.config import config_get_int
25
+ from rucio.common.constraints import STRING_TYPES
26
+ from rucio.common.logging import formatted_logger
27
+ from rucio.common.utils import make_valid_did
28
+
29
+ if TYPE_CHECKING:
30
+ from collections.abc import Callable
31
+
32
+ from sqlalchemy.orm import Session
33
+
34
+ from rucio.rse.protocols.protocol import RSEProtocol
35
+
36
+
37
+ def get_scope_protocol(vo: str = 'def') -> 'Callable':
38
+ """
39
+ Returns the callable protocol to translate the pfn to a name/scope pair
40
+
41
+ :returns:
42
+ Callable: Scope Parser function
43
+ """
44
+ from rucio.rse.translation import RSEDeterministicScopeTranslation
45
+ translation = RSEDeterministicScopeTranslation(vo=vo)
46
+ return translation.parser
47
+
48
+
49
+ def get_rse_info(
50
+ rse: Optional[str] = None,
51
+ vo: str = 'def',
52
+ rse_id: Optional[str] = None,
53
+ session: Optional["Session"] = None
54
+ ) -> types.RSESettingsDict:
55
+ """
56
+ Returns all protocol related RSE attributes.
57
+ Call with either rse and vo, or (in server mode) rse_id
58
+
59
+ :param rse: Name of the requested RSE
60
+ :param vo: The VO for the RSE.
61
+ :param rse_id: The id of the rse (use in server mode to avoid db calls)
62
+ :param session: The eventual database session.
63
+
64
+ :returns: a dict object with the following attributes:
65
+ id ... an internal identifier
66
+ rse ... the name of the RSE as string
67
+ type ... the storage type odf the RSE e.g. DISK
68
+ volatile ... boolean indicating if the RSE is volatile
69
+ verify_checksum ... boolean indicating whether RSE supports requests for checksums
70
+ deterministic ... boolean indicating of the naming of the files follows the defined determinism
71
+ domain ... indicating the domain that should be assumed for transfers. Values are 'ALL', 'LAN', or 'WAN'
72
+ protocols ... all supported protocol in form of a list of dict objects with the following structure
73
+ - scheme ... protocol scheme e.g. http, srm, ...
74
+ - hostname ... hostname of the site
75
+ - prefix ... path to the folder where the files are stored
76
+ - port ... port used for this protocol
77
+ - impl ... naming the python class of the protocol implementation
78
+ - extended_attributes ... additional information for the protocol
79
+ - domains ... a dict naming each domain and the priority of the protocol for each operation (lower is better, zero is not supported)
80
+
81
+ :raises RSENotFound: if the provided RSE could not be found in the database.
82
+ """
83
+ # __request_rse_info will be assigned when the module is loaded as it depends on the rucio environment (server or client)
84
+ # __request_rse_info, rse_region are defined in /rucio/rse/__init__.py
85
+ key = '{}:{}'.format(rse, vo) if rse_id is None else str(rse_id)
86
+ key = 'rse_info_%s' % (key)
87
+ rse_info = RSE_REGION.get(key) # NOQA pylint: disable=undefined-variable
88
+ if not rse_info: # no cached entry found
89
+ rse_info = __request_rse_info(str(rse), vo=vo, rse_id=rse_id, session=session) # NOQA pylint: disable=undefined-variable
90
+ RSE_REGION.set(key, rse_info) # NOQA pylint: disable=undefined-variable
91
+ return rse_info
92
+
93
+
94
+ def _get_possible_protocols(
95
+ rse_settings: types.RSESettingsDict,
96
+ operation: str,
97
+ scheme: Optional[Union[list[str], str]] = None,
98
+ domain: Optional[str] = None,
99
+ impl: Optional[str] = None
100
+ ) -> list[types.RSEProtocolDict]:
101
+ """
102
+ Filter the list of available protocols or provided by the supported ones.
103
+
104
+ :param rse_settings: The rse settings.
105
+ :param operation: The operation (write, read).
106
+ :param scheme: Optional filter if no specific protocol is defined in
107
+ rse_setting for the provided operation.
108
+ :param domain: Optional domain (lan/wan), if not specified, both will be returned
109
+ :returns: The list of possible protocols.
110
+ """
111
+ operation = operation.lower()
112
+ candidates = rse_settings['protocols']
113
+
114
+ # convert scheme to list, if given as string
115
+ if scheme and not isinstance(scheme, list):
116
+ scheme = scheme.split(',')
117
+
118
+ tbr = []
119
+ for protocol in candidates:
120
+ # Check if impl given and filter if so
121
+ if impl and protocol['impl'] != impl:
122
+ tbr.append(protocol)
123
+ continue
124
+
125
+ # Check if scheme given and filter if so
126
+ if scheme and protocol['scheme'] not in scheme:
127
+ tbr.append(protocol)
128
+ continue
129
+
130
+ filtered = True
131
+
132
+ if not domain:
133
+ for d in list(protocol['domains'].keys()):
134
+ if protocol['domains'][d][operation]:
135
+ filtered = False
136
+ else:
137
+ if protocol['domains'].get(domain, {operation: None}).get(operation):
138
+ filtered = False
139
+
140
+ if filtered:
141
+ tbr.append(protocol)
142
+
143
+ if len(candidates) <= len(tbr):
144
+ raise exception.RSEProtocolNotSupported('No protocol for provided settings'
145
+ ' found : %s.' % str(rse_settings))
146
+
147
+ return [c for c in candidates if c not in tbr]
148
+
149
+
150
+ def get_protocols_ordered(
151
+ rse_settings: types.RSESettingsDict,
152
+ operation: constants.RSE_ALL_SUPPORTED_PROTOCOL_OPERATIONS_LITERAL,
153
+ scheme: Optional[Union[list[str], str]] = None,
154
+ domain: str = 'wan',
155
+ impl: Optional[str] = None
156
+ ) -> list[types.RSEProtocolDict]:
157
+ if operation not in constants.RSE_ALL_SUPPORTED_PROTOCOL_OPERATIONS:
158
+ raise exception.RSEOperationNotSupported('Operation %s is not supported' % operation)
159
+
160
+ if domain and domain not in utils.rse_supported_protocol_domains():
161
+ raise exception.RSEProtocolDomainNotSupported('Domain %s not supported' % domain)
162
+
163
+ candidates = _get_possible_protocols(rse_settings, operation, scheme, domain, impl)
164
+ candidates.sort(key=lambda k: k['domains'][domain][operation])
165
+ return candidates
166
+
167
+
168
+ def select_protocol(
169
+ rse_settings: types.RSESettingsDict,
170
+ operation: constants.RSE_ALL_SUPPORTED_PROTOCOL_OPERATIONS_LITERAL,
171
+ scheme: Optional[str] = None,
172
+ domain: str = 'wan'
173
+ ) -> types.RSEProtocolDict:
174
+ if operation not in constants.RSE_ALL_SUPPORTED_PROTOCOL_OPERATIONS:
175
+ raise exception.RSEOperationNotSupported('Operation %s is not supported' % operation)
176
+
177
+ if domain and domain not in utils.rse_supported_protocol_domains():
178
+ raise exception.RSEProtocolDomainNotSupported('Domain %s not supported' % domain)
179
+
180
+ candidates = _get_possible_protocols(rse_settings, operation, scheme, domain)
181
+ # Shuffle candidates to load-balance over equal sources
182
+ random.shuffle(candidates)
183
+ return min(candidates, key=lambda k: k['domains'][domain][operation])
184
+
185
+
186
+ def create_protocol(
187
+ rse_settings: types.RSESettingsDict,
188
+ operation: str,
189
+ scheme: Optional[str] = None,
190
+ domain: str = 'wan',
191
+ auth_token: Optional[str] = None,
192
+ protocol_attr: Optional[types.RSEProtocolDict] = None,
193
+ logger: types.LoggerFunction = logging.log,
194
+ impl: Optional[str] = None
195
+ ) -> "RSEProtocol":
196
+ """
197
+ Instantiates the protocol defined for the given operation.
198
+
199
+ :param rse_settings: RSE attributes
200
+ :param operation: Intended operation for this protocol
201
+ :param scheme: Optional filter if no specific protocol is defined in rse_setting for the provided operation
202
+ :param domain: Optional specification of the domain
203
+ :param auth_token: Optionally passing JSON Web Token (OIDC) string for authentication
204
+ :param protocol_attr: Optionally passing the full protocol availability information to correctly select WAN/LAN
205
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
206
+ :returns: An instance of the requested protocol
207
+ """
208
+
209
+ # Verify feasibility of Protocol
210
+ operation = operation.lower()
211
+ if operation not in constants.RSE_ALL_SUPPORTED_PROTOCOL_OPERATIONS:
212
+ raise exception.RSEOperationNotSupported('Operation %s is not supported' % operation)
213
+
214
+ operation = cast("constants.RSE_ALL_SUPPORTED_PROTOCOL_OPERATIONS_LITERAL", operation)
215
+
216
+ if domain and domain not in utils.rse_supported_protocol_domains():
217
+ raise exception.RSEProtocolDomainNotSupported('Domain %s not supported' % domain)
218
+
219
+ if impl:
220
+ candidate = _get_possible_protocols(rse_settings, operation, scheme, domain, impl=impl)
221
+ if len(candidate) == 0:
222
+ raise exception.RSEProtocolNotSupported('Protocol implementation %s operation %s on domain %s not supported' % (impl, operation, domain))
223
+ protocol_attr = candidate[0]
224
+ elif not protocol_attr:
225
+ protocol_attr = select_protocol(rse_settings, operation, scheme, domain)
226
+ else:
227
+ candidates = _get_possible_protocols(rse_settings, operation, scheme, domain)
228
+ if protocol_attr not in candidates:
229
+ raise exception.RSEProtocolNotSupported('Protocol %s operation %s on domain %s not supported' % (protocol_attr, operation, domain))
230
+
231
+ # Instantiate protocol
232
+ comp = protocol_attr['impl'].split('.')
233
+ prefix = '.'.join(comp[-2:]) + ': '
234
+ logger = formatted_logger(logger, prefix + "%s")
235
+ mod = __import__('.'.join(comp[:-1]))
236
+ for n in comp[1:]:
237
+ try:
238
+ mod = getattr(mod, n)
239
+ except AttributeError as e:
240
+ logger(logging.DEBUG, 'Protocol implementations not supported.')
241
+ raise exception.RucioException(str(e)) # TODO: provide proper rucio exception
242
+ protocol_attr['auth_token'] = auth_token
243
+ protocol = mod(protocol_attr, rse_settings, logger=logger)
244
+ return protocol
245
+
246
+
247
+ def lfns2pfns(
248
+ rse_settings: types.RSESettingsDict,
249
+ lfns: Union[list[types.LFNDict], types.LFNDict],
250
+ operation: str = 'write',
251
+ scheme: Optional[str] = None,
252
+ domain: str = 'wan',
253
+ auth_token: Optional[str] = None,
254
+ logger: types.LoggerFunction = logging.log,
255
+ impl: Optional[str] = None
256
+ ) -> dict[str, str]:
257
+ """
258
+ Convert the lfn to a pfn
259
+
260
+ :param rse_settings: RSE attributes
261
+ :param lfns: logical file names as a dict containing 'scope' and 'name' as keys. For bulk a list of dicts can be provided
262
+ :param operation: Intended operation for this protocol
263
+ :param scheme: Optional filter if no specific protocol is defined in rse_setting for the provided operation
264
+ :param domain: Optional specification of the domain
265
+ :param auth_token: Optionally passing JSON Web Token (OIDC) string for authentication
266
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
267
+
268
+ :returns: a dict with scope:name as key and the PFN as value
269
+
270
+ """
271
+ return create_protocol(rse_settings, operation, scheme, domain, auth_token=auth_token, logger=logger, impl=impl).lfns2pfns(lfns)
272
+
273
+
274
+ def parse_pfns(
275
+ rse_settings: types.RSESettingsDict,
276
+ pfns: list[str],
277
+ operation: str = 'read',
278
+ domain: str = 'wan',
279
+ auth_token: Optional[str] = None
280
+ ) -> dict[str, dict[str, str]]:
281
+ """
282
+ Checks if a PFN is feasible for a given RSE. If so it splits the pfn in its various components.
283
+
284
+ :param rse_settings: RSE attributes
285
+ :param pfns: list of PFNs
286
+ :param operation: Intended operation for this protocol
287
+ :param domain: Optional specification of the domain
288
+ :param auth_token: Optionally passing JSON Web Token (OIDC) string for authentication
289
+
290
+ :returns: A dict with the parts known by the selected protocol e.g. scheme, hostname, prefix, path, name
291
+
292
+ :raises RSEFileNameNotSupported: if provided PFN is not supported by the RSE/protocol
293
+ :raises RSENotFound: if the referred storage is not found i the repository (rse_id)
294
+ :raises InvalidObject: If the properties parameter doesn't include scheme, hostname, and port as keys
295
+ :raises RSEOperationNotSupported: If no matching protocol was found for the requested operation
296
+ """
297
+ if len(set([urlparse(pfn).scheme for pfn in pfns])) != 1:
298
+ raise ValueError('All PFNs must provide the same protocol scheme')
299
+ return create_protocol(rse_settings, operation, urlparse(pfns[0]).scheme, domain, auth_token=auth_token).parse_pfns(pfns)
300
+
301
+
302
+ def exists(
303
+ rse_settings: types.RSESettingsDict,
304
+ files: Union[list[dict[str, str]], dict[str, str]],
305
+ domain: str = 'wan',
306
+ scheme: Optional[str] = None,
307
+ impl: Optional[str] = None,
308
+ auth_token: Optional[str] = None,
309
+ vo: str = 'def',
310
+ logger: types.LoggerFunction = logging.log
311
+ ) -> Union[bool, list[Union[bool, dict[dict[str, str], bool]]]]:
312
+ """
313
+ Checks if a file is present at the connected storage.
314
+ Providing a list indicates the bulk mode.
315
+
316
+ :param rse_settings: RSE attributes
317
+ :param files: a single dict or a list with dicts containing 'scope' and 'name'
318
+ if LFNs are used and only 'name' if PFNs are used.
319
+ E.g. {'name': '2_rse_remote_get.raw', 'scope': 'user.jdoe'}, {'name': 'user/jdoe/5a/98/3_rse_remote_get.raw'}
320
+ :param domain: The network domain, either 'wan' (default) or 'lan'
321
+ :param auth_token: Optionally passing JSON Web Token (OIDC) string for authentication
322
+ :param vo: The VO for the RSE
323
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
324
+
325
+ :returns: True/False for a single file or a dict object with 'scope:name' for LFNs or 'name' for PFNs as keys and True or the exception as value for each file in bulk mode
326
+
327
+ :raises RSENotConnected: no connection to a specific storage has been established
328
+ """
329
+
330
+ protocol = create_protocol(rse_settings, 'read', scheme=scheme, impl=impl, domain=domain, auth_token=auth_token, logger=logger)
331
+ protocol.connect()
332
+
333
+ from rucio.rse.protocols.protocol import RSEProtocol # Placed it here to avoid possible circular imports
334
+ # Check if 'exists' is truly overridden on the read protocol
335
+ if not utils.is_method_overridden(protocol, RSEProtocol, 'exists'):
336
+ # If not overridden, optionally fall back to a write protocol
337
+ protocol = create_protocol(rse_settings, 'write', scheme=scheme, domain=domain, auth_token=auth_token, logger=logger)
338
+ protocol.connect()
339
+
340
+ ret = {}
341
+ gs = True # gs represents the global status which indicates if every operation worked in bulk mode
342
+
343
+ if not isinstance(files, list):
344
+ files = [files]
345
+ for f in files:
346
+ exists = None
347
+ if isinstance(f, STRING_TYPES):
348
+ exists = protocol.exists(f)
349
+ ret[f] = exists
350
+ elif 'scope' in f: # a LFN is provided
351
+ f = cast("types.LFNDict", f)
352
+ pfn = list(protocol.lfns2pfns(f).values())[0]
353
+ if isinstance(pfn, exception.RucioException):
354
+ raise pfn
355
+ logger(logging.DEBUG, 'Checking if %s exists', pfn)
356
+ # deal with URL signing if required
357
+ if rse_settings['sign_url'] is not None and pfn[:5] == 'https':
358
+ pfn = __get_signed_url(rse_settings['rse'], rse_settings['sign_url'], 'read', pfn, vo) # NOQA pylint: disable=undefined-variable
359
+ exists = protocol.exists(pfn)
360
+ ret[f['scope'] + ':' + f['name']] = exists
361
+ else:
362
+ exists = protocol.exists(f['name'])
363
+ ret[f['name']] = exists
364
+ if not exists:
365
+ gs = False
366
+
367
+ protocol.close()
368
+ if len(ret) == 1:
369
+ return next(iter(ret.values()))
370
+ return [gs, ret]
371
+
372
+
373
+ def upload(
374
+ rse_settings: types.RSESettingsDict,
375
+ lfns: Union[list[types.LFNDict], types.LFNDict],
376
+ domain: str = 'wan',
377
+ source_dir: Optional[str] = None,
378
+ force_pfn: Optional[str] = None,
379
+ force_scheme: Optional[str] = None,
380
+ transfer_timeout: Optional[int] = None,
381
+ delete_existing: bool = False,
382
+ sign_service: Optional[str] = None,
383
+ auth_token: Optional[str] = None,
384
+ vo: str = 'def',
385
+ logger: types.LoggerFunction = logging.log,
386
+ impl: Optional[str] = None
387
+ ) -> dict[Union[int, str], Union[bool, str, dict[str, Union[Literal[True], Exception]]]]:
388
+ """
389
+ Uploads a file to the connected storage.
390
+ Providing a list indicates the bulk mode.
391
+
392
+ :param rse_settings: RSE attributes
393
+ :param lfns: a single dict or a list with dicts containing 'scope' and 'name'.
394
+ Examples:
395
+ [
396
+ {'name': '1_rse_local_put.raw', 'scope': 'user.jdoe', 'filesize': 42, 'adler32': '87HS3J968JSNWID'},
397
+ {'name': '2_rse_local_put.raw', 'scope': 'user.jdoe', 'filesize': 4711, 'adler32': 'RSSMICETHMISBA837464F'}
398
+ ]
399
+ If the 'filename' key is present, it will be used by Rucio as the actual name of the file on disk (separate from the Rucio 'name').
400
+ :param domain: The network domain, either 'wan' (default) or 'lan'
401
+ :param source_dir: path to the local directory including the source files
402
+ :param force_pfn: use the given PFN -- can lead to dark data, use sparingly
403
+ :param force_scheme: use the given protocol scheme, overriding the protocol priority in the RSE description
404
+ :param transfer_timeout: set this timeout (in seconds) for the transfers, for protocols that support it
405
+ :param sign_service: use the given service (e.g. gcs, s3, swift) to sign the URL
406
+ :param auth_token: Optionally passing JSON Web Token (OIDC) string for authentication
407
+ :param vo: The VO for the RSE
408
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
409
+
410
+ :returns: True/False for a single file or a dict object with 'scope:name' as keys and True or the exception as value for each file in bulk mode
411
+
412
+ :raises RSENotConnected: no connection to a specific storage has been established
413
+ :raises SourceNotFound: local source file can not be found
414
+ :raises DestinationNotAccessible: remote destination directory is not accessible
415
+ :raises ServiceUnavailable: for any other reason
416
+ """
417
+
418
+ ret = {}
419
+ gs = True # gs represents the global status which indicates if every operation worked in bulk mode
420
+
421
+ protocol = create_protocol(rse_settings, 'write', scheme=force_scheme, domain=domain, auth_token=auth_token, logger=logger, impl=impl)
422
+ protocol.connect()
423
+ protocol_delete = create_protocol(rse_settings, 'delete', domain=domain, auth_token=auth_token, logger=logger, impl=impl)
424
+ protocol_delete.connect()
425
+
426
+ if not isinstance(lfns, list):
427
+ lfns = [lfns]
428
+ for lfn in lfns:
429
+ base_name = lfn.get('filename', lfn['name'])
430
+ name = lfn.get('name', base_name)
431
+ scope = lfn['scope']
432
+ if 'adler32' not in lfn and 'md5' not in lfn:
433
+ gs = False
434
+ ret['%s:%s' % (scope, name)] = exception.RucioException('Missing checksum for file %s:%s' % (lfn['scope'], name))
435
+ continue
436
+ if 'filesize' not in lfn:
437
+ gs = False
438
+ ret['%s:%s' % (scope, name)] = exception.RucioException('Missing filesize for file %s:%s' % (lfn['scope'], name))
439
+ continue
440
+ if force_pfn:
441
+ pfn = force_pfn
442
+ readpfn = force_pfn
443
+ else:
444
+ pfn = list(protocol.lfns2pfns(make_valid_did(lfn)).values())[0]
445
+ if isinstance(pfn, exception.RucioException):
446
+ raise pfn
447
+ readpfn = pfn
448
+ if sign_service is not None:
449
+ # need a separate signed URL for read operations (exists and stat)
450
+ readpfn = __get_signed_url(rse_settings['rse'], sign_service, 'read', pfn, vo) # NOQA pylint: disable=undefined-variable
451
+ pfn = __get_signed_url(rse_settings['rse'], sign_service, 'write', pfn, vo) # NOQA pylint: disable=undefined-variable
452
+
453
+ # First check if renaming operation is supported
454
+ if protocol.renaming:
455
+
456
+ # Check if file replica is already on the storage system
457
+ if protocol.overwrite is False and delete_existing is False and protocol.exists(pfn):
458
+ ret['%s:%s' % (scope, name)] = exception.FileReplicaAlreadyExists('File %s in scope %s already exists on storage as PFN %s' % (name, scope, pfn))
459
+ gs = False
460
+ else:
461
+ if protocol.exists('%s.rucio.upload' % pfn): # Check for left over of previous unsuccessful attempts
462
+ try:
463
+ logger(logging.DEBUG, 'Deleting %s.rucio.upload', pfn)
464
+ protocol_delete.delete('%s.rucio.upload' % list(protocol_delete.lfns2pfns(make_valid_did(lfn)).values())[0])
465
+ except Exception as e:
466
+ ret['%s:%s' % (scope, name)] = exception.RSEOperationNotSupported('Unable to remove temporary file %s.rucio.upload: %s' % (pfn, str(e)))
467
+ gs = False
468
+ continue
469
+
470
+ if delete_existing:
471
+ if protocol.exists('%s' % pfn): # Check for previous completed uploads that have to be removed before upload
472
+ try:
473
+ logger(logging.DEBUG, 'Deleting %s', pfn)
474
+ protocol_delete.delete('%s' % list(protocol_delete.lfns2pfns(make_valid_did(lfn)).values())[0])
475
+ except Exception as e:
476
+ ret['%s:%s' % (scope, name)] = exception.RSEOperationNotSupported('Unable to remove file %s: %s' % (pfn, str(e)))
477
+ gs = False
478
+ continue
479
+
480
+ try: # Try uploading file
481
+ logger(logging.DEBUG, 'Uploading to %s.rucio.upload', pfn)
482
+ protocol.put(base_name, '%s.rucio.upload' % pfn, source_dir, transfer_timeout=transfer_timeout) # type: ignore (source_dir could be None)
483
+ except Exception as e:
484
+ gs = False
485
+ ret['%s:%s' % (scope, name)] = e
486
+ continue
487
+
488
+ valid = None
489
+
490
+ try: # Get metadata of file to verify if upload was successful
491
+ try:
492
+ stats = _retry_protocol_stat(protocol, '%s.rucio.upload' % pfn)
493
+ # Verify all supported checksums and keep rack of the verified ones
494
+ verified_checksums = []
495
+ for checksum_name in GLOBALLY_SUPPORTED_CHECKSUMS:
496
+ if (checksum_name in stats) and (checksum_name in lfn):
497
+ verified_checksums.append(stats[checksum_name] == lfn[checksum_name])
498
+ # Upload is successful if at least one checksum was found
499
+ valid = any(verified_checksums)
500
+ if not valid and ('filesize' in stats) and ('filesize' in lfn):
501
+ valid = stats['filesize'] == lfn['filesize']
502
+ except NotImplementedError:
503
+ if rse_settings['verify_checksum'] is False:
504
+ valid = True
505
+ else:
506
+ raise exception.RucioException('Checksum not validated')
507
+ except exception.RSEChecksumUnavailable:
508
+ if rse_settings['verify_checksum'] is False:
509
+ valid = True
510
+ else:
511
+ raise exception.RucioException('Checksum not validated')
512
+ except Exception as e:
513
+ gs = False
514
+ ret['%s:%s' % (scope, name)] = e
515
+ continue
516
+
517
+ if valid: # The upload finished successful and the file can be renamed
518
+ try:
519
+ logger(logging.DEBUG, 'Renaming %s.rucio.upload to %s', pfn, pfn)
520
+ protocol.rename('%s.rucio.upload' % pfn, pfn)
521
+ ret['%s:%s' % (scope, name)] = True
522
+ except Exception as e:
523
+ gs = False
524
+ ret['%s:%s' % (scope, name)] = e
525
+ else:
526
+ gs = False
527
+ ret['%s:%s' % (scope, name)] = exception.RucioException('Replica %s is corrupted.' % pfn)
528
+ else:
529
+
530
+ # Check if file replica is already on the storage system
531
+ if protocol.overwrite is False and delete_existing is False and protocol.exists(readpfn):
532
+ ret['%s:%s' % (scope, name)] = exception.FileReplicaAlreadyExists('File %s in scope %s already exists on storage as PFN %s' % (name, scope, pfn))
533
+ gs = False
534
+ else:
535
+ try: # Try uploading file
536
+ logger(logging.DEBUG, 'Uploading to %s', pfn)
537
+ protocol.put(base_name, pfn, source_dir, transfer_timeout=transfer_timeout)
538
+ except Exception as e:
539
+ gs = False
540
+ ret['%s:%s' % (scope, name)] = e
541
+ continue
542
+
543
+ valid = None
544
+ try: # Get metadata of file to verify if upload was successful
545
+ try:
546
+ stats = _retry_protocol_stat(protocol, pfn)
547
+
548
+ # Verify all supported checksums and keep rack of the verified ones
549
+ verified_checksums = []
550
+ for checksum_name in GLOBALLY_SUPPORTED_CHECKSUMS:
551
+ if (checksum_name in stats) and (checksum_name in lfn):
552
+ verified_checksums.append(stats[checksum_name] == lfn[checksum_name])
553
+
554
+ # Upload is successful if at least one checksum was found
555
+ valid = any(verified_checksums)
556
+ if not valid and ('filesize' in stats) and ('filesize' in lfn):
557
+ valid = stats['filesize'] == lfn['filesize']
558
+ except NotImplementedError:
559
+ if rse_settings['verify_checksum'] is False:
560
+ valid = True
561
+ else:
562
+ raise exception.RucioException('Checksum not validated')
563
+ except exception.RSEChecksumUnavailable:
564
+ if rse_settings['verify_checksum'] is False:
565
+ valid = True
566
+ else:
567
+ raise exception.RucioException('Checksum not validated')
568
+ except Exception as e:
569
+ gs = False
570
+ ret['%s:%s' % (scope, name)] = e
571
+ continue
572
+
573
+ if not valid:
574
+ gs = False
575
+ ret['%s:%s' % (scope, name)] = exception.RucioException('Replica %s is corrupted.' % pfn)
576
+
577
+ protocol.close()
578
+ protocol_delete.close()
579
+ if len(ret) == 1:
580
+ ret_value = next(iter(ret.values()))
581
+ if isinstance(ret_value, Exception):
582
+ raise ret_value
583
+ else:
584
+ return {0: ret_value, 1: ret, 'success': ret_value, 'pfn': pfn}
585
+ return {0: gs, 1: ret, 'success': gs, 'pfn': pfn}
586
+
587
+
588
+ def delete(
589
+ rse_settings: types.RSESettingsDict,
590
+ lfns: Union[list[types.LFNDict], types.LFNDict],
591
+ domain: str = 'wan',
592
+ auth_token: Optional[str] = None,
593
+ logger: types.LoggerFunction = logging.log,
594
+ impl: Optional[str] = None
595
+ ) -> Union[bool, list[Union[bool, dict[str, Union[Literal[True], Exception]]]]]:
596
+ """
597
+ Delete a file from the connected storage.
598
+ Providing a list indicates the bulk mode.
599
+
600
+ :param rse_settings: RSE attributes
601
+ :param lfns: a single dict or a list with dicts containing 'scope' and 'name'. E.g. [{'name': '1_rse_remote_delete.raw', 'scope': 'user.jdoe'}, {'name': '2_rse_remote_delete.raw', 'scope': 'user.jdoe'}]
602
+ :param domain: The network domain, either 'wan' (default) or 'lan'
603
+ :param auth_token: Optionally passing JSON Web Token (OIDC) string for authentication
604
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
605
+ :returns: True/False for a single file or a dict object with 'scope:name' as keys and True or the exception as value for each file in bulk mode
606
+
607
+ :raises RSENotConnected: no connection to a specific storage has been established
608
+ :raises SourceNotFound: remote source file can not be found on storage
609
+ :raises ServiceUnavailable: for any other reason
610
+
611
+ """
612
+ ret = {}
613
+ gs = True # gs represents the global status which indicates if every operation worked in bulk mode
614
+
615
+ protocol = create_protocol(rse_settings, 'delete', domain=domain, auth_token=auth_token, logger=logger, impl=impl)
616
+ protocol.connect()
617
+
618
+ if not isinstance(lfns, list):
619
+ lfns = [lfns]
620
+ for lfn in lfns:
621
+ pfn = list(protocol.lfns2pfns(lfn).values())[0]
622
+ try:
623
+ protocol.delete(pfn)
624
+ ret['%s:%s' % (lfn['scope'], lfn['name'])] = True
625
+ except Exception as e:
626
+ ret['%s:%s' % (lfn['scope'], lfn['name'])] = e
627
+ gs = False
628
+
629
+ protocol.close()
630
+ if len(ret) == 1:
631
+ ret_value = next(iter(ret.values()))
632
+ if isinstance(ret_value, Exception):
633
+ raise ret_value
634
+ else:
635
+ return ret_value
636
+ return [gs, ret]
637
+
638
+
639
+ def rename(
640
+ rse_settings: types.RSESettingsDict,
641
+ files: Union[list[dict[str, str]], dict[str, str]],
642
+ domain: str = 'wan',
643
+ auth_token: Optional[str] = None,
644
+ logger: types.LoggerFunction = logging.log,
645
+ impl: Optional[str] = None
646
+ ) -> Union[bool, list[Union[bool, dict[str, Union[Literal[True], Exception]]]]]:
647
+ """
648
+ Rename files stored on the connected storage.
649
+ Providing a list indicates the bulk mode.
650
+
651
+ :param rse_settings: RSE attributes
652
+ :param files: a single dict or a list with dicts containing 'scope', 'name', 'new_scope' and 'new_name'
653
+ if LFNs are used or only 'name' and 'new_name' if PFNs are used.
654
+ If 'new_scope' or 'new_name' are not provided, the current one is used.
655
+ Examples:
656
+ [
657
+ {'name': '3_rse_remote_rename.raw', 'scope': 'user.jdoe', 'new_name': '3_rse_new.raw', 'new_scope': 'user.jdoe'},
658
+ {'name': 'user/jdoe/d9/cb/9_rse_remote_rename.raw', 'new_name': 'user/jdoe/c6/4a/9_rse_new.raw'}
659
+ ]
660
+ :param domain: The network domain, either 'wan' (default) or 'lan'
661
+ :param auth_token: Optionally passing JSON Web Token (OIDC) string for authentication
662
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
663
+
664
+ :returns: True/False for a single file or a dict object with LFN (key) and True/False (value) in bulk mode
665
+
666
+ :raises RSENotConnected: no connection to a specific storage has been established
667
+ :raises SourceNotFound: remote source file can not be found on storage
668
+ :raises DestinationNotAccessible: remote destination directory is not accessible
669
+ :raises ServiceUnavailable: for any other reason
670
+ """
671
+ ret = {}
672
+ gs = True # gs represents the global status which indicates if every operation worked in bulk mode
673
+
674
+ protocol = create_protocol(rse_settings, 'write', domain=domain, auth_token=auth_token, logger=logger, impl=impl)
675
+ protocol.connect()
676
+
677
+ if not isinstance(files, list):
678
+ files = [files]
679
+ for f in files:
680
+ pfn = None
681
+ new_pfn = None
682
+ key = None
683
+ if 'scope' in f: # LFN is provided
684
+ key = '%s:%s' % (f['scope'], f['name'])
685
+ # Check if new name is provided
686
+ if 'new_name' not in f:
687
+ f['new_name'] = f['name']
688
+ # Check if new scope is provided
689
+ if 'new_scope' not in f:
690
+ f['new_scope'] = f['scope']
691
+ pfn = list(protocol.lfns2pfns({'name': f['name'], 'scope': f['scope']}).values())[0]
692
+ new_pfn = list(protocol.lfns2pfns({'name': f['new_name'], 'scope': f['new_scope']}).values())[0]
693
+ else:
694
+ pfn = f['name']
695
+ new_pfn = f['new_name']
696
+ key = pfn
697
+ # Check if target is not on storage
698
+ if protocol.exists(new_pfn):
699
+ ret[key] = exception.FileReplicaAlreadyExists('File %s already exists on storage' % (new_pfn))
700
+ gs = False
701
+ # Check if source is on storage
702
+ elif not protocol.exists(pfn):
703
+ ret[key] = exception.SourceNotFound('File %s not found on storage' % (pfn))
704
+ gs = False
705
+ else:
706
+ try:
707
+ protocol.rename(pfn, new_pfn)
708
+ ret[key] = True
709
+ except Exception as e:
710
+ ret[key] = e
711
+ gs = False
712
+
713
+ protocol.close()
714
+ if len(ret) == 1:
715
+ ret_value = next(iter(ret.values()))
716
+ if isinstance(ret_value, Exception):
717
+ raise ret_value
718
+ else:
719
+ return ret_value
720
+ return [gs, ret]
721
+
722
+
723
+ def get_space_usage(
724
+ rse_settings: types.RSESettingsDict,
725
+ scheme: Optional[str] = None,
726
+ domain: str = 'wan',
727
+ auth_token: Optional[str] = None,
728
+ logger: types.LoggerFunction = logging.log,
729
+ impl: Optional[str] = None
730
+ ) -> list[Union[bool, Union[dict[str, int], Exception]]]:
731
+ """
732
+ Get RSE space usage information.
733
+
734
+ :param rse_settings: RSE attributes
735
+ :param scheme: optional filter to select which protocol to be used.
736
+ :param domain: The network domain, either 'wan' (default) or 'lan'
737
+ :param auth_token: Optionally passing JSON Web Token (OIDC) string for authentication
738
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
739
+
740
+ :returns: a list with dict containing 'totalsize' and 'unusedsize'
741
+
742
+ :raises ServiceUnavailable: if some generic error occurred in the library.
743
+ """
744
+ gs = True
745
+ ret = {}
746
+
747
+ protocol = create_protocol(rse_settings, 'read', scheme=scheme, domain=domain, auth_token=auth_token, logger=logger, impl=impl)
748
+ protocol.connect()
749
+
750
+ try:
751
+ totalsize, unusedsize = protocol.get_space_usage()
752
+ ret["totalsize"] = totalsize
753
+ ret["unusedsize"] = unusedsize
754
+ except Exception as e:
755
+ ret = e
756
+ gs = False
757
+
758
+ protocol.close()
759
+ return [gs, ret]
760
+
761
+
762
+ def find_matching_scheme(
763
+ rse_settings_dest: types.RSESettingsDict,
764
+ rse_settings_src: types.RSESettingsDict,
765
+ operation_src: str,
766
+ operation_dest: str,
767
+ domain: str = 'wan',
768
+ scheme: Optional[Union[str, list[str]]] = None
769
+ ) -> tuple[str, str, int, int]:
770
+ """
771
+ Find the best matching scheme between two RSEs
772
+
773
+ :param rse_settings_dest: RSE settings for the destination RSE.
774
+ :param rse_settings_src: RSE settings for the src RSE.
775
+ :param operation_src: Source Operation such as read, write.
776
+ :param operation_dest: Dest Operation such as read, write.
777
+ :param domain: Domain such as lan, wan.
778
+ :param scheme: List of supported schemes.
779
+ :returns: Tuple of matching schemes (dest_scheme, src_scheme, dest_scheme_priority, src_scheme_priority).
780
+ """
781
+ operation_src = operation_src.lower()
782
+ operation_dest = operation_dest.lower()
783
+
784
+ src_candidates = copy.copy(rse_settings_src['protocols'])
785
+ dest_candidates = copy.copy(rse_settings_dest['protocols'])
786
+
787
+ # Clean up src_candidates
788
+ tbr = list()
789
+ for protocol in src_candidates:
790
+ # Check if scheme given and filter if so
791
+ if scheme:
792
+ if not isinstance(scheme, list):
793
+ scheme = scheme.split(',')
794
+ if protocol['scheme'] not in scheme:
795
+ tbr.append(protocol)
796
+ continue
797
+ prot = protocol['domains'].get(domain, {}).get(operation_src, 1)
798
+ if prot is None or prot == 0:
799
+ tbr.append(protocol)
800
+ for r in tbr:
801
+ src_candidates.remove(r)
802
+
803
+ # Clean up dest_candidates
804
+ tbr = list()
805
+ for protocol in dest_candidates:
806
+ # Check if scheme given and filter if so
807
+ if scheme:
808
+ if not isinstance(scheme, list):
809
+ scheme = scheme.split(',')
810
+ if protocol['scheme'] not in scheme:
811
+ tbr.append(protocol)
812
+ continue
813
+ prot = protocol['domains'].get(domain, {}).get(operation_dest, 1)
814
+ if prot is None or prot == 0:
815
+ tbr.append(protocol)
816
+ for r in tbr:
817
+ dest_candidates.remove(r)
818
+
819
+ if not len(src_candidates) or not len(dest_candidates):
820
+ raise exception.RSEProtocolNotSupported('No protocol for provided settings found : %s.' % str(rse_settings_dest))
821
+
822
+ # Shuffle the candidates to load-balance across equal weights.
823
+ random.shuffle(dest_candidates)
824
+ random.shuffle(src_candidates)
825
+
826
+ # Select the one with the highest priority
827
+ dest_candidates = sorted(dest_candidates, key=lambda k: k['domains'][domain][operation_dest])
828
+ src_candidates = sorted(src_candidates, key=lambda k: k['domains'][domain][operation_src])
829
+
830
+ for dest_protocol in dest_candidates:
831
+ for src_protocol in src_candidates:
832
+ if __check_compatible_scheme(dest_protocol['scheme'], src_protocol['scheme']):
833
+ return (dest_protocol['scheme'], src_protocol['scheme'], dest_protocol['domains'][domain][operation_dest], src_protocol['domains'][domain][operation_src])
834
+
835
+ raise exception.RSEProtocolNotSupported('No protocol for provided settings found : %s.' % str(rse_settings_dest))
836
+
837
+
838
+ def _retry_protocol_stat(
839
+ protocol: "RSEProtocol",
840
+ pfn: str
841
+ ) -> dict[str, Any]:
842
+ """
843
+ try to stat file, on fail try again 1s, 2s, 4s, 8s, 16s, 32s later. Fail is all fail
844
+
845
+ :param protocol: The protocol to use to reach this file
846
+ :param pfn: Physical file name of the target for the protocol stat
847
+ """
848
+ retries = config_get_int('client', 'protocol_stat_retries', raise_exception=False, default=6)
849
+ for attempt in range(retries):
850
+ try:
851
+ stats = protocol.stat(pfn)
852
+ return stats
853
+ except exception.RSEChecksumUnavailable as e:
854
+ # The stat succeeded here, but the checksum failed
855
+ raise e
856
+ except NotImplementedError:
857
+ break
858
+ except Exception:
859
+ sleep(2**attempt)
860
+ return protocol.stat(pfn)
861
+
862
+
863
+ def __check_compatible_scheme(
864
+ dest_scheme: str,
865
+ src_scheme: str
866
+ ) -> bool:
867
+ """
868
+ Check if two schemes are compatible, such as srm and gsiftp
869
+
870
+ :param dest_scheme: Destination scheme
871
+ :param src_scheme: Source scheme
872
+ :param scheme: List of supported schemes
873
+ :returns: True if schemes are compatible, False otherwise.
874
+ """
875
+
876
+ if dest_scheme == src_scheme:
877
+ return True
878
+ if src_scheme in constants.SCHEME_MAP.get(dest_scheme, []):
879
+ return True
880
+
881
+ return False