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