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.
- rucio/__init__.py +17 -0
- rucio/alembicrevision.py +15 -0
- rucio/client/__init__.py +15 -0
- rucio/client/accountclient.py +433 -0
- rucio/client/accountlimitclient.py +183 -0
- rucio/client/baseclient.py +974 -0
- rucio/client/client.py +76 -0
- rucio/client/configclient.py +126 -0
- rucio/client/credentialclient.py +59 -0
- rucio/client/didclient.py +866 -0
- rucio/client/diracclient.py +56 -0
- rucio/client/downloadclient.py +1785 -0
- rucio/client/exportclient.py +44 -0
- rucio/client/fileclient.py +50 -0
- rucio/client/importclient.py +42 -0
- rucio/client/lifetimeclient.py +90 -0
- rucio/client/lockclient.py +109 -0
- rucio/client/metaconventionsclient.py +140 -0
- rucio/client/pingclient.py +44 -0
- rucio/client/replicaclient.py +454 -0
- rucio/client/requestclient.py +125 -0
- rucio/client/rseclient.py +746 -0
- rucio/client/ruleclient.py +294 -0
- rucio/client/scopeclient.py +90 -0
- rucio/client/subscriptionclient.py +173 -0
- rucio/client/touchclient.py +82 -0
- rucio/client/uploadclient.py +955 -0
- rucio/common/__init__.py +13 -0
- rucio/common/cache.py +74 -0
- rucio/common/config.py +801 -0
- rucio/common/constants.py +159 -0
- rucio/common/constraints.py +17 -0
- rucio/common/didtype.py +189 -0
- rucio/common/exception.py +1151 -0
- rucio/common/extra.py +36 -0
- rucio/common/logging.py +420 -0
- rucio/common/pcache.py +1408 -0
- rucio/common/plugins.py +153 -0
- rucio/common/policy.py +84 -0
- rucio/common/schema/__init__.py +150 -0
- rucio/common/schema/atlas.py +413 -0
- rucio/common/schema/belleii.py +408 -0
- rucio/common/schema/domatpc.py +401 -0
- rucio/common/schema/escape.py +426 -0
- rucio/common/schema/generic.py +433 -0
- rucio/common/schema/generic_multi_vo.py +412 -0
- rucio/common/schema/icecube.py +406 -0
- rucio/common/stomp_utils.py +159 -0
- rucio/common/stopwatch.py +55 -0
- rucio/common/test_rucio_server.py +148 -0
- rucio/common/types.py +403 -0
- rucio/common/utils.py +2238 -0
- rucio/rse/__init__.py +96 -0
- rucio/rse/protocols/__init__.py +13 -0
- rucio/rse/protocols/bittorrent.py +184 -0
- rucio/rse/protocols/cache.py +122 -0
- rucio/rse/protocols/dummy.py +111 -0
- rucio/rse/protocols/gfal.py +703 -0
- rucio/rse/protocols/globus.py +243 -0
- rucio/rse/protocols/gsiftp.py +92 -0
- rucio/rse/protocols/http_cache.py +82 -0
- rucio/rse/protocols/mock.py +123 -0
- rucio/rse/protocols/ngarc.py +209 -0
- rucio/rse/protocols/posix.py +250 -0
- rucio/rse/protocols/protocol.py +594 -0
- rucio/rse/protocols/rclone.py +364 -0
- rucio/rse/protocols/rfio.py +136 -0
- rucio/rse/protocols/srm.py +338 -0
- rucio/rse/protocols/ssh.py +413 -0
- rucio/rse/protocols/storm.py +206 -0
- rucio/rse/protocols/webdav.py +550 -0
- rucio/rse/protocols/xrootd.py +301 -0
- rucio/rse/rsemanager.py +764 -0
- rucio/vcsversion.py +11 -0
- rucio/version.py +38 -0
- rucio_clients-35.8.2.data/data/etc/rse-accounts.cfg.template +25 -0
- rucio_clients-35.8.2.data/data/etc/rucio.cfg.atlas.client.template +42 -0
- rucio_clients-35.8.2.data/data/etc/rucio.cfg.template +257 -0
- rucio_clients-35.8.2.data/data/requirements.client.txt +15 -0
- rucio_clients-35.8.2.data/data/rucio_client/merge_rucio_configs.py +144 -0
- rucio_clients-35.8.2.data/scripts/rucio +2542 -0
- rucio_clients-35.8.2.data/scripts/rucio-admin +2447 -0
- rucio_clients-35.8.2.dist-info/METADATA +50 -0
- rucio_clients-35.8.2.dist-info/RECORD +88 -0
- rucio_clients-35.8.2.dist-info/WHEEL +5 -0
- rucio_clients-35.8.2.dist-info/licenses/AUTHORS.rst +97 -0
- rucio_clients-35.8.2.dist-info/licenses/LICENSE +201 -0
- rucio_clients-35.8.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,703 @@
|
|
|
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 errno
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import subprocess
|
|
21
|
+
import urllib.parse as urlparse
|
|
22
|
+
from threading import Timer
|
|
23
|
+
|
|
24
|
+
from rucio.common import config, exception
|
|
25
|
+
from rucio.common.constraints import STRING_TYPES
|
|
26
|
+
from rucio.common.utils import GLOBALLY_SUPPORTED_CHECKSUMS, PREFERRED_CHECKSUM
|
|
27
|
+
from rucio.rse.protocols import protocol
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
import gfal2 # pylint: disable=import-error
|
|
31
|
+
except:
|
|
32
|
+
if 'RUCIO_CLIENT_MODE' not in os.environ:
|
|
33
|
+
if not config.config_has_section('database'):
|
|
34
|
+
raise exception.MissingDependency('Missing dependency : gfal2')
|
|
35
|
+
else:
|
|
36
|
+
if os.environ['RUCIO_CLIENT_MODE']:
|
|
37
|
+
raise exception.MissingDependency('Missing dependency : gfal2')
|
|
38
|
+
|
|
39
|
+
TIMEOUT = config.config_get('deletion', 'timeout', False, None)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Default(protocol.RSEProtocol):
|
|
43
|
+
""" Implementing access to RSEs using the srm protocol."""
|
|
44
|
+
|
|
45
|
+
def lfns2pfns(self, lfns):
|
|
46
|
+
"""
|
|
47
|
+
Returns a fully qualified PFN for the file referred by path.
|
|
48
|
+
|
|
49
|
+
:param path: The path to the file.
|
|
50
|
+
|
|
51
|
+
:returns: Fully qualified PFN.
|
|
52
|
+
"""
|
|
53
|
+
lfns = [lfns] if isinstance(lfns, dict) else lfns
|
|
54
|
+
|
|
55
|
+
pfns = {}
|
|
56
|
+
prefix = self.attributes['prefix']
|
|
57
|
+
if self.attributes['extended_attributes'] is not None and 'web_service_path' in list(self.attributes['extended_attributes'].keys()):
|
|
58
|
+
web_service_path = self.attributes['extended_attributes']['web_service_path']
|
|
59
|
+
else:
|
|
60
|
+
web_service_path = ''
|
|
61
|
+
|
|
62
|
+
if not prefix.startswith('/'):
|
|
63
|
+
prefix = ''.join(['/', prefix])
|
|
64
|
+
if not prefix.endswith('/'):
|
|
65
|
+
prefix = ''.join([prefix, '/'])
|
|
66
|
+
|
|
67
|
+
hostname = self.attributes['hostname']
|
|
68
|
+
if '://' in hostname:
|
|
69
|
+
hostname = hostname.split("://")[1]
|
|
70
|
+
|
|
71
|
+
if self.attributes['port'] == 0:
|
|
72
|
+
for lfn in lfns:
|
|
73
|
+
scope, name = str(lfn['scope']), lfn['name']
|
|
74
|
+
path = lfn['path'] if 'path' in lfn and lfn['path'] else self._get_path(scope=scope, name=name)
|
|
75
|
+
if self.attributes['scheme'] != 'root' and path.startswith('/'): # do not modify path if it is root
|
|
76
|
+
path = path[1:]
|
|
77
|
+
pfns['%s:%s' % (scope, name)] = ''.join([self.attributes['scheme'], '://', hostname, web_service_path, prefix, path])
|
|
78
|
+
else:
|
|
79
|
+
for lfn in lfns:
|
|
80
|
+
scope, name = str(lfn['scope']), lfn['name']
|
|
81
|
+
path = lfn['path'] if 'path' in lfn and lfn['path'] else self._get_path(scope=scope, name=name)
|
|
82
|
+
if self.attributes['scheme'] != 'root' and path.startswith('/'): # do not modify path if it is root
|
|
83
|
+
path = path[1:]
|
|
84
|
+
if re.match(r'^\w+://', path): # This is already a URL
|
|
85
|
+
pfns['%s:%s' % (scope, name)] = path
|
|
86
|
+
else:
|
|
87
|
+
pfns['%s:%s' % (scope, name)] = ''.join([self.attributes['scheme'], '://', hostname, ':', str(self.attributes['port']), web_service_path, prefix, path])
|
|
88
|
+
|
|
89
|
+
return pfns
|
|
90
|
+
|
|
91
|
+
def parse_pfns(self, pfns):
|
|
92
|
+
"""
|
|
93
|
+
Splits the given PFN into the parts known by the protocol. During parsing the PFN is also checked for
|
|
94
|
+
validity on the given RSE with the given protocol.
|
|
95
|
+
|
|
96
|
+
:param pfn: a fully qualified PFN
|
|
97
|
+
|
|
98
|
+
:returns: a dict containing all known parts of the PFN for the protocol e.g. scheme, path, filename
|
|
99
|
+
|
|
100
|
+
:raises RSEFileNameNotSupported: if the provided PFN doesn't match with the protocol settings
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
self.logger(logging.DEBUG, 'parsing {} pfns'.format(len(list(pfns))))
|
|
104
|
+
ret = dict()
|
|
105
|
+
pfns = [pfns] if isinstance(pfns, STRING_TYPES) else pfns
|
|
106
|
+
for pfn in pfns:
|
|
107
|
+
parsed = urlparse.urlparse(pfn)
|
|
108
|
+
if parsed.path.startswith('/srm/managerv2') or parsed.path.startswith('/srm/managerv1') or parsed.path.startswith('/srm/v2/server'):
|
|
109
|
+
scheme, hostname, port, service_path, path = re.findall(r"([^:]+)://([^:/]+):?(\d+)?([^:]+=)?([^:]+)", pfn)[0]
|
|
110
|
+
else:
|
|
111
|
+
scheme = parsed.scheme
|
|
112
|
+
hostname = parsed.netloc.partition(':')[0]
|
|
113
|
+
port = parsed.netloc.partition(':')[2]
|
|
114
|
+
path = parsed.path
|
|
115
|
+
service_path = ''
|
|
116
|
+
|
|
117
|
+
if self.attributes['hostname'] != hostname and self.attributes['hostname'] != scheme + "://" + hostname:
|
|
118
|
+
raise exception.RSEFileNameNotSupported('Invalid hostname: provided \'%s\', expected \'%s\'' % (hostname, self.attributes['hostname']))
|
|
119
|
+
|
|
120
|
+
if port != '' and str(self.attributes['port']) != str(port):
|
|
121
|
+
raise exception.RSEFileNameNotSupported('Invalid port: provided \'%s\', expected \'%s\'' % (port, self.attributes['port']))
|
|
122
|
+
elif port == '':
|
|
123
|
+
port = self.attributes['port']
|
|
124
|
+
|
|
125
|
+
if not path.startswith(self.attributes['prefix']):
|
|
126
|
+
raise exception.RSEFileNameNotSupported('Invalid prefix: provided \'%s\', expected \'%s\'' % ('/'.join(path.split('/')[0:len(self.attributes['prefix'].split('/')) - 1]),
|
|
127
|
+
self.attributes['prefix'])) # len(...)-1 due to the leading '/
|
|
128
|
+
# Splitting path into prefix, path, filename
|
|
129
|
+
prefix = self.attributes['prefix']
|
|
130
|
+
path = path.partition(self.attributes['prefix'])[2]
|
|
131
|
+
name = path.split('/')[-1]
|
|
132
|
+
path = '/'.join(path.split('/')[:-1])
|
|
133
|
+
if not path.startswith('/'):
|
|
134
|
+
path = '/' + path
|
|
135
|
+
if path != '/' and not path.endswith('/'):
|
|
136
|
+
path = path + '/'
|
|
137
|
+
ret[pfn] = {'scheme': scheme, 'port': port, 'hostname': hostname, 'path': path, 'name': name, 'prefix': prefix, 'web_service_path': service_path}
|
|
138
|
+
|
|
139
|
+
return ret
|
|
140
|
+
|
|
141
|
+
def path2pfn(self, path):
|
|
142
|
+
"""
|
|
143
|
+
Returns a fully qualified PFN for the file referred by path.
|
|
144
|
+
|
|
145
|
+
:param path: The path to the file.
|
|
146
|
+
|
|
147
|
+
:returns: Fully qualified PFN.
|
|
148
|
+
"""
|
|
149
|
+
self.logger(logging.DEBUG, 'getting pfn for {}'.format(path))
|
|
150
|
+
|
|
151
|
+
if '://' in path:
|
|
152
|
+
return path
|
|
153
|
+
|
|
154
|
+
hostname = self.attributes['hostname']
|
|
155
|
+
if '://' in hostname:
|
|
156
|
+
hostname = hostname.split("://")[1]
|
|
157
|
+
|
|
158
|
+
if 'extended_attributes' in list(self.attributes.keys()) and self.attributes['extended_attributes'] is not None and 'web_service_path' in list(self.attributes['extended_attributes'].keys()):
|
|
159
|
+
web_service_path = self.attributes['extended_attributes']['web_service_path']
|
|
160
|
+
else:
|
|
161
|
+
web_service_path = ''
|
|
162
|
+
|
|
163
|
+
if not path.startswith('srm'):
|
|
164
|
+
if self.attributes['port'] > 0:
|
|
165
|
+
return ''.join([self.attributes['scheme'], '://', hostname, ':', str(self.attributes['port']), web_service_path, path])
|
|
166
|
+
else:
|
|
167
|
+
return ''.join([self.attributes['scheme'], '://', hostname, web_service_path, path])
|
|
168
|
+
else:
|
|
169
|
+
return path
|
|
170
|
+
|
|
171
|
+
def connect(self):
|
|
172
|
+
"""
|
|
173
|
+
Establishes the actual connection to the referred RSE.
|
|
174
|
+
If we decide to use gfal, init should be done here.
|
|
175
|
+
|
|
176
|
+
:raises RSEAccessDenied
|
|
177
|
+
"""
|
|
178
|
+
self.logger(logging.DEBUG, 'connecting to storage')
|
|
179
|
+
|
|
180
|
+
if 'RUCIO_CLIENT_MODE' in os.environ:
|
|
181
|
+
gfal2.set_verbose(gfal2.verbose_level.verbose)
|
|
182
|
+
else:
|
|
183
|
+
gfal2.set_verbose(gfal2.verbose_level.warning)
|
|
184
|
+
|
|
185
|
+
self.__ctx = gfal2.creat_context() # pylint: disable=no-member
|
|
186
|
+
self.__ctx.set_opt_string_list("SRM PLUGIN", "TURL_PROTOCOLS", ["gsiftp", "rfio", "gsidcap", "dcap", "kdcap"])
|
|
187
|
+
self.__ctx.set_opt_string("XROOTD PLUGIN", "XRD.WANTPROT", "gsi,unix")
|
|
188
|
+
self.__ctx.set_opt_boolean("XROOTD PLUGIN", "NORMALIZE_PATH", False)
|
|
189
|
+
auth_configured = False
|
|
190
|
+
if self.auth_token:
|
|
191
|
+
self.__ctx.set_opt_string("BEARER", "TOKEN", self.auth_token)
|
|
192
|
+
auth_configured = True
|
|
193
|
+
# Configure gfal authentication to use the rucio client proxy if and only if gfal didn't initialize its credentials already
|
|
194
|
+
# (https://gitlab.cern.ch/dmc/gfal2/-/blob/48cfe3476392c884b53d00799198b1238603a406/src/core/common/gfal_common.c#L79)
|
|
195
|
+
if not auth_configured:
|
|
196
|
+
try:
|
|
197
|
+
self.__ctx.get_opt_string("X509", "CERT")
|
|
198
|
+
self.__ctx.get_opt_string("X509", "KEY")
|
|
199
|
+
auth_configured = True
|
|
200
|
+
except gfal2.GError: # pylint: disable=no-member
|
|
201
|
+
pass
|
|
202
|
+
if not auth_configured:
|
|
203
|
+
try:
|
|
204
|
+
self.__ctx.get_opt_string("BEARER", "TOKEN")
|
|
205
|
+
auth_configured = True
|
|
206
|
+
except gfal2.GError: # pylint: disable=no-member
|
|
207
|
+
pass
|
|
208
|
+
if not auth_configured:
|
|
209
|
+
proxy = config.config_get('client', 'client_x509_proxy', default=None, raise_exception=False)
|
|
210
|
+
if proxy:
|
|
211
|
+
self.logger(logging.INFO, 'Configuring authentication to use {}'.format(proxy))
|
|
212
|
+
self.__ctx.set_opt_string("X509", "CERT", proxy)
|
|
213
|
+
self.__ctx.set_opt_string("X509", "KEY", proxy)
|
|
214
|
+
|
|
215
|
+
if TIMEOUT:
|
|
216
|
+
try:
|
|
217
|
+
timeout = int(TIMEOUT)
|
|
218
|
+
self.__ctx.set_opt_integer("HTTP PLUGIN", "OPERATION_TIMEOUT", timeout)
|
|
219
|
+
self.__ctx.set_opt_integer("SRM PLUGIN", "OPERATION_TIMEOUT", timeout)
|
|
220
|
+
self.__ctx.set_opt_integer("GRIDFTP PLUGIN", "OPERATION_TIMEOUT", timeout)
|
|
221
|
+
except ValueError:
|
|
222
|
+
self.logger(logging.ERROR, 'wrong timeout value %s', TIMEOUT)
|
|
223
|
+
|
|
224
|
+
def get(self, path, dest, transfer_timeout=None):
|
|
225
|
+
"""
|
|
226
|
+
Provides access to files stored inside connected the RSE.
|
|
227
|
+
|
|
228
|
+
:param path: Physical file name of requested file
|
|
229
|
+
:param dest: Name and path of the files when stored at the client
|
|
230
|
+
:param transfer_timeout: Transfer timeout (in seconds)
|
|
231
|
+
|
|
232
|
+
:raises DestinationNotAccessible: if the destination storage was not accessible.
|
|
233
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
234
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
235
|
+
"""
|
|
236
|
+
self.logger(logging.DEBUG, 'downloading file from {} to {}'.format(path, dest))
|
|
237
|
+
|
|
238
|
+
dest = os.path.abspath(dest)
|
|
239
|
+
if ':' not in dest:
|
|
240
|
+
dest = "file://" + dest
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
status = self.__gfal2_copy(path, dest, transfer_timeout=transfer_timeout)
|
|
244
|
+
if status:
|
|
245
|
+
raise exception.RucioException()
|
|
246
|
+
except exception.DestinationNotAccessible as error:
|
|
247
|
+
raise exception.DestinationNotAccessible(str(error))
|
|
248
|
+
except exception.SourceNotFound as error:
|
|
249
|
+
raise exception.SourceNotFound(str(error))
|
|
250
|
+
except Exception as error:
|
|
251
|
+
raise exception.ServiceUnavailable(error)
|
|
252
|
+
|
|
253
|
+
def put(self, source, target, source_dir, transfer_timeout=None):
|
|
254
|
+
"""
|
|
255
|
+
Allows to store files inside the referred RSE.
|
|
256
|
+
|
|
257
|
+
:param source: path to the source file on the client file system
|
|
258
|
+
:param target: path to the destination file on the storage
|
|
259
|
+
:param source_dir: Path where the to be transferred files are stored in the local file system
|
|
260
|
+
:param transfer_timeout: Transfer timeout (in seconds)
|
|
261
|
+
|
|
262
|
+
:raises DestinationNotAccessible: if the destination storage was not accessible.
|
|
263
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
264
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
265
|
+
"""
|
|
266
|
+
self.logger(logging.DEBUG, 'uploading file from {} to {}'.format(source, target))
|
|
267
|
+
|
|
268
|
+
source_url = '%s/%s' % (source_dir, source) if source_dir else source
|
|
269
|
+
source_url = os.path.abspath(source_url)
|
|
270
|
+
if not os.path.exists(source_url):
|
|
271
|
+
raise exception.SourceNotFound()
|
|
272
|
+
if ':' not in source_url:
|
|
273
|
+
source_url = "file://" + source_url
|
|
274
|
+
|
|
275
|
+
space_token = None
|
|
276
|
+
if self.attributes['extended_attributes'] is not None and 'space_token' in list(self.attributes['extended_attributes'].keys()):
|
|
277
|
+
space_token = self.attributes['extended_attributes']['space_token']
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
status = self.__gfal2_copy(str(source_url), str(target), None, space_token, transfer_timeout=transfer_timeout)
|
|
281
|
+
if status:
|
|
282
|
+
raise exception.RucioException()
|
|
283
|
+
except exception.DestinationNotAccessible as error:
|
|
284
|
+
raise exception.DestinationNotAccessible(str(error))
|
|
285
|
+
except exception.SourceNotFound as error:
|
|
286
|
+
raise exception.DestinationNotAccessible(str(error))
|
|
287
|
+
except Exception as error:
|
|
288
|
+
raise exception.ServiceUnavailable(error)
|
|
289
|
+
|
|
290
|
+
def delete(self, path):
|
|
291
|
+
"""
|
|
292
|
+
Deletes a file from the connected RSE.
|
|
293
|
+
|
|
294
|
+
:param path: path to the to be deleted file
|
|
295
|
+
|
|
296
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
297
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
298
|
+
"""
|
|
299
|
+
self.logger(logging.DEBUG, 'deleting file {}'.format(path))
|
|
300
|
+
|
|
301
|
+
pfns = [path] if isinstance(path, STRING_TYPES) else path
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
status = self.__gfal2_rm(pfns)
|
|
305
|
+
if status:
|
|
306
|
+
raise exception.RucioException()
|
|
307
|
+
except exception.SourceNotFound as error:
|
|
308
|
+
raise exception.SourceNotFound(str(error))
|
|
309
|
+
except Exception as error:
|
|
310
|
+
raise exception.ServiceUnavailable(error)
|
|
311
|
+
|
|
312
|
+
def rename(self, path, new_path):
|
|
313
|
+
"""
|
|
314
|
+
Allows to rename a file stored inside the connected RSE.
|
|
315
|
+
|
|
316
|
+
:param path: path to the current file on the storage
|
|
317
|
+
:param new_path: path to the new file on the storage
|
|
318
|
+
|
|
319
|
+
:raises DestinationNotAccessible: if the destination storage was not accessible.
|
|
320
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
321
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
322
|
+
"""
|
|
323
|
+
self.logger(logging.DEBUG, 'renaming file from {} to {}'.format(path, new_path))
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
status = self.__gfal2_rename(path, new_path)
|
|
327
|
+
if status:
|
|
328
|
+
raise exception.RucioException()
|
|
329
|
+
except exception.DestinationNotAccessible as error:
|
|
330
|
+
raise exception.DestinationNotAccessible(str(error))
|
|
331
|
+
except exception.SourceNotFound as error:
|
|
332
|
+
raise exception.SourceNotFound(str(error))
|
|
333
|
+
except Exception as error:
|
|
334
|
+
raise exception.ServiceUnavailable(error)
|
|
335
|
+
|
|
336
|
+
def exists(self, path):
|
|
337
|
+
"""
|
|
338
|
+
Checks if the requested file is known by the referred RSE.
|
|
339
|
+
|
|
340
|
+
:param path: Physical file name
|
|
341
|
+
|
|
342
|
+
:returns: True if the file exists, False if it doesn't
|
|
343
|
+
|
|
344
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
345
|
+
"""
|
|
346
|
+
self.logger(logging.DEBUG, 'checking if file exists {}'.format(path))
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
status = self.__gfal2_exist(path)
|
|
350
|
+
if status:
|
|
351
|
+
return False
|
|
352
|
+
return True
|
|
353
|
+
except exception.SourceNotFound:
|
|
354
|
+
return False
|
|
355
|
+
except Exception as error:
|
|
356
|
+
raise exception.ServiceUnavailable(error)
|
|
357
|
+
|
|
358
|
+
def close(self):
|
|
359
|
+
"""
|
|
360
|
+
Closes the connection to RSE.
|
|
361
|
+
"""
|
|
362
|
+
self.logger(logging.DEBUG, 'closing protocol connection')
|
|
363
|
+
del self.__ctx
|
|
364
|
+
self.__ctx = None
|
|
365
|
+
|
|
366
|
+
def stat(self, path):
|
|
367
|
+
"""
|
|
368
|
+
Returns the stats of a file.
|
|
369
|
+
|
|
370
|
+
:param path: path to file
|
|
371
|
+
|
|
372
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
373
|
+
|
|
374
|
+
:returns: a dict with two keys, filesize and an element of GLOBALLY_SUPPORTED_CHECKSUMS.
|
|
375
|
+
"""
|
|
376
|
+
self.logger(logging.DEBUG, 'getting stats of file {}'.format(path))
|
|
377
|
+
|
|
378
|
+
ret = {}
|
|
379
|
+
ctx = self.__ctx
|
|
380
|
+
|
|
381
|
+
path = str(path)
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
stat_str = str(ctx.stat(path))
|
|
385
|
+
except Exception as error:
|
|
386
|
+
msg = 'Error while processing gfal stat call. Error: %s'
|
|
387
|
+
raise exception.ServiceUnavailable(msg % str(error))
|
|
388
|
+
|
|
389
|
+
stats = stat_str.split()
|
|
390
|
+
if len(stats) < 8:
|
|
391
|
+
msg = 'gfal stat call result has unknown format. Result: %s'
|
|
392
|
+
raise exception.ServiceUnavailable(msg % stat_str)
|
|
393
|
+
|
|
394
|
+
ret['filesize'] = stats[7]
|
|
395
|
+
|
|
396
|
+
if not self.rse.get('verify_checksum', True):
|
|
397
|
+
return ret
|
|
398
|
+
|
|
399
|
+
message = "\n"
|
|
400
|
+
try:
|
|
401
|
+
ret[PREFERRED_CHECKSUM] = ctx.checksum(path, str(PREFERRED_CHECKSUM.upper()))
|
|
402
|
+
return ret
|
|
403
|
+
except Exception as error:
|
|
404
|
+
message += 'Error while processing gfal checksum call (%s). Error: %s \n' % (PREFERRED_CHECKSUM, str(error))
|
|
405
|
+
|
|
406
|
+
for checksum_name in GLOBALLY_SUPPORTED_CHECKSUMS:
|
|
407
|
+
if checksum_name == PREFERRED_CHECKSUM:
|
|
408
|
+
continue
|
|
409
|
+
try:
|
|
410
|
+
ret[checksum_name] = ctx.checksum(path, str(checksum_name.upper()))
|
|
411
|
+
return ret
|
|
412
|
+
except Exception as error:
|
|
413
|
+
message += 'Error while processing gfal checksum call (%s). Error: %s \n' % (checksum_name, str(error))
|
|
414
|
+
|
|
415
|
+
raise exception.RSEChecksumUnavailable(message)
|
|
416
|
+
|
|
417
|
+
def __gfal2_cancel(self):
|
|
418
|
+
"""
|
|
419
|
+
Cancel all gfal operations in progress.
|
|
420
|
+
"""
|
|
421
|
+
self.logger(logging.DEBUG, 'gfal: cancelling all operations')
|
|
422
|
+
|
|
423
|
+
ctx = self.__ctx
|
|
424
|
+
if ctx:
|
|
425
|
+
ctx.cancel()
|
|
426
|
+
|
|
427
|
+
def __gfal2_copy(self, src, dest, src_spacetoken=None, dest_spacetoken=None, transfer_timeout=None):
|
|
428
|
+
"""
|
|
429
|
+
Uses gfal2 to copy file from src to dest.
|
|
430
|
+
|
|
431
|
+
:param src: Physical source file name
|
|
432
|
+
:param src_spacetoken: The source file's space token
|
|
433
|
+
:param dest: Physical destination file name
|
|
434
|
+
:param dest_spacetoken: The destination file's space token
|
|
435
|
+
:param transfer_timeout: Transfer timeout (in seconds)
|
|
436
|
+
|
|
437
|
+
:returns: 0 if copied successfully, other than 0 if failed
|
|
438
|
+
|
|
439
|
+
:raises SourceNotFound: if source file cannot be found.
|
|
440
|
+
:raises RucioException: if it failed to copy the file.
|
|
441
|
+
"""
|
|
442
|
+
ctx = self.__ctx
|
|
443
|
+
if transfer_timeout:
|
|
444
|
+
ctx.set_opt_integer("HTTP PLUGIN", "OPERATION_TIMEOUT", int(transfer_timeout))
|
|
445
|
+
ctx.set_opt_integer("SRM PLUGIN", "OPERATION_TIMEOUT", int(transfer_timeout))
|
|
446
|
+
ctx.set_opt_integer("GRIDFTP PLUGIN", "OPERATION_TIMEOUT", int(transfer_timeout))
|
|
447
|
+
watchdog = Timer(int(transfer_timeout) + 60, self.__gfal2_cancel)
|
|
448
|
+
params = ctx.transfer_parameters()
|
|
449
|
+
if src_spacetoken:
|
|
450
|
+
params.src_spacetoken = str(src_spacetoken)
|
|
451
|
+
if dest_spacetoken:
|
|
452
|
+
params.dst_spacetoken = str(dest_spacetoken)
|
|
453
|
+
|
|
454
|
+
if not (self.renaming and dest.startswith('https')):
|
|
455
|
+
params.create_parent = True
|
|
456
|
+
|
|
457
|
+
if not self.renaming:
|
|
458
|
+
params.strict_copy = True
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
if transfer_timeout:
|
|
462
|
+
params.timeout = int(transfer_timeout)
|
|
463
|
+
watchdog.start()
|
|
464
|
+
ret = ctx.filecopy(params, str(src), str(dest))
|
|
465
|
+
if transfer_timeout:
|
|
466
|
+
watchdog.cancel()
|
|
467
|
+
return ret
|
|
468
|
+
except gfal2.GError as error: # pylint: disable=no-member
|
|
469
|
+
if transfer_timeout:
|
|
470
|
+
watchdog.cancel()
|
|
471
|
+
if error.code == errno.ENOENT or 'No such file' in str(error):
|
|
472
|
+
raise exception.SourceNotFound(error)
|
|
473
|
+
raise exception.RucioException(error)
|
|
474
|
+
|
|
475
|
+
def __gfal2_rm(self, paths):
|
|
476
|
+
"""
|
|
477
|
+
Uses gfal2 to remove the file.
|
|
478
|
+
|
|
479
|
+
:param path: Physical file name
|
|
480
|
+
|
|
481
|
+
:returns: 0 if removed successfully, other than 0 if failed
|
|
482
|
+
|
|
483
|
+
:raises SourceNotFound: if the source file was not found.
|
|
484
|
+
:raises RucioException: if it failed to remove the file.
|
|
485
|
+
"""
|
|
486
|
+
|
|
487
|
+
ctx = self.__ctx
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
for path in paths:
|
|
491
|
+
if self.__gfal2_exist(path) == 0:
|
|
492
|
+
ret = ctx.unlink(str(path))
|
|
493
|
+
if ret:
|
|
494
|
+
return ret
|
|
495
|
+
else:
|
|
496
|
+
raise exception.SourceNotFound
|
|
497
|
+
return ret
|
|
498
|
+
except gfal2.GError as error: # pylint: disable=no-member
|
|
499
|
+
if error.code == errno.ENOENT or 'No such file' in str(error):
|
|
500
|
+
raise exception.SourceNotFound(error)
|
|
501
|
+
raise exception.RucioException(error)
|
|
502
|
+
|
|
503
|
+
def __gfal2_exist(self, path):
|
|
504
|
+
"""
|
|
505
|
+
Uses gfal2 to check whether the file exists.
|
|
506
|
+
|
|
507
|
+
:param path: Physical file name
|
|
508
|
+
|
|
509
|
+
:returns: 0 if it exists, -1 if it doesn't
|
|
510
|
+
|
|
511
|
+
:raises RucioException: if the error is not source not found.
|
|
512
|
+
"""
|
|
513
|
+
ctx = self.__ctx
|
|
514
|
+
try:
|
|
515
|
+
if ctx.stat(str(path)):
|
|
516
|
+
return 0
|
|
517
|
+
return -1
|
|
518
|
+
except gfal2.GError as error: # pylint: disable=no-member
|
|
519
|
+
if error.code == errno.ENOENT or 'No such file' in str(error): # pylint: disable=no-member
|
|
520
|
+
return -1
|
|
521
|
+
raise exception.RucioException(error)
|
|
522
|
+
|
|
523
|
+
def __gfal2_rename(self, path, new_path):
|
|
524
|
+
"""
|
|
525
|
+
Uses gfal2 to rename a file.
|
|
526
|
+
|
|
527
|
+
:param path: path to the current file on the storage
|
|
528
|
+
:param new_path: path to the new file on the storage
|
|
529
|
+
|
|
530
|
+
:returns: 0 if it exists, -1 if it doesn't
|
|
531
|
+
|
|
532
|
+
:raises RucioException: if failed.
|
|
533
|
+
"""
|
|
534
|
+
ctx = self.__ctx
|
|
535
|
+
|
|
536
|
+
try:
|
|
537
|
+
dir_name = os.path.dirname(new_path)
|
|
538
|
+
# This function will be removed soon. gfal2 will create parent dir automatically.
|
|
539
|
+
try:
|
|
540
|
+
ctx.mkdir_rec(str(dir_name), 0o775)
|
|
541
|
+
except Exception:
|
|
542
|
+
pass
|
|
543
|
+
ret = ctx.rename(str(path), str(new_path))
|
|
544
|
+
return ret
|
|
545
|
+
except gfal2.GError as error: # pylint: disable=no-member
|
|
546
|
+
if error.code == errno.ENOENT or 'No such file' in str(error):
|
|
547
|
+
raise exception.SourceNotFound(error)
|
|
548
|
+
raise exception.RucioException(error)
|
|
549
|
+
|
|
550
|
+
def get_space_usage(self):
|
|
551
|
+
"""
|
|
552
|
+
Get RSE space usage information.
|
|
553
|
+
|
|
554
|
+
:returns: a list with dict containing 'totalsize' and 'unusedsize'
|
|
555
|
+
|
|
556
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
557
|
+
"""
|
|
558
|
+
endpoint_basepath = self.path2pfn(self.attributes['prefix'])
|
|
559
|
+
self.logger(logging.DEBUG, 'getting space usage from {}'.format(endpoint_basepath))
|
|
560
|
+
|
|
561
|
+
space_token = None
|
|
562
|
+
if self.attributes['extended_attributes'] is not None and 'space_token' in list(self.attributes['extended_attributes'].keys()):
|
|
563
|
+
space_token = self.attributes['extended_attributes']['space_token']
|
|
564
|
+
|
|
565
|
+
if space_token is None or space_token == "":
|
|
566
|
+
raise exception.RucioException("Space token is not defined for protocol: %s" % (self.attributes['scheme']))
|
|
567
|
+
|
|
568
|
+
try:
|
|
569
|
+
totalsize, unusedsize = self.__gfal2_get_space_usage(endpoint_basepath, space_token)
|
|
570
|
+
return totalsize, unusedsize
|
|
571
|
+
except Exception as error:
|
|
572
|
+
raise exception.ServiceUnavailable(error)
|
|
573
|
+
|
|
574
|
+
def __gfal2_get_space_usage(self, path, space_token):
|
|
575
|
+
"""
|
|
576
|
+
Uses gfal2 to get space usage info with space token.
|
|
577
|
+
|
|
578
|
+
:param path: the endpoint path
|
|
579
|
+
:param space_token: a string space token. E.g. "ATLASDATADISK"
|
|
580
|
+
|
|
581
|
+
:returns: a list with dict containing 'totalsize' and 'unusedsize'
|
|
582
|
+
|
|
583
|
+
:raises ServiceUnavailable: if failed.
|
|
584
|
+
"""
|
|
585
|
+
ctx = self.__ctx
|
|
586
|
+
|
|
587
|
+
try:
|
|
588
|
+
ret_usage = ctx.getxattr(str(path), str("spacetoken.description?" + space_token))
|
|
589
|
+
usage = json.loads(ret_usage)
|
|
590
|
+
totalsize = usage[0]["totalsize"]
|
|
591
|
+
unusedsize = usage[0]["unusedsize"]
|
|
592
|
+
return totalsize, unusedsize
|
|
593
|
+
except gfal2.GError as error: # pylint: disable=no-member
|
|
594
|
+
raise Exception(str(error))
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class NoRename(Default):
|
|
598
|
+
|
|
599
|
+
""" Implementing access to RSEs using the srm protocol without renaming files on upload/download. Necessary for some storage endpoints. """
|
|
600
|
+
|
|
601
|
+
def __init__(self, protocol_attr, rse_settings, logger=logging.log):
|
|
602
|
+
""" Initializes the object with information about the referred RSE.
|
|
603
|
+
|
|
604
|
+
:param protocol_attr: Properties of the requested protocol.
|
|
605
|
+
:param rse_settting: The RSE settings.
|
|
606
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
607
|
+
"""
|
|
608
|
+
super(NoRename, self).__init__(protocol_attr, rse_settings, logger=logger)
|
|
609
|
+
self.renaming = False
|
|
610
|
+
self.attributes.pop('determinism_type', None)
|
|
611
|
+
self.files = []
|
|
612
|
+
|
|
613
|
+
def rename(self, pfn, new_pfn):
|
|
614
|
+
""" Allows to rename a file stored inside the connected RSE.
|
|
615
|
+
|
|
616
|
+
:param pfn: Current physical file name
|
|
617
|
+
:param new_pfn New physical file name
|
|
618
|
+
|
|
619
|
+
:raises DestinationNotAccessible, ServiceUnavailable, SourceNotFound
|
|
620
|
+
"""
|
|
621
|
+
raise NotImplementedError
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
class CLI(Default):
|
|
625
|
+
|
|
626
|
+
""" Implementing access to RSEs using the srm protocol through CLI with 'gfal' commands. """
|
|
627
|
+
|
|
628
|
+
def __init__(self, protocol_attr, rse_settings, logger=logging.log):
|
|
629
|
+
""" Initializes the object with information about the referred RSE.
|
|
630
|
+
|
|
631
|
+
:param props: Properties derived from the RSE Repository
|
|
632
|
+
"""
|
|
633
|
+
|
|
634
|
+
super(CLI, self).__init__(protocol_attr, rse_settings, logger=logger)
|
|
635
|
+
if not logger:
|
|
636
|
+
logger = logging.getLogger('%s.null' % __name__)
|
|
637
|
+
self.logger = logger
|
|
638
|
+
|
|
639
|
+
def get(self, path, dest, transfer_timeout=None):
|
|
640
|
+
"""
|
|
641
|
+
Provides access to files stored inside connected the RSE.
|
|
642
|
+
|
|
643
|
+
:param path: Physical file name of requested file
|
|
644
|
+
:param dest: Name and path of the files when stored at the client
|
|
645
|
+
:param transfer_timeout: Transfer timeout (in seconds)
|
|
646
|
+
|
|
647
|
+
:raises RucioException: Passthrough of gfal-copy error message.
|
|
648
|
+
"""
|
|
649
|
+
|
|
650
|
+
dest = os.path.abspath(dest)
|
|
651
|
+
if ':' not in dest:
|
|
652
|
+
dest = "file://" + dest
|
|
653
|
+
|
|
654
|
+
cmd = 'gfal-copy -vf -p -t %s -T %s %s %s' % (transfer_timeout, transfer_timeout, path, dest)
|
|
655
|
+
self.logger(logging.DEBUG, 'Command: ' + cmd)
|
|
656
|
+
cmd = cmd.split()
|
|
657
|
+
|
|
658
|
+
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
659
|
+
stdout, stderr = p.communicate()
|
|
660
|
+
|
|
661
|
+
if p.returncode:
|
|
662
|
+
self.logger(logging.DEBUG, 'Error STDOUT: ' + str(stdout))
|
|
663
|
+
self.logger(logging.DEBUG, 'Error STDERR: ' + str(stderr))
|
|
664
|
+
raise exception.RucioException(str(stderr))
|
|
665
|
+
|
|
666
|
+
def put(self, source, target, source_dir, transfer_timeout=None):
|
|
667
|
+
"""
|
|
668
|
+
Allows to store files inside the referred RSE.
|
|
669
|
+
|
|
670
|
+
:param source: path to the source file on the client file system
|
|
671
|
+
:param target: path to the destination file on the storage
|
|
672
|
+
:param source_dir: Path where the to be transferred files are stored in the local file system
|
|
673
|
+
:param transfer_timeout: Transfer timeout (in seconds)
|
|
674
|
+
|
|
675
|
+
:raises RucioException: Passthrough of gfal-copy error message.
|
|
676
|
+
"""
|
|
677
|
+
|
|
678
|
+
source_dir = source_dir or '.'
|
|
679
|
+
source_url = '%s/%s' % (source_dir, source)
|
|
680
|
+
self.logger(logging.DEBUG, 'source: ' + str(source_url))
|
|
681
|
+
source_url = os.path.abspath(source_url)
|
|
682
|
+
if not os.path.exists(source_url):
|
|
683
|
+
raise exception.SourceNotFound()
|
|
684
|
+
if ':' not in source_url:
|
|
685
|
+
source_url = "file://" + source_url
|
|
686
|
+
|
|
687
|
+
cmd = 'gfal-copy -vf -p -t %s -T %s %s %s ' % (transfer_timeout, transfer_timeout, source, target)
|
|
688
|
+
|
|
689
|
+
space_token = None
|
|
690
|
+
if self.attributes['extended_attributes'] is not None and 'space_token' in list(self.attributes['extended_attributes'].keys()):
|
|
691
|
+
space_token = self.attributes['extended_attributes']['space_token']
|
|
692
|
+
cmd = 'gfal-copy -vf -p -t %s -T %s -S %s %s %s ' % (transfer_timeout, transfer_timeout, space_token, source, target)
|
|
693
|
+
|
|
694
|
+
self.logger(logging.DEBUG, 'Command: ' + cmd)
|
|
695
|
+
cmd = cmd.split()
|
|
696
|
+
|
|
697
|
+
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
698
|
+
stdout, stderr = p.communicate()
|
|
699
|
+
|
|
700
|
+
if p.returncode:
|
|
701
|
+
self.logger(logging.DEBUG, 'Error STDOUT: ' + str(stdout))
|
|
702
|
+
self.logger(logging.DEBUG, 'Error STDERR: ' + str(stderr))
|
|
703
|
+
raise exception.RucioException(str(stderr))
|