rucio-clients 32.8.6__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.
- rucio/__init__.py +18 -0
- rucio/alembicrevision.py +16 -0
- rucio/client/__init__.py +16 -0
- rucio/client/accountclient.py +413 -0
- rucio/client/accountlimitclient.py +155 -0
- rucio/client/baseclient.py +929 -0
- rucio/client/client.py +77 -0
- rucio/client/configclient.py +113 -0
- rucio/client/credentialclient.py +54 -0
- rucio/client/didclient.py +691 -0
- rucio/client/diracclient.py +48 -0
- rucio/client/downloadclient.py +1674 -0
- rucio/client/exportclient.py +44 -0
- rucio/client/fileclient.py +51 -0
- rucio/client/importclient.py +42 -0
- rucio/client/lifetimeclient.py +74 -0
- rucio/client/lockclient.py +99 -0
- rucio/client/metaclient.py +137 -0
- rucio/client/pingclient.py +45 -0
- rucio/client/replicaclient.py +444 -0
- rucio/client/requestclient.py +109 -0
- rucio/client/rseclient.py +664 -0
- rucio/client/ruleclient.py +287 -0
- rucio/client/scopeclient.py +88 -0
- rucio/client/subscriptionclient.py +161 -0
- rucio/client/touchclient.py +78 -0
- rucio/client/uploadclient.py +871 -0
- rucio/common/__init__.py +14 -0
- rucio/common/cache.py +74 -0
- rucio/common/config.py +796 -0
- rucio/common/constants.py +92 -0
- rucio/common/constraints.py +18 -0
- rucio/common/didtype.py +187 -0
- rucio/common/exception.py +1092 -0
- rucio/common/extra.py +37 -0
- rucio/common/logging.py +404 -0
- rucio/common/pcache.py +1387 -0
- rucio/common/policy.py +84 -0
- rucio/common/schema/__init__.py +143 -0
- rucio/common/schema/atlas.py +411 -0
- rucio/common/schema/belleii.py +406 -0
- rucio/common/schema/cms.py +478 -0
- rucio/common/schema/domatpc.py +399 -0
- rucio/common/schema/escape.py +424 -0
- rucio/common/schema/generic.py +431 -0
- rucio/common/schema/generic_multi_vo.py +410 -0
- rucio/common/schema/icecube.py +404 -0
- rucio/common/schema/lsst.py +423 -0
- rucio/common/stomp_utils.py +160 -0
- rucio/common/stopwatch.py +56 -0
- rucio/common/test_rucio_server.py +148 -0
- rucio/common/types.py +158 -0
- rucio/common/utils.py +1946 -0
- rucio/rse/__init__.py +97 -0
- rucio/rse/protocols/__init__.py +14 -0
- rucio/rse/protocols/cache.py +123 -0
- rucio/rse/protocols/dummy.py +112 -0
- rucio/rse/protocols/gfal.py +701 -0
- rucio/rse/protocols/globus.py +243 -0
- rucio/rse/protocols/gsiftp.py +93 -0
- rucio/rse/protocols/http_cache.py +83 -0
- rucio/rse/protocols/mock.py +124 -0
- rucio/rse/protocols/ngarc.py +210 -0
- rucio/rse/protocols/posix.py +251 -0
- rucio/rse/protocols/protocol.py +530 -0
- rucio/rse/protocols/rclone.py +365 -0
- rucio/rse/protocols/rfio.py +137 -0
- rucio/rse/protocols/srm.py +339 -0
- rucio/rse/protocols/ssh.py +414 -0
- rucio/rse/protocols/storm.py +207 -0
- rucio/rse/protocols/webdav.py +547 -0
- rucio/rse/protocols/xrootd.py +295 -0
- rucio/rse/rsemanager.py +752 -0
- rucio/vcsversion.py +11 -0
- rucio/version.py +46 -0
- rucio_clients-32.8.6.data/data/etc/rse-accounts.cfg.template +25 -0
- rucio_clients-32.8.6.data/data/etc/rucio.cfg.atlas.client.template +42 -0
- rucio_clients-32.8.6.data/data/etc/rucio.cfg.template +257 -0
- rucio_clients-32.8.6.data/data/requirements.txt +55 -0
- rucio_clients-32.8.6.data/data/rucio_client/merge_rucio_configs.py +147 -0
- rucio_clients-32.8.6.data/scripts/rucio +2540 -0
- rucio_clients-32.8.6.data/scripts/rucio-admin +2434 -0
- rucio_clients-32.8.6.dist-info/METADATA +50 -0
- rucio_clients-32.8.6.dist-info/RECORD +88 -0
- rucio_clients-32.8.6.dist-info/WHEEL +5 -0
- rucio_clients-32.8.6.dist-info/licenses/AUTHORS.rst +94 -0
- rucio_clients-32.8.6.dist-info/licenses/LICENSE +201 -0
- rucio_clients-32.8.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright European Organization for Nuclear Research (CERN) since 2012
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
from rucio.common import exception
|
|
21
|
+
from rucio.common.config import get_config_dirs
|
|
22
|
+
from rucio.common.utils import execute, PREFERRED_CHECKSUM
|
|
23
|
+
from rucio.rse.protocols import protocol
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_conf_file(file_name):
|
|
27
|
+
config_dir = next(filter(lambda d: os.path.exists(os.path.join(d, file_name)), get_config_dirs()))
|
|
28
|
+
with open(os.path.join(config_dir, file_name)) as f:
|
|
29
|
+
return json.load(f)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Default(protocol.RSEProtocol):
|
|
33
|
+
""" Implementing access to RSEs using the rclone protocol."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, protocol_attr, rse_settings, logger=logging.log):
|
|
36
|
+
""" Initializes the object with information about the referred RSE.
|
|
37
|
+
|
|
38
|
+
:param props: Properties derived from the RSE Repository
|
|
39
|
+
"""
|
|
40
|
+
super(Default, self).__init__(protocol_attr, rse_settings, logger=logger)
|
|
41
|
+
if len(rse_settings['protocols']) == 1:
|
|
42
|
+
raise exception.RucioException('rclone initialization requires at least one other protocol defined on the RSE. (from ssh, sftp, posix, webdav)')
|
|
43
|
+
self.scheme = self.attributes['scheme']
|
|
44
|
+
setuprclone = False
|
|
45
|
+
for protocols in reversed(rse_settings['protocols']):
|
|
46
|
+
if protocol_attr['impl'] == protocols['impl']:
|
|
47
|
+
continue
|
|
48
|
+
else:
|
|
49
|
+
setuprclone = self.setuphostname(protocols)
|
|
50
|
+
if setuprclone:
|
|
51
|
+
break
|
|
52
|
+
|
|
53
|
+
if not setuprclone:
|
|
54
|
+
raise exception.RucioException('rclone could not be initialized.')
|
|
55
|
+
self.logger = logger
|
|
56
|
+
|
|
57
|
+
def setuphostname(self, protocols):
|
|
58
|
+
""" Initializes the rclone object with information about protocols in the referred RSE.
|
|
59
|
+
|
|
60
|
+
:param protocols: Protocols in the RSE
|
|
61
|
+
"""
|
|
62
|
+
if protocols['scheme'] in ['scp', 'rsync', 'sftp']:
|
|
63
|
+
self.hostname = 'ssh_rclone_rse'
|
|
64
|
+
self.host = protocols['hostname']
|
|
65
|
+
self.port = str(protocols['port'])
|
|
66
|
+
if protocols['extended_attributes'] is not None and 'user' in list(protocols['extended_attributes'].keys()):
|
|
67
|
+
self.user = protocols['extended_attributes']['user']
|
|
68
|
+
else:
|
|
69
|
+
self.user = None
|
|
70
|
+
try:
|
|
71
|
+
data = load_conf_file('rclone-init.cfg')
|
|
72
|
+
key_file = data[self.host + '_ssh']['key_file']
|
|
73
|
+
except KeyError:
|
|
74
|
+
self.logger(logging.ERROR, 'rclone.init: rclone-init.cfg:- Field value missing for "{}_ssh: key_file"'.format(self.host))
|
|
75
|
+
return False
|
|
76
|
+
try:
|
|
77
|
+
if self.user:
|
|
78
|
+
cmd = 'rclone config create {0} sftp host {1} user {2} port {3} key_file {4}'.format(self.hostname, self.host, self.user, str(self.port), key_file)
|
|
79
|
+
self.logger(logging.DEBUG, 'rclone.init: cmd: {}'.format(cmd))
|
|
80
|
+
status, out, err = execute(cmd)
|
|
81
|
+
if status:
|
|
82
|
+
return False
|
|
83
|
+
else:
|
|
84
|
+
cmd = 'rclone config create {0} sftp host {1} port {2} key_file {3}'.format(self.hostname, self.host, str(self.port), key_file)
|
|
85
|
+
self.logger(logging.DEBUG, 'rclone.init: cmd: {}'.format(cmd))
|
|
86
|
+
status, out, err = execute(cmd)
|
|
87
|
+
if status:
|
|
88
|
+
return False
|
|
89
|
+
except Exception as e:
|
|
90
|
+
raise exception.ServiceUnavailable(e)
|
|
91
|
+
|
|
92
|
+
elif protocols['scheme'] == 'file':
|
|
93
|
+
self.hostname = '%s_rclone_rse' % (protocols['scheme'])
|
|
94
|
+
self.host = 'localhost'
|
|
95
|
+
try:
|
|
96
|
+
cmd = 'rclone config create {0} local'.format(self.hostname)
|
|
97
|
+
self.logger(logging.DEBUG, 'rclone.init: cmd: {}'.format(cmd))
|
|
98
|
+
status, out, err = execute(cmd)
|
|
99
|
+
if status:
|
|
100
|
+
return False
|
|
101
|
+
except Exception as e:
|
|
102
|
+
raise exception.ServiceUnavailable(e)
|
|
103
|
+
|
|
104
|
+
elif protocols['scheme'] in ['davs', 'https']:
|
|
105
|
+
self.hostname = '%s_rclone_rse' % (protocols['scheme'])
|
|
106
|
+
self.host = protocols['hostname']
|
|
107
|
+
url = '%s://%s:%s%s' % (protocols['scheme'], protocols['hostname'], str(protocols['port']), protocols['prefix'])
|
|
108
|
+
try:
|
|
109
|
+
data = load_conf_file('rclone-init.cfg')
|
|
110
|
+
bearer_token = data[self.host + '_webdav']['bearer_token']
|
|
111
|
+
except KeyError:
|
|
112
|
+
self.logger(logging.ERROR, 'rclone.init: rclone-init.cfg:- Field value missing for "{}_webdav: bearer_token"'.format(self.host))
|
|
113
|
+
return False
|
|
114
|
+
try:
|
|
115
|
+
cmd = 'rclone config create {0} webdav url {1} vendor other bearer_token {2}'.format(self.hostname, url, bearer_token)
|
|
116
|
+
self.logger(logging.DEBUG, 'rclone.init: cmd: {}'.format(cmd))
|
|
117
|
+
status, out, err = execute(cmd)
|
|
118
|
+
if status:
|
|
119
|
+
return False
|
|
120
|
+
except Exception as e:
|
|
121
|
+
raise exception.ServiceUnavailable(e)
|
|
122
|
+
|
|
123
|
+
else:
|
|
124
|
+
self.logger(logging.DEBUG, 'rclone.init: {} protocol impl not supported by rucio rclone'.format(protocols['impl']))
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
def path2pfn(self, path):
|
|
130
|
+
"""
|
|
131
|
+
Returns a fully qualified PFN for the file referred by path.
|
|
132
|
+
|
|
133
|
+
:param path: The path to the file.
|
|
134
|
+
|
|
135
|
+
:returns: Fully qualified PFN.
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
self.logger(logging.DEBUG, 'rclone.path2pfn: path: {}'.format(path))
|
|
139
|
+
if not path.startswith('rclone://'):
|
|
140
|
+
return '%s://%s/%s' % (self.scheme, self.host, path)
|
|
141
|
+
else:
|
|
142
|
+
return path
|
|
143
|
+
|
|
144
|
+
def exists(self, pfn):
|
|
145
|
+
""" Checks if the requested file is known by the referred RSE.
|
|
146
|
+
|
|
147
|
+
:param pfn: Physical file name
|
|
148
|
+
|
|
149
|
+
:returns: True if the file exists, False if it doesn't
|
|
150
|
+
|
|
151
|
+
:raise ServiceUnavailable
|
|
152
|
+
"""
|
|
153
|
+
self.logger(logging.DEBUG, 'rclone.exists: pfn: {}'.format(pfn))
|
|
154
|
+
try:
|
|
155
|
+
path = self.pfn2path(pfn)
|
|
156
|
+
cmd = 'rclone lsf %s:%s' % (self.hostname, path)
|
|
157
|
+
self.logger(logging.DEBUG, 'rclone.exists: cmd: {}'.format(cmd))
|
|
158
|
+
status, out, err = execute(cmd)
|
|
159
|
+
if status:
|
|
160
|
+
return False
|
|
161
|
+
except Exception as e:
|
|
162
|
+
raise exception.ServiceUnavailable(e)
|
|
163
|
+
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
def stat(self, path):
|
|
167
|
+
"""
|
|
168
|
+
Returns the stats of a file.
|
|
169
|
+
|
|
170
|
+
:param path: path to file
|
|
171
|
+
|
|
172
|
+
:raises ServiceUnavailable: if some generic error occured in the library.
|
|
173
|
+
|
|
174
|
+
:returns: a dict with two keys, filesize and an element of GLOBALLY_SUPPORTED_CHECKSUMS.
|
|
175
|
+
"""
|
|
176
|
+
self.logger(logging.DEBUG, 'rclone.stat: path: {}'.format(path))
|
|
177
|
+
ret = {}
|
|
178
|
+
chsum = None
|
|
179
|
+
if path.startswith('rclone://'):
|
|
180
|
+
path = self.pfn2path(path)
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
# rclone stat for getting filesize
|
|
184
|
+
cmd = 'rclone size {0}:{1}'.format(self.hostname, path)
|
|
185
|
+
self.logger(logging.DEBUG, 'rclone.stat: filesize cmd: {}'.format(cmd))
|
|
186
|
+
status_stat, out, err = execute(cmd)
|
|
187
|
+
if status_stat == 0:
|
|
188
|
+
fsize = (out.split('\n')[1]).split(' ')[4][1:]
|
|
189
|
+
ret['filesize'] = fsize
|
|
190
|
+
|
|
191
|
+
# rclone query checksum for getting md5 checksum
|
|
192
|
+
cmd = 'rclone md5sum %s:%s' % (self.hostname, path)
|
|
193
|
+
self.logger(logging.DEBUG, 'rclone.stat: checksum cmd: {}'.format(cmd))
|
|
194
|
+
status_query, out, err = execute(cmd)
|
|
195
|
+
|
|
196
|
+
if status_query == 0:
|
|
197
|
+
chsum = 'md5'
|
|
198
|
+
val = out.strip(' ').split()
|
|
199
|
+
ret[chsum] = val[0]
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
raise exception.ServiceUnavailable(e)
|
|
203
|
+
|
|
204
|
+
if 'filesize' not in ret:
|
|
205
|
+
raise exception.ServiceUnavailable('Filesize could not be retrieved.')
|
|
206
|
+
if PREFERRED_CHECKSUM != chsum or not chsum:
|
|
207
|
+
msg = '{} does not match with {}'.format(chsum, PREFERRED_CHECKSUM)
|
|
208
|
+
raise exception.RSEChecksumUnavailable(msg)
|
|
209
|
+
|
|
210
|
+
return ret
|
|
211
|
+
|
|
212
|
+
def pfn2path(self, pfn):
|
|
213
|
+
"""
|
|
214
|
+
Returns the path of a file given the pfn, i.e. scheme, user and hostname are subtracted from the pfn.
|
|
215
|
+
|
|
216
|
+
:param path: pfn of a file
|
|
217
|
+
|
|
218
|
+
:returns: path.
|
|
219
|
+
"""
|
|
220
|
+
path = pfn
|
|
221
|
+
if pfn.startswith('rclone://'):
|
|
222
|
+
self.logger(logging.DEBUG, 'rclone.pfn2path: pfn: {}'.format(pfn))
|
|
223
|
+
prefix = self.attributes['prefix']
|
|
224
|
+
path = pfn.partition(self.attributes['prefix'])[2]
|
|
225
|
+
path = prefix + path
|
|
226
|
+
return path
|
|
227
|
+
|
|
228
|
+
def lfns2pfns(self, lfns):
|
|
229
|
+
"""
|
|
230
|
+
Returns a fully qualified PFN for the file referred by path.
|
|
231
|
+
|
|
232
|
+
:param path: The path to the file.
|
|
233
|
+
|
|
234
|
+
:returns: Fully qualified PFN.
|
|
235
|
+
"""
|
|
236
|
+
self.logger(logging.DEBUG, 'rclone.lfns2pfns: lfns: {}'.format(lfns))
|
|
237
|
+
pfns = {}
|
|
238
|
+
prefix = self.attributes['prefix']
|
|
239
|
+
|
|
240
|
+
if not prefix.startswith('/'):
|
|
241
|
+
prefix = ''.join(['/', prefix])
|
|
242
|
+
if not prefix.endswith('/'):
|
|
243
|
+
prefix = ''.join([prefix, '/'])
|
|
244
|
+
|
|
245
|
+
lfns = [lfns] if type(lfns) == dict else lfns
|
|
246
|
+
for lfn in lfns:
|
|
247
|
+
scope, name = lfn['scope'], lfn['name']
|
|
248
|
+
if 'path' in lfn and lfn['path'] is not None:
|
|
249
|
+
pfns['%s:%s' % (scope, name)] = ''.join([self.attributes['scheme'], '://', self.host, ':', prefix, lfn['path']])
|
|
250
|
+
else:
|
|
251
|
+
pfns['%s:%s' % (scope, name)] = ''.join([self.attributes['scheme'], '://', self.host, ':', prefix, self._get_path(scope=scope, name=name)])
|
|
252
|
+
return pfns
|
|
253
|
+
|
|
254
|
+
def connect(self):
|
|
255
|
+
""" Establishes the actual connection to the referred RSE.
|
|
256
|
+
|
|
257
|
+
:raises RSEAccessDenied
|
|
258
|
+
"""
|
|
259
|
+
self.logger(logging.DEBUG, 'rclone.connect: hostname {}'.format(self.hostname))
|
|
260
|
+
try:
|
|
261
|
+
cmd = 'rclone lsd %s:' % (self.hostname)
|
|
262
|
+
status, out, err = execute(cmd)
|
|
263
|
+
if status:
|
|
264
|
+
raise exception.RSEAccessDenied(err)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
raise exception.RSEAccessDenied(e)
|
|
267
|
+
|
|
268
|
+
def close(self):
|
|
269
|
+
""" Closes the connection to RSE."""
|
|
270
|
+
pass
|
|
271
|
+
|
|
272
|
+
def get(self, pfn, dest, transfer_timeout=None):
|
|
273
|
+
""" Provides access to files stored inside connected the RSE.
|
|
274
|
+
|
|
275
|
+
:param pfn: Physical file name of requested file
|
|
276
|
+
:param dest: Name and path of the files when stored at the client
|
|
277
|
+
:param transfer_timeout: Transfer timeout (in seconds) - dummy
|
|
278
|
+
|
|
279
|
+
:raises DestinationNotAccessible, ServiceUnavailable, SourceNotFound
|
|
280
|
+
"""
|
|
281
|
+
self.logger(logging.DEBUG, 'rclone.get: pfn: {}'.format(pfn))
|
|
282
|
+
try:
|
|
283
|
+
path = self.pfn2path(pfn)
|
|
284
|
+
cmd = 'rclone copyto %s:%s %s' % (self.hostname, path, dest)
|
|
285
|
+
self.logger(logging.DEBUG, 'rclone.get: cmd: {}'.format(cmd))
|
|
286
|
+
status, out, err = execute(cmd)
|
|
287
|
+
if status:
|
|
288
|
+
raise exception.RucioException(err)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
raise exception.ServiceUnavailable(e)
|
|
291
|
+
|
|
292
|
+
def put(self, filename, target, source_dir, transfer_timeout=None):
|
|
293
|
+
"""
|
|
294
|
+
Allows to store files inside the referred RSE.
|
|
295
|
+
|
|
296
|
+
:param source: path to the source file on the client file system
|
|
297
|
+
:param target: path to the destination file on the storage
|
|
298
|
+
:param source_dir: Path where the to be transferred files are stored in the local file system
|
|
299
|
+
:param transfer_timeout: Transfer timeout (in seconds) - dummy
|
|
300
|
+
|
|
301
|
+
:raises DestinationNotAccessible: if the destination storage was not accessible.
|
|
302
|
+
:raises ServiceUnavailable: if some generic error occured in the library.
|
|
303
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
304
|
+
"""
|
|
305
|
+
self.logger(logging.DEBUG, 'rclone.put: filename: {} target: {}'.format(filename, target))
|
|
306
|
+
source_dir = source_dir or '.'
|
|
307
|
+
source_url = '%s/%s' % (source_dir, filename)
|
|
308
|
+
self.logger(logging.DEBUG, 'rclone.put: source url: {}'.format(source_url))
|
|
309
|
+
|
|
310
|
+
path = self.pfn2path(target)
|
|
311
|
+
if not os.path.exists(source_url):
|
|
312
|
+
raise exception.SourceNotFound()
|
|
313
|
+
try:
|
|
314
|
+
cmd = 'rclone copyto %s %s:%s' % (source_url, self.hostname, path)
|
|
315
|
+
self.logger(logging.DEBUG, 'rclone.put: cmd: {}'.format(cmd))
|
|
316
|
+
status, out, err = execute(cmd)
|
|
317
|
+
if status:
|
|
318
|
+
raise exception.RucioException(err)
|
|
319
|
+
except Exception as e:
|
|
320
|
+
raise exception.ServiceUnavailable(e)
|
|
321
|
+
|
|
322
|
+
def delete(self, pfn):
|
|
323
|
+
"""
|
|
324
|
+
Deletes a file from the connected RSE.
|
|
325
|
+
|
|
326
|
+
:param pfn: Physical file name
|
|
327
|
+
|
|
328
|
+
:raises ServiceUnavailable: if some generic error occured in the library.
|
|
329
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
330
|
+
"""
|
|
331
|
+
self.logger(logging.DEBUG, 'rclone.delete: pfn: {}'.format(pfn))
|
|
332
|
+
if not self.exists(pfn):
|
|
333
|
+
raise exception.SourceNotFound()
|
|
334
|
+
try:
|
|
335
|
+
path = self.pfn2path(pfn)
|
|
336
|
+
cmd = 'rclone delete %s:%s' % (self.hostname, path)
|
|
337
|
+
self.logger(logging.DEBUG, 'rclone.delete: cmd: {}'.format(cmd))
|
|
338
|
+
status, out, err = execute(cmd)
|
|
339
|
+
if status != 0:
|
|
340
|
+
raise exception.RucioException(err)
|
|
341
|
+
except Exception as e:
|
|
342
|
+
raise exception.ServiceUnavailable(e)
|
|
343
|
+
|
|
344
|
+
def rename(self, pfn, new_pfn):
|
|
345
|
+
""" Allows to rename a file stored inside the connected RSE.
|
|
346
|
+
|
|
347
|
+
:param pfn: Current physical file name
|
|
348
|
+
:param new_pfn New physical file name
|
|
349
|
+
:raises DestinationNotAccessible: if the destination storage was not accessible.
|
|
350
|
+
:raises ServiceUnavailable: if some generic error occured in the library.
|
|
351
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
352
|
+
"""
|
|
353
|
+
self.logger(logging.DEBUG, 'rclone.rename: pfn: {}'.format(pfn))
|
|
354
|
+
if not self.exists(pfn):
|
|
355
|
+
raise exception.SourceNotFound()
|
|
356
|
+
try:
|
|
357
|
+
path = self.pfn2path(pfn)
|
|
358
|
+
new_path = self.pfn2path(new_pfn)
|
|
359
|
+
cmd = 'rclone moveto %s:%s %s:%s' % (self.hostname, path, self.hostname, new_path)
|
|
360
|
+
self.logger(logging.DEBUG, 'rclone.stat: rename cmd: {}'.format(cmd))
|
|
361
|
+
status, out, err = execute(cmd)
|
|
362
|
+
if status != 0:
|
|
363
|
+
raise exception.RucioException(err)
|
|
364
|
+
except Exception as e:
|
|
365
|
+
raise exception.ServiceUnavailable(e)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright European Organization for Nuclear Research (CERN) since 2012
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
RFIO protocol
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from os.path import dirname
|
|
22
|
+
from urllib.parse import urlparse
|
|
23
|
+
|
|
24
|
+
from rucio.common import exception
|
|
25
|
+
from rucio.common.utils import execute
|
|
26
|
+
from rucio.rse.protocols import protocol
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Default(protocol.RSEProtocol):
|
|
30
|
+
""" Implementing access to RSEs using the RFIO protocol. """
|
|
31
|
+
|
|
32
|
+
def connect(self, credentials):
|
|
33
|
+
"""
|
|
34
|
+
Establishes the actual connection to the referred RSE.
|
|
35
|
+
|
|
36
|
+
:param credentials: needed to establish a connection with the stroage.
|
|
37
|
+
|
|
38
|
+
:raises RSEAccessDenied: if no connection could be established.
|
|
39
|
+
"""
|
|
40
|
+
extended_attributes = self.rse['protocol']['extended_attributes']
|
|
41
|
+
if 'STAGE_SVCCLASS' in extended_attributes:
|
|
42
|
+
os.environ['STAGE_SVCCLASS'] = extended_attributes['STAGE_SVCCLASS']
|
|
43
|
+
|
|
44
|
+
def path2pfn(self, path):
|
|
45
|
+
"""
|
|
46
|
+
Retruns a fully qualified PFN for the file referred by path.
|
|
47
|
+
|
|
48
|
+
:param path: The path to the file.
|
|
49
|
+
|
|
50
|
+
:returns: Fully qualified PFN.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
return ''.join([self.rse['scheme'], '://', path])
|
|
54
|
+
|
|
55
|
+
def exists(self, path):
|
|
56
|
+
"""
|
|
57
|
+
Checks if the requested file is known by the referred RSE.
|
|
58
|
+
|
|
59
|
+
:param path: Physical file name
|
|
60
|
+
|
|
61
|
+
:returns: True if the file exists, False if it doesn't
|
|
62
|
+
|
|
63
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
64
|
+
"""
|
|
65
|
+
cmd = f'rfstat {path}'
|
|
66
|
+
status, out, err = execute(cmd)
|
|
67
|
+
return status == 0
|
|
68
|
+
|
|
69
|
+
def close(self):
|
|
70
|
+
""" Closes the connection to RSE."""
|
|
71
|
+
if 'STAGE_SVCCLASS' in os.environ:
|
|
72
|
+
del os.environ['STAGE_SVCCLASS']
|
|
73
|
+
|
|
74
|
+
def put(self, source, target, source_dir, transfer_timeout=None):
|
|
75
|
+
"""
|
|
76
|
+
Allows to store files inside the referred RSE.
|
|
77
|
+
|
|
78
|
+
:param source: path to the source file on the client file system
|
|
79
|
+
:param target: path to the destination file on the storage
|
|
80
|
+
:param source_dir: Path where the to be transferred files are stored in the local file system
|
|
81
|
+
:param transfer_timeout: Transfer timeout (in seconds) - dummy
|
|
82
|
+
|
|
83
|
+
:raises DestinationNotAccessible: if the destination storage was not accessible.
|
|
84
|
+
:raises ServiceUnavailable: if some generic error occured in the library.
|
|
85
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
86
|
+
"""
|
|
87
|
+
if not self.exists(dirname(target)):
|
|
88
|
+
self.mkdir(dirname(target))
|
|
89
|
+
|
|
90
|
+
cmd = f'rfcp {source} {target}'
|
|
91
|
+
status, out, err = execute(cmd)
|
|
92
|
+
return status == 0
|
|
93
|
+
|
|
94
|
+
def mkdir(self, directory):
|
|
95
|
+
""" Create new directory. """
|
|
96
|
+
cmd = f'rfmkdir -p {directory}'
|
|
97
|
+
status, out, err = execute(cmd)
|
|
98
|
+
return status == 0
|
|
99
|
+
|
|
100
|
+
def split_pfn(self, pfn):
|
|
101
|
+
"""
|
|
102
|
+
Splits the given PFN into the parts known by the protocol. During parsing the PFN is also checked for
|
|
103
|
+
validity on the given RSE with the given protocol.
|
|
104
|
+
|
|
105
|
+
:param pfn: a fully qualified PFN
|
|
106
|
+
|
|
107
|
+
:returns: a dict containing all known parts of the PFN for the protocol e.g. scheme, hostname, port, prefix, path, filename
|
|
108
|
+
|
|
109
|
+
:raises RSEFileNameNotSupported: if the provided PFN doesn't match with the protocol settings
|
|
110
|
+
"""
|
|
111
|
+
parsed = urlparse(pfn)
|
|
112
|
+
ret = dict()
|
|
113
|
+
ret['scheme'] = parsed.scheme
|
|
114
|
+
ret['hostname'] = parsed.netloc.partition(':')[0]
|
|
115
|
+
ret['port'] = int(parsed.netloc.partition(':')[2]) if parsed.netloc.partition(':')[2] != '' else 0
|
|
116
|
+
ret['path'] = parsed.path
|
|
117
|
+
|
|
118
|
+
# Protect against 'lazy' defined prefixes for RSEs in the repository
|
|
119
|
+
self.rse['prefix'] = '' if self.rse['prefix'] is None else self.rse['prefix']
|
|
120
|
+
if not self.rse['prefix'].startswith('/'):
|
|
121
|
+
self.rse['prefix'] = '/' + self.rse['prefix']
|
|
122
|
+
if not self.rse['prefix'].endswith('/'):
|
|
123
|
+
self.rse['prefix'] += '/'
|
|
124
|
+
|
|
125
|
+
if self.rse['hostname'] != ret['hostname']:
|
|
126
|
+
raise exception.RSEFileNameNotSupported('Invalid hostname: provided \'%s\', expected \'%s\'' % (ret['hostname'], self.rse['hostname']))
|
|
127
|
+
|
|
128
|
+
if not ret['path'].startswith(self.rse['prefix']):
|
|
129
|
+
raise exception.RSEFileNameNotSupported('Invalid prefix: provided \'%s\', expected \'%s\'' % ('/'.join(ret['path'].split('/')[0:len(self.rse['prefix'].split('/')) - 1]),
|
|
130
|
+
self.rse['prefix'])) # len(...)-1 due to the leading '/
|
|
131
|
+
# Spliting parsed.path into prefix, path, filename
|
|
132
|
+
ret['prefix'] = self.rse['prefix']
|
|
133
|
+
ret['path'] = ret['path'].partition(self.rse['prefix'])[2]
|
|
134
|
+
ret['name'] = ret['path'].split('/')[-1]
|
|
135
|
+
ret['path'] = ret['path'].partition(ret['name'])[0]
|
|
136
|
+
|
|
137
|
+
return ret
|