rucio-clients 37.0.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of rucio-clients might be problematic. Click here for more details.
- rucio/__init__.py +17 -0
- rucio/alembicrevision.py +15 -0
- rucio/cli/__init__.py +14 -0
- rucio/cli/account.py +216 -0
- rucio/cli/bin_legacy/__init__.py +13 -0
- rucio/cli/bin_legacy/rucio.py +2825 -0
- rucio/cli/bin_legacy/rucio_admin.py +2500 -0
- rucio/cli/command.py +272 -0
- rucio/cli/config.py +72 -0
- rucio/cli/did.py +191 -0
- rucio/cli/download.py +128 -0
- rucio/cli/lifetime_exception.py +33 -0
- rucio/cli/replica.py +162 -0
- rucio/cli/rse.py +293 -0
- rucio/cli/rule.py +158 -0
- rucio/cli/scope.py +40 -0
- rucio/cli/subscription.py +73 -0
- rucio/cli/upload.py +60 -0
- rucio/cli/utils.py +226 -0
- rucio/client/__init__.py +15 -0
- rucio/client/accountclient.py +432 -0
- rucio/client/accountlimitclient.py +183 -0
- rucio/client/baseclient.py +983 -0
- rucio/client/client.py +120 -0
- rucio/client/configclient.py +126 -0
- rucio/client/credentialclient.py +59 -0
- rucio/client/didclient.py +868 -0
- rucio/client/diracclient.py +56 -0
- rucio/client/downloadclient.py +1783 -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 +452 -0
- rucio/client/requestclient.py +125 -0
- rucio/client/richclient.py +317 -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 +969 -0
- rucio/common/__init__.py +13 -0
- rucio/common/bittorrent.py +234 -0
- rucio/common/cache.py +111 -0
- rucio/common/checksum.py +168 -0
- rucio/common/client.py +122 -0
- rucio/common/config.py +788 -0
- rucio/common/constants.py +217 -0
- rucio/common/constraints.py +17 -0
- rucio/common/didtype.py +237 -0
- rucio/common/exception.py +1208 -0
- rucio/common/extra.py +31 -0
- rucio/common/logging.py +420 -0
- rucio/common/pcache.py +1409 -0
- rucio/common/plugins.py +185 -0
- rucio/common/policy.py +93 -0
- rucio/common/schema/__init__.py +200 -0
- rucio/common/schema/generic.py +416 -0
- rucio/common/schema/generic_multi_vo.py +395 -0
- rucio/common/stomp_utils.py +423 -0
- rucio/common/stopwatch.py +55 -0
- rucio/common/test_rucio_server.py +154 -0
- rucio/common/types.py +483 -0
- rucio/common/utils.py +1688 -0
- rucio/rse/__init__.py +96 -0
- rucio/rse/protocols/__init__.py +13 -0
- rucio/rse/protocols/bittorrent.py +194 -0
- rucio/rse/protocols/cache.py +111 -0
- rucio/rse/protocols/dummy.py +100 -0
- rucio/rse/protocols/gfal.py +708 -0
- rucio/rse/protocols/globus.py +243 -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 +361 -0
- rucio/rse/protocols/rclone.py +365 -0
- rucio/rse/protocols/rfio.py +145 -0
- rucio/rse/protocols/srm.py +338 -0
- rucio/rse/protocols/ssh.py +414 -0
- rucio/rse/protocols/storm.py +195 -0
- rucio/rse/protocols/webdav.py +594 -0
- rucio/rse/protocols/xrootd.py +302 -0
- rucio/rse/rsemanager.py +881 -0
- rucio/rse/translation.py +260 -0
- rucio/vcsversion.py +11 -0
- rucio/version.py +45 -0
- rucio_clients-37.0.0rc1.data/data/etc/rse-accounts.cfg.template +25 -0
- rucio_clients-37.0.0rc1.data/data/etc/rucio.cfg.atlas.client.template +43 -0
- rucio_clients-37.0.0rc1.data/data/etc/rucio.cfg.template +241 -0
- rucio_clients-37.0.0rc1.data/data/requirements.client.txt +19 -0
- rucio_clients-37.0.0rc1.data/data/rucio_client/merge_rucio_configs.py +144 -0
- rucio_clients-37.0.0rc1.data/scripts/rucio +133 -0
- rucio_clients-37.0.0rc1.data/scripts/rucio-admin +97 -0
- rucio_clients-37.0.0rc1.dist-info/METADATA +54 -0
- rucio_clients-37.0.0rc1.dist-info/RECORD +104 -0
- rucio_clients-37.0.0rc1.dist-info/WHEEL +5 -0
- rucio_clients-37.0.0rc1.dist-info/licenses/AUTHORS.rst +100 -0
- rucio_clients-37.0.0rc1.dist-info/licenses/LICENSE +201 -0
- rucio_clients-37.0.0rc1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,250 @@
|
|
|
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 logging
|
|
16
|
+
import os
|
|
17
|
+
import os.path
|
|
18
|
+
import shutil
|
|
19
|
+
from subprocess import call
|
|
20
|
+
|
|
21
|
+
from rucio.common import exception
|
|
22
|
+
from rucio.common.checksum import adler32
|
|
23
|
+
from rucio.rse.protocols import protocol
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Default(protocol.RSEProtocol):
|
|
27
|
+
""" Implementing access to RSEs using the local filesystem."""
|
|
28
|
+
|
|
29
|
+
def exists(self, pfn):
|
|
30
|
+
"""
|
|
31
|
+
Checks if the requested file is known by the referred RSE.
|
|
32
|
+
|
|
33
|
+
:param pfn: Physical file name
|
|
34
|
+
|
|
35
|
+
:returns: True if the file exists, False if it doesn't
|
|
36
|
+
|
|
37
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
38
|
+
"""
|
|
39
|
+
status = ''
|
|
40
|
+
try:
|
|
41
|
+
status = os.path.exists(self.pfn2path(pfn))
|
|
42
|
+
except Exception as e:
|
|
43
|
+
raise exception.ServiceUnavailable(e)
|
|
44
|
+
return status
|
|
45
|
+
|
|
46
|
+
def connect(self):
|
|
47
|
+
"""
|
|
48
|
+
Establishes the actual connection to the referred RSE.
|
|
49
|
+
|
|
50
|
+
:param credentials: needed to establish a connection with the storage.
|
|
51
|
+
|
|
52
|
+
:raises RSEAccessDenied: if no connection could be established.
|
|
53
|
+
"""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def close(self):
|
|
57
|
+
""" Closes the connection to RSE."""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
def get(self, pfn, dest, transfer_timeout=None):
|
|
61
|
+
""" Provides access to files stored inside connected the RSE.
|
|
62
|
+
|
|
63
|
+
:param pfn: Physical file name of requested file
|
|
64
|
+
:param dest: Name and path of the files when stored at the client
|
|
65
|
+
:param transfer_timeout Transfer timeout (in seconds) - dummy
|
|
66
|
+
|
|
67
|
+
:raises DestinationNotAccessible: if the destination storage was not accessible.
|
|
68
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
69
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
shutil.copy(self.pfn2path(pfn), dest)
|
|
73
|
+
except OSError as e:
|
|
74
|
+
try: # To check if the error happened local or remote
|
|
75
|
+
with open(dest, 'wb'):
|
|
76
|
+
pass
|
|
77
|
+
call(['rm', '-rf', dest]) # noqa: S607
|
|
78
|
+
except OSError as e:
|
|
79
|
+
if e.errno == 2:
|
|
80
|
+
raise exception.DestinationNotAccessible(e)
|
|
81
|
+
else:
|
|
82
|
+
raise exception.ServiceUnavailable(e)
|
|
83
|
+
if e.errno == 2:
|
|
84
|
+
raise exception.SourceNotFound(e)
|
|
85
|
+
else:
|
|
86
|
+
raise exception.ServiceUnavailable(e)
|
|
87
|
+
|
|
88
|
+
def put(self, source, target, source_dir=None, transfer_timeout=None):
|
|
89
|
+
"""
|
|
90
|
+
Allows to store files inside the referred RSE.
|
|
91
|
+
|
|
92
|
+
:param source: path to the source file on the client file system
|
|
93
|
+
:param target: path to the destination file on the storage
|
|
94
|
+
:param source_dir: Path where the to be transferred files are stored in the local file system
|
|
95
|
+
:param transfer_timeout Transfer timeout (in seconds) - dummy
|
|
96
|
+
|
|
97
|
+
:raises DestinationNotAccessible: if the destination storage was not accessible.
|
|
98
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
99
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
100
|
+
"""
|
|
101
|
+
target = self.pfn2path(target)
|
|
102
|
+
|
|
103
|
+
if source_dir:
|
|
104
|
+
sf = source_dir + '/' + source
|
|
105
|
+
else:
|
|
106
|
+
sf = source
|
|
107
|
+
try:
|
|
108
|
+
dirs = os.path.dirname(target)
|
|
109
|
+
if not os.path.exists(dirs):
|
|
110
|
+
os.makedirs(dirs)
|
|
111
|
+
shutil.copy(sf, target)
|
|
112
|
+
except OSError as e:
|
|
113
|
+
if e.errno == 2:
|
|
114
|
+
raise exception.SourceNotFound(e)
|
|
115
|
+
elif not self.exists(self.rse['prefix']):
|
|
116
|
+
path = ''
|
|
117
|
+
for p in self.rse['prefix'].split('/'):
|
|
118
|
+
path += p + '/'
|
|
119
|
+
os.mkdir(path)
|
|
120
|
+
shutil.copy(sf, self.pfn2path(target))
|
|
121
|
+
else:
|
|
122
|
+
raise exception.DestinationNotAccessible(e)
|
|
123
|
+
|
|
124
|
+
def delete(self, pfn):
|
|
125
|
+
""" Deletes a file from the connected RSE.
|
|
126
|
+
|
|
127
|
+
:param pfn: pfn to the to be deleted file
|
|
128
|
+
|
|
129
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
130
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
os.remove(self.pfn2path(pfn))
|
|
134
|
+
except OSError as e:
|
|
135
|
+
if e.errno == 2:
|
|
136
|
+
raise exception.SourceNotFound(e)
|
|
137
|
+
|
|
138
|
+
def rename(self, pfn, new_pfn):
|
|
139
|
+
""" Allows to rename a file stored inside the connected RSE.
|
|
140
|
+
|
|
141
|
+
:param path: path to the current file on the storage
|
|
142
|
+
:param new_path: path to the new file on the storage
|
|
143
|
+
|
|
144
|
+
:raises DestinationNotAccessible: if the destination storage was not accessible.
|
|
145
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
146
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
147
|
+
"""
|
|
148
|
+
path = self.pfn2path(pfn)
|
|
149
|
+
new_path = self.pfn2path(new_pfn)
|
|
150
|
+
try:
|
|
151
|
+
if not os.path.exists(os.path.dirname(new_path)):
|
|
152
|
+
os.makedirs(os.path.dirname(new_path))
|
|
153
|
+
os.rename(path, new_path)
|
|
154
|
+
except OSError as e:
|
|
155
|
+
if e.errno == 2:
|
|
156
|
+
if self.exists(self.pfn2path(path)):
|
|
157
|
+
raise exception.SourceNotFound(e)
|
|
158
|
+
else:
|
|
159
|
+
raise exception.DestinationNotAccessible(e)
|
|
160
|
+
else:
|
|
161
|
+
raise exception.ServiceUnavailable(e)
|
|
162
|
+
|
|
163
|
+
def lfns2pfns(self, lfns):
|
|
164
|
+
""" Returns fully qualified PFNs for the file referred by each lfn in
|
|
165
|
+
the lfns list.
|
|
166
|
+
|
|
167
|
+
:param lfns: List of lfns. If lfn['path'] is present it is used as
|
|
168
|
+
the path to the file, otherwise the path is constructed
|
|
169
|
+
deterministically.
|
|
170
|
+
|
|
171
|
+
:returns: Fully qualified PFNs.
|
|
172
|
+
"""
|
|
173
|
+
pfns = {}
|
|
174
|
+
prefix = self.attributes['prefix']
|
|
175
|
+
|
|
176
|
+
if not prefix.startswith('/'):
|
|
177
|
+
prefix = ''.join(['/', prefix])
|
|
178
|
+
if not prefix.endswith('/'):
|
|
179
|
+
prefix = ''.join([prefix, '/'])
|
|
180
|
+
|
|
181
|
+
lfns = [lfns] if isinstance(lfns, dict) else lfns
|
|
182
|
+
for lfn in lfns:
|
|
183
|
+
scope, name = str(lfn['scope']), lfn['name']
|
|
184
|
+
if 'path' in lfn and lfn.get('path'):
|
|
185
|
+
pfns['%s:%s' % (scope, name)] = ''.join([self.attributes['scheme'],
|
|
186
|
+
'://',
|
|
187
|
+
self.attributes['hostname'],
|
|
188
|
+
prefix,
|
|
189
|
+
lfn['path'] if not lfn['path'].startswith('/') else lfn['path'][1:]
|
|
190
|
+
])
|
|
191
|
+
else:
|
|
192
|
+
pfns['%s:%s' % (scope, name)] = ''.join([self.attributes['scheme'],
|
|
193
|
+
'://',
|
|
194
|
+
self.attributes['hostname'],
|
|
195
|
+
prefix,
|
|
196
|
+
self._get_path(scope=scope, name=name)
|
|
197
|
+
])
|
|
198
|
+
return pfns
|
|
199
|
+
|
|
200
|
+
def pfn2path(self, pfn):
|
|
201
|
+
tmp = list(self.parse_pfns(pfn).values())[0]
|
|
202
|
+
return '/'.join([tmp['prefix'], tmp['path'], tmp['name']])
|
|
203
|
+
|
|
204
|
+
def stat(self, pfn):
|
|
205
|
+
""" Determines the file size in bytes and checksum (adler32) of the provided file.
|
|
206
|
+
|
|
207
|
+
:param pfn: The PFN the file.
|
|
208
|
+
|
|
209
|
+
:returns: a dict containing the keys filesize and adler32.
|
|
210
|
+
"""
|
|
211
|
+
path = self.pfn2path(pfn)
|
|
212
|
+
return {'filesize': os.stat(path)[os.path.stat.ST_SIZE], 'adler32': adler32(path)}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class Symlink(Default):
|
|
216
|
+
""" Implementing access to RSEs using the local filesystem, creating a symlink on a get """
|
|
217
|
+
|
|
218
|
+
def get(self, pfn, dest, transfer_timeout=None):
|
|
219
|
+
""" Provides access to files stored inside connected the RSE.
|
|
220
|
+
A download/get will create a symlink on the local file system pointing to the
|
|
221
|
+
underlying file. Other operations act directly on the remote file.
|
|
222
|
+
:param pfn: Physical file name of requested file
|
|
223
|
+
:param dest: Name and path of the files when stored at the client
|
|
224
|
+
:param transfer_timeout Transfer timeout (in seconds) - dummy
|
|
225
|
+
:raises DestinationNotAccessible: if the destination storage was not accessible.
|
|
226
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
227
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
228
|
+
"""
|
|
229
|
+
path = self.pfn2path(pfn)
|
|
230
|
+
os.symlink(path, dest)
|
|
231
|
+
self.logger(logging.DEBUG,
|
|
232
|
+
'Symlink {} created for {} from {}'
|
|
233
|
+
.format(dest, path, pfn))
|
|
234
|
+
if not os.lstat(dest):
|
|
235
|
+
# problem in creating the symlink
|
|
236
|
+
self.logger(logging.ERROR, 'Symlink {} could not be created'.format(dest))
|
|
237
|
+
raise exception.DestinationNotAccessible()
|
|
238
|
+
if not os.path.exists(dest):
|
|
239
|
+
# could not find the file following the symlink
|
|
240
|
+
self.logger(logging.ERROR, 'Symlink {} appears to be a broken link to {}'
|
|
241
|
+
.format(dest, path))
|
|
242
|
+
if os.lstat(dest) and os.path.islink(dest):
|
|
243
|
+
os.unlink(dest)
|
|
244
|
+
raise exception.SourceNotFound()
|
|
245
|
+
|
|
246
|
+
def pfn2path(self, pfn):
|
|
247
|
+
# obtain path and sanitise from multiple slashes, etc
|
|
248
|
+
path = os.path.normpath(super().pfn2path(pfn))
|
|
249
|
+
self.logger(logging.DEBUG, 'Extracted path: {} from: {}'.format(path, pfn))
|
|
250
|
+
return path
|
|
@@ -0,0 +1,361 @@
|
|
|
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
|
+
"""
|
|
16
|
+
This module defines the base class for implementing a transfer protocol,
|
|
17
|
+
along with some of the default methods for LFN2PFN translations.
|
|
18
|
+
"""
|
|
19
|
+
import logging
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
22
|
+
from urllib.parse import urlparse
|
|
23
|
+
|
|
24
|
+
from rucio.common import exception
|
|
25
|
+
from rucio.rse import rsemanager
|
|
26
|
+
from rucio.rse.translation import RSEDeterministicTranslation
|
|
27
|
+
|
|
28
|
+
if getattr(rsemanager, 'CLIENT_MODE', None):
|
|
29
|
+
from rucio.client.rseclient import RSEClient
|
|
30
|
+
|
|
31
|
+
if getattr(rsemanager, 'SERVER_MODE', None):
|
|
32
|
+
from rucio.core import replica
|
|
33
|
+
from rucio.core.rse import get_rse_vo
|
|
34
|
+
|
|
35
|
+
if getattr(rsemanager, 'SERVER_MODE', None) or TYPE_CHECKING:
|
|
36
|
+
from rucio.common.types import InternalScope, LFNDict, LoggerFunction, RSESettingsDict
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from collections.abc import Iterable
|
|
40
|
+
|
|
41
|
+
from rucio.common.types import DIDDict
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RSEProtocol(ABC):
|
|
45
|
+
""" This class is virtual and acts as a base to inherit new protocols from. It further provides some common functionality which applies for the majority of the protocols."""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
protocol_attr: dict[str, Any],
|
|
50
|
+
rse_settings: "RSESettingsDict",
|
|
51
|
+
logger: "LoggerFunction" = logging.log
|
|
52
|
+
):
|
|
53
|
+
""" Initializes the object with information about the referred RSE.
|
|
54
|
+
|
|
55
|
+
:param protocol_attr: Properties of the requested protocol.
|
|
56
|
+
:param rse_settting: The RSE settings.
|
|
57
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
if 'auth_token' not in protocol_attr:
|
|
61
|
+
raise exception.NoAuthInformation('No authentication token passed for the RSE protocol')
|
|
62
|
+
|
|
63
|
+
self.auth_token = protocol_attr['auth_token']
|
|
64
|
+
protocol_attr.pop('auth_token')
|
|
65
|
+
self.attributes = protocol_attr
|
|
66
|
+
self.translator = None
|
|
67
|
+
self.renaming = True
|
|
68
|
+
self.overwrite = False
|
|
69
|
+
self.rse = rse_settings
|
|
70
|
+
self.logger = logger
|
|
71
|
+
if self.rse['deterministic']:
|
|
72
|
+
self.translator = RSEDeterministicTranslation(self.rse['rse'], rse_settings, self.attributes)
|
|
73
|
+
if getattr(rsemanager, 'CLIENT_MODE', None) and \
|
|
74
|
+
not RSEDeterministicTranslation.supports(self.rse.get('lfn2pfn_algorithm')):
|
|
75
|
+
# Remote server has an algorithm we don't understand; always make the server do the lookup.
|
|
76
|
+
setattr(self, 'lfns2pfns', self.__lfns2pfns_client)
|
|
77
|
+
else:
|
|
78
|
+
if getattr(rsemanager, 'CLIENT_MODE', None):
|
|
79
|
+
setattr(self, 'lfns2pfns', self.__lfns2pfns_client)
|
|
80
|
+
if getattr(rsemanager, 'SERVER_MODE', None):
|
|
81
|
+
setattr(self, '_get_path', self._get_path_nondeterministic_server)
|
|
82
|
+
|
|
83
|
+
def lfns2pfns(
|
|
84
|
+
self,
|
|
85
|
+
lfns: Union[list["LFNDict"], "LFNDict"]
|
|
86
|
+
) -> dict[str, str]:
|
|
87
|
+
"""
|
|
88
|
+
Returns a fully qualified PFN for the file referred by path.
|
|
89
|
+
|
|
90
|
+
:param path: The path to the file.
|
|
91
|
+
|
|
92
|
+
:returns: Fully qualified PFN.
|
|
93
|
+
"""
|
|
94
|
+
pfns = {}
|
|
95
|
+
prefix = self.attributes['prefix']
|
|
96
|
+
|
|
97
|
+
if not prefix.startswith('/'):
|
|
98
|
+
prefix = ''.join(['/', prefix])
|
|
99
|
+
if not prefix.endswith('/'):
|
|
100
|
+
prefix = ''.join([prefix, '/'])
|
|
101
|
+
|
|
102
|
+
lfns = [lfns] if isinstance(lfns, dict) else lfns
|
|
103
|
+
for lfn in lfns:
|
|
104
|
+
scope, name = str(lfn['scope']), lfn['name']
|
|
105
|
+
if 'path' in lfn and lfn['path'] is not None:
|
|
106
|
+
pfns['%s:%s' % (scope, name)] = ''.join([self.attributes['scheme'],
|
|
107
|
+
'://',
|
|
108
|
+
self.attributes['hostname'],
|
|
109
|
+
':',
|
|
110
|
+
str(self.attributes['port']),
|
|
111
|
+
prefix,
|
|
112
|
+
lfn['path'] if not lfn['path'].startswith('/') else lfn['path'][1:]
|
|
113
|
+
])
|
|
114
|
+
else:
|
|
115
|
+
try:
|
|
116
|
+
pfns['%s:%s' % (scope, name)] = ''.join([self.attributes['scheme'],
|
|
117
|
+
'://',
|
|
118
|
+
self.attributes['hostname'],
|
|
119
|
+
':',
|
|
120
|
+
str(self.attributes['port']),
|
|
121
|
+
prefix,
|
|
122
|
+
self._get_path(scope=scope, name=name)
|
|
123
|
+
])
|
|
124
|
+
except exception.ReplicaNotFound as e:
|
|
125
|
+
self.logger(logging.WARNING, str(e))
|
|
126
|
+
return pfns
|
|
127
|
+
|
|
128
|
+
def __lfns2pfns_client(
|
|
129
|
+
self,
|
|
130
|
+
lfns: Union[list["DIDDict"], "DIDDict"]
|
|
131
|
+
) -> dict[str, str]:
|
|
132
|
+
""" Provides the path of a replica for non-deterministic sites. Will be assigned to get path by the __init__ method if necessary.
|
|
133
|
+
|
|
134
|
+
:param scope: list of DIDs
|
|
135
|
+
|
|
136
|
+
:returns: dict with scope:name as keys and PFN as value (in case of errors the Rucio exception si assigned to the key)
|
|
137
|
+
"""
|
|
138
|
+
client = RSEClient() # pylint: disable=E0601
|
|
139
|
+
|
|
140
|
+
lfns = [lfns] if isinstance(lfns, dict) else lfns
|
|
141
|
+
lfn_query = ["%s:%s" % (lfn['scope'], lfn['name']) for lfn in lfns]
|
|
142
|
+
return client.lfns2pfns(self.rse['rse'], lfn_query, scheme=self.attributes['scheme'])
|
|
143
|
+
|
|
144
|
+
def _get_path(
|
|
145
|
+
self,
|
|
146
|
+
scope: str,
|
|
147
|
+
name: str):
|
|
148
|
+
""" Transforms the logical file name into a PFN.
|
|
149
|
+
Suitable for sites implementing the RUCIO naming convention.
|
|
150
|
+
This implementation is only invoked if the RSE is deterministic.
|
|
151
|
+
|
|
152
|
+
:param scope: scope
|
|
153
|
+
:param name: filename
|
|
154
|
+
|
|
155
|
+
:returns: RSE specific URI of the physical file
|
|
156
|
+
"""
|
|
157
|
+
return self.translator.path(scope, name) # type: ignore (translator could be none)
|
|
158
|
+
|
|
159
|
+
def _get_path_nondeterministic_server( # pylint: disable=invalid-name
|
|
160
|
+
self,
|
|
161
|
+
scope: str,
|
|
162
|
+
name: str
|
|
163
|
+
) -> str:
|
|
164
|
+
""" Provides the path of a replica for non-deterministic sites. Will be assigned to get path by the __init__ method if necessary. """
|
|
165
|
+
vo = get_rse_vo(self.rse['id']) # pylint: disable=E0601
|
|
166
|
+
internal_scope = InternalScope(scope, vo=vo) # pylint: disable=E0601
|
|
167
|
+
rep = replica.get_replica(scope=internal_scope, name=name, rse_id=self.rse['id']) # pylint: disable=E0601
|
|
168
|
+
if 'path' in rep and rep['path'] is not None:
|
|
169
|
+
path = rep['path']
|
|
170
|
+
elif 'state' in rep and (rep['state'] is None or rep['state'] == 'UNAVAILABLE'):
|
|
171
|
+
raise exception.ReplicaUnAvailable('Missing path information and state is UNAVAILABLE for replica %s:%s on non-deterministic storage named %s' % (internal_scope, name, self.rse['rse']))
|
|
172
|
+
else:
|
|
173
|
+
raise exception.ReplicaNotFound('Missing path information for replica %s:%s on non-deterministic storage named %s' % (internal_scope, name, self.rse['rse']))
|
|
174
|
+
if path.startswith('/'):
|
|
175
|
+
path = path[1:]
|
|
176
|
+
if path.endswith('/'):
|
|
177
|
+
path = path[:-1]
|
|
178
|
+
return path
|
|
179
|
+
|
|
180
|
+
def parse_pfns(
|
|
181
|
+
self,
|
|
182
|
+
pfns: Union['Iterable[str]', str]
|
|
183
|
+
) -> dict[str, dict[str, str]]:
|
|
184
|
+
"""
|
|
185
|
+
Splits the given PFN into the parts known by the protocol. It is also checked if the provided protocol supports the given PFNs.
|
|
186
|
+
|
|
187
|
+
:param pfns: a list of a fully qualified PFNs
|
|
188
|
+
|
|
189
|
+
:returns: dic with PFN as key and a dict with path and name as value
|
|
190
|
+
|
|
191
|
+
:raises RSEFileNameNotSupported: if the provided PFN doesn't match with the protocol settings
|
|
192
|
+
"""
|
|
193
|
+
ret = dict()
|
|
194
|
+
pfns = [pfns] if isinstance(pfns, str) else pfns
|
|
195
|
+
|
|
196
|
+
for pfn in pfns:
|
|
197
|
+
parsed = urlparse(pfn)
|
|
198
|
+
scheme = parsed.scheme
|
|
199
|
+
hostname = parsed.netloc.partition(':')[0]
|
|
200
|
+
port = int(parsed.netloc.partition(':')[2]) if parsed.netloc.partition(':')[2] != '' else 0
|
|
201
|
+
while '//' in parsed.path:
|
|
202
|
+
parsed = parsed._replace(path=parsed.path.replace('//', '/'))
|
|
203
|
+
path = parsed.path
|
|
204
|
+
prefix = self.attributes['prefix']
|
|
205
|
+
while '//' in prefix:
|
|
206
|
+
prefix = prefix.replace('//', '/')
|
|
207
|
+
|
|
208
|
+
# Protect against 'lazy' defined prefixes for RSEs in the repository
|
|
209
|
+
if not prefix.startswith('/'):
|
|
210
|
+
prefix = '/' + prefix
|
|
211
|
+
if not prefix.endswith('/'):
|
|
212
|
+
prefix += '/'
|
|
213
|
+
|
|
214
|
+
if self.attributes['hostname'] != hostname:
|
|
215
|
+
if self.attributes['hostname'] != 'localhost': # In the database empty hostnames are replaced with localhost but for some URIs (e.g. file) a hostname is not included
|
|
216
|
+
raise exception.RSEFileNameNotSupported('Invalid hostname: provided \'%s\', expected \'%s\'' % (hostname, self.attributes['hostname']))
|
|
217
|
+
|
|
218
|
+
if self.attributes['port'] != port:
|
|
219
|
+
raise exception.RSEFileNameNotSupported('Invalid port: provided \'%s\', expected \'%s\'' % (port, self.attributes['port']))
|
|
220
|
+
|
|
221
|
+
if not path.startswith(prefix):
|
|
222
|
+
raise exception.RSEFileNameNotSupported('Invalid prefix: provided \'%s\', expected \'%s\'' % ('/'.join(path.split('/')[0:len(prefix.split('/')) - 1]),
|
|
223
|
+
prefix)) # len(...)-1 due to the leading '/
|
|
224
|
+
|
|
225
|
+
# Splitting parsed.path into prefix, path, filename
|
|
226
|
+
path = path.partition(prefix)[2]
|
|
227
|
+
name = path.split('/')[-1]
|
|
228
|
+
path = '/'.join(path.split('/')[:-1])
|
|
229
|
+
if not path.startswith('/'):
|
|
230
|
+
path = '/' + path
|
|
231
|
+
if path != '/' and not path.endswith('/'):
|
|
232
|
+
path = path + '/'
|
|
233
|
+
ret[pfn] = {'path': path, 'name': name, 'scheme': scheme, 'prefix': prefix, 'port': port, 'hostname': hostname, }
|
|
234
|
+
|
|
235
|
+
return ret
|
|
236
|
+
|
|
237
|
+
def exists(
|
|
238
|
+
self,
|
|
239
|
+
path: Optional[str]
|
|
240
|
+
) -> bool:
|
|
241
|
+
"""
|
|
242
|
+
Checks if the requested file is known by the referred RSE.
|
|
243
|
+
|
|
244
|
+
:param path: Physical file name
|
|
245
|
+
|
|
246
|
+
:returns: True if the file exists, False if it doesn't
|
|
247
|
+
|
|
248
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
249
|
+
"""
|
|
250
|
+
raise NotImplementedError
|
|
251
|
+
|
|
252
|
+
@abstractmethod
|
|
253
|
+
def connect(self) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Establishes the actual connection to the referred RSE.
|
|
256
|
+
|
|
257
|
+
:raises RSEAccessDenied: if no connection could be established.
|
|
258
|
+
"""
|
|
259
|
+
raise NotImplementedError
|
|
260
|
+
|
|
261
|
+
@abstractmethod
|
|
262
|
+
def close(self) -> None:
|
|
263
|
+
""" Closes the connection to RSE."""
|
|
264
|
+
raise NotImplementedError
|
|
265
|
+
|
|
266
|
+
@abstractmethod
|
|
267
|
+
def get(
|
|
268
|
+
self,
|
|
269
|
+
path: str,
|
|
270
|
+
dest: str,
|
|
271
|
+
transfer_timeout: Optional[int] = None
|
|
272
|
+
) -> None:
|
|
273
|
+
"""
|
|
274
|
+
Provides access to files stored inside connected the RSE.
|
|
275
|
+
|
|
276
|
+
:param path: Physical file name of requested file
|
|
277
|
+
:param dest: Name and path of the files when stored at the client
|
|
278
|
+
:param transfer_timeout: Transfer timeout (in seconds)
|
|
279
|
+
|
|
280
|
+
:raises DestinationNotAccessible: if the destination storage was not accessible.
|
|
281
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
282
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
283
|
+
"""
|
|
284
|
+
raise NotImplementedError
|
|
285
|
+
|
|
286
|
+
@abstractmethod
|
|
287
|
+
def put(
|
|
288
|
+
self,
|
|
289
|
+
source: str,
|
|
290
|
+
target: str,
|
|
291
|
+
source_dir: Optional[str],
|
|
292
|
+
transfer_timeout: Optional[int] = None
|
|
293
|
+
) -> None:
|
|
294
|
+
"""
|
|
295
|
+
Allows to store files inside the referred RSE.
|
|
296
|
+
|
|
297
|
+
:param source: path to the source file on the client file system
|
|
298
|
+
:param target: path to the destination file on the storage
|
|
299
|
+
:param source_dir: Path where the to be transferred files are stored in the local file system
|
|
300
|
+
:param transfer_timeout: Transfer timeout (in seconds)
|
|
301
|
+
|
|
302
|
+
:raises DestinationNotAccessible: if the destination storage was not accessible.
|
|
303
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
304
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
305
|
+
"""
|
|
306
|
+
raise NotImplementedError
|
|
307
|
+
|
|
308
|
+
@abstractmethod
|
|
309
|
+
def delete(
|
|
310
|
+
self,
|
|
311
|
+
path: str
|
|
312
|
+
) -> None:
|
|
313
|
+
"""
|
|
314
|
+
Deletes a file from the connected RSE.
|
|
315
|
+
|
|
316
|
+
:param path: path to the to be deleted file
|
|
317
|
+
|
|
318
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
319
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
320
|
+
"""
|
|
321
|
+
raise NotImplementedError
|
|
322
|
+
|
|
323
|
+
@abstractmethod
|
|
324
|
+
def rename(
|
|
325
|
+
self,
|
|
326
|
+
path: str,
|
|
327
|
+
new_path: str
|
|
328
|
+
) -> None:
|
|
329
|
+
""" Allows to rename a file stored inside the connected RSE.
|
|
330
|
+
|
|
331
|
+
:param path: path to the current file on the storage
|
|
332
|
+
:param new_path: path to the new file on the storage
|
|
333
|
+
|
|
334
|
+
:raises DestinationNotAccessible: if the destination storage was not accessible.
|
|
335
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
336
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
337
|
+
"""
|
|
338
|
+
raise NotImplementedError
|
|
339
|
+
|
|
340
|
+
def get_space_usage(self) -> tuple[int, int]:
|
|
341
|
+
"""
|
|
342
|
+
Get RSE space usage information.
|
|
343
|
+
|
|
344
|
+
:returns: a tuple 'totalsize' and 'unusedsize'
|
|
345
|
+
|
|
346
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
347
|
+
"""
|
|
348
|
+
raise NotImplementedError
|
|
349
|
+
|
|
350
|
+
def stat(self, path: str) -> dict[str, Any]:
|
|
351
|
+
"""
|
|
352
|
+
Returns the stats of a file.
|
|
353
|
+
|
|
354
|
+
:param path: path to file
|
|
355
|
+
|
|
356
|
+
:raises ServiceUnavailable: if some generic error occurred in the library.
|
|
357
|
+
:raises SourceNotFound: if the source file was not found on the referred storage.
|
|
358
|
+
|
|
359
|
+
:returns: a dict with two keys, filesize and adler32 of the file provided in path.
|
|
360
|
+
"""
|
|
361
|
+
raise NotImplementedError
|