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,444 @@
|
|
|
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
|
+
from datetime import datetime
|
|
17
|
+
from json import dumps, loads
|
|
18
|
+
from urllib.parse import quote_plus
|
|
19
|
+
|
|
20
|
+
from requests.status_codes import codes
|
|
21
|
+
|
|
22
|
+
from rucio.client.baseclient import BaseClient
|
|
23
|
+
from rucio.client.baseclient import choice
|
|
24
|
+
from rucio.common.utils import build_url, render_json, chunks
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ReplicaClient(BaseClient):
|
|
28
|
+
"""Replica client class for working with replicas"""
|
|
29
|
+
|
|
30
|
+
REPLICAS_BASEURL = 'replicas'
|
|
31
|
+
REPLICAS_CHUNK_SIZE = 1000
|
|
32
|
+
|
|
33
|
+
def quarantine_replicas(self, replicas, rse=None, rse_id=None):
|
|
34
|
+
"""
|
|
35
|
+
Add quaratined replicas for RSE.
|
|
36
|
+
|
|
37
|
+
:param replicas: List of replica infos: {'scope': <scope> (optional), 'name': <name> (optional), 'path':<path> (required)}.
|
|
38
|
+
:param rse: RSE name.
|
|
39
|
+
:param rse_id: RSE id. Either RSE name or RSE id must be specified, but not both
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
if (rse is None) == (rse_id is None):
|
|
43
|
+
raise ValueError("Either RSE name or RSE id must be specified, but not both")
|
|
44
|
+
|
|
45
|
+
url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'quarantine']))
|
|
46
|
+
headers = {}
|
|
47
|
+
for chunk in chunks(replicas, self.REPLICAS_CHUNK_SIZE):
|
|
48
|
+
data = {'rse': rse, 'rse_id': rse_id, 'replicas': chunk}
|
|
49
|
+
r = self._send_request(url, headers=headers, type_='POST', data=dumps(data))
|
|
50
|
+
if r.status_code != codes.ok:
|
|
51
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
52
|
+
raise exc_cls(exc_msg)
|
|
53
|
+
|
|
54
|
+
def declare_bad_file_replicas(self, replicas, reason, force=False):
|
|
55
|
+
"""
|
|
56
|
+
Declare a list of bad replicas.
|
|
57
|
+
|
|
58
|
+
:param replicas: Either a list of PFNs (string) or a list of dicts {'scope': <scope>, 'name': <name>, 'rse_id': <rse_id> or 'rse': <rse_name>}
|
|
59
|
+
:param reason: The reason of the loss.
|
|
60
|
+
:param force: boolean, tell the serrver to ignore existing replica status in the bad_replicas table. Default: False
|
|
61
|
+
:returns: Dictionary {"rse_name": ["did: error",...]} - list of strings for DIDs failed to declare, by RSE
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
out = {} # {rse: ["did: error text",...]}
|
|
65
|
+
url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'bad']))
|
|
66
|
+
headers = {}
|
|
67
|
+
for chunk in chunks(replicas, self.REPLICAS_CHUNK_SIZE):
|
|
68
|
+
data = {'reason': reason, 'replicas': chunk, 'force': force}
|
|
69
|
+
r = self._send_request(url, headers=headers, type_='POST', data=dumps(data))
|
|
70
|
+
if r.status_code not in (codes.created, codes.ok):
|
|
71
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
72
|
+
raise exc_cls(exc_msg)
|
|
73
|
+
chunk_result = loads(r.text)
|
|
74
|
+
if chunk_result:
|
|
75
|
+
for rse, lst in chunk_result.items():
|
|
76
|
+
out.setdefault(rse, []).extend(lst)
|
|
77
|
+
return out
|
|
78
|
+
|
|
79
|
+
def declare_bad_did_replicas(self, rse, dids, reason):
|
|
80
|
+
"""
|
|
81
|
+
Declare a list of bad replicas.
|
|
82
|
+
|
|
83
|
+
:param rse: The RSE where the bad replicas reside
|
|
84
|
+
:param dids: The DIDs of the bad replicas
|
|
85
|
+
:param reason: The reason of the loss.
|
|
86
|
+
"""
|
|
87
|
+
data = {'reason': reason, 'rse': rse, 'dids': dids}
|
|
88
|
+
url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'bad/dids']))
|
|
89
|
+
headers = {}
|
|
90
|
+
r = self._send_request(url, headers=headers, type_='POST', data=dumps(data))
|
|
91
|
+
if r.status_code == codes.created:
|
|
92
|
+
return loads(r.text)
|
|
93
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
94
|
+
raise exc_cls(exc_msg)
|
|
95
|
+
|
|
96
|
+
def declare_suspicious_file_replicas(self, pfns, reason):
|
|
97
|
+
"""
|
|
98
|
+
Declare a list of bad replicas.
|
|
99
|
+
|
|
100
|
+
:param pfns: The list of PFNs.
|
|
101
|
+
:param reason: The reason of the loss.
|
|
102
|
+
"""
|
|
103
|
+
data = {'reason': reason, 'pfns': pfns}
|
|
104
|
+
url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'suspicious']))
|
|
105
|
+
headers = {}
|
|
106
|
+
r = self._send_request(url, headers=headers, type_='POST', data=dumps(data))
|
|
107
|
+
if r.status_code == codes.created:
|
|
108
|
+
return loads(r.text)
|
|
109
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
110
|
+
raise exc_cls(exc_msg)
|
|
111
|
+
|
|
112
|
+
def get_did_from_pfns(self, pfns, rse=None):
|
|
113
|
+
"""
|
|
114
|
+
Get the DIDs associated to a PFN on one given RSE
|
|
115
|
+
|
|
116
|
+
:param pfns: The list of PFNs.
|
|
117
|
+
:param rse: The RSE name.
|
|
118
|
+
:returns: A list of dictionaries {pfn: {'scope': scope, 'name': name}}
|
|
119
|
+
"""
|
|
120
|
+
data = {'rse': rse, 'pfns': pfns}
|
|
121
|
+
url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'dids']))
|
|
122
|
+
headers = {}
|
|
123
|
+
r = self._send_request(url, headers=headers, type_='POST', data=dumps(data))
|
|
124
|
+
if r.status_code == codes.ok:
|
|
125
|
+
return self._load_json_data(r)
|
|
126
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
127
|
+
raise exc_cls(exc_msg)
|
|
128
|
+
|
|
129
|
+
def list_replicas(self, dids, schemes=None, ignore_availability=True,
|
|
130
|
+
all_states=False, metalink=False, rse_expression=None,
|
|
131
|
+
client_location=None, sort=None, domain=None,
|
|
132
|
+
signature_lifetime=None, nrandom=None,
|
|
133
|
+
resolve_archives=True, resolve_parents=False,
|
|
134
|
+
updated_after=None):
|
|
135
|
+
"""
|
|
136
|
+
List file replicas for a list of data identifiers (DIDs).
|
|
137
|
+
|
|
138
|
+
:param dids: The list of data identifiers (DIDs) like :
|
|
139
|
+
[{'scope': <scope1>, 'name': <name1>}, {'scope': <scope2>, 'name': <name2>}, ...]
|
|
140
|
+
:param schemes: A list of schemes to filter the replicas. (e.g. file, http, ...)
|
|
141
|
+
:param ignore_availability: Also include replicas from blocked RSEs into the list
|
|
142
|
+
:param metalink: ``False`` (default) retrieves as JSON,
|
|
143
|
+
``True`` retrieves as metalink4+xml.
|
|
144
|
+
:param rse_expression: The RSE expression to restrict replicas on a set of RSEs.
|
|
145
|
+
:param client_location: Client location dictionary for PFN modification {'ip', 'fqdn', 'site', 'latitude', 'longitude'}
|
|
146
|
+
:param sort: Sort the replicas: ``geoip`` - based on src/dst IP topographical distance
|
|
147
|
+
``closeness`` - based on src/dst closeness
|
|
148
|
+
``dynamic`` - Rucio Dynamic Smart Sort (tm)
|
|
149
|
+
:param domain: Define the domain. None is fallback to 'wan', otherwise 'wan, 'lan', or 'all'
|
|
150
|
+
:param signature_lifetime: If supported, in seconds, restrict the lifetime of the signed PFN.
|
|
151
|
+
:param nrandom: pick N random replicas. If the initial number of replicas is smaller than N, returns all replicas.
|
|
152
|
+
:param resolve_archives: When set to True, find archives which contain the replicas.
|
|
153
|
+
:param resolve_parents: When set to True, find all parent datasets which contain the replicas.
|
|
154
|
+
:param updated_after: epoch timestamp or datetime object (UTC time), only return replicas updated after this time
|
|
155
|
+
|
|
156
|
+
:returns: A list of dictionaries with replica information.
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
data = {'dids': dids,
|
|
160
|
+
'domain': domain}
|
|
161
|
+
|
|
162
|
+
if schemes:
|
|
163
|
+
data['schemes'] = schemes
|
|
164
|
+
if ignore_availability is not None:
|
|
165
|
+
data['ignore_availability'] = ignore_availability
|
|
166
|
+
data['all_states'] = all_states
|
|
167
|
+
|
|
168
|
+
if rse_expression:
|
|
169
|
+
data['rse_expression'] = rse_expression
|
|
170
|
+
|
|
171
|
+
if client_location:
|
|
172
|
+
data['client_location'] = client_location
|
|
173
|
+
|
|
174
|
+
if sort:
|
|
175
|
+
data['sort'] = sort
|
|
176
|
+
|
|
177
|
+
if updated_after:
|
|
178
|
+
if isinstance(updated_after, datetime):
|
|
179
|
+
# encode in UTC string with format '%Y-%m-%dT%H:%M:%S' e.g. '2020-03-02T12:01:38'
|
|
180
|
+
data['updated_after'] = updated_after.strftime('%Y-%m-%dT%H:%M:%S')
|
|
181
|
+
else:
|
|
182
|
+
data['updated_after'] = updated_after
|
|
183
|
+
|
|
184
|
+
if signature_lifetime:
|
|
185
|
+
data['signature_lifetime'] = signature_lifetime
|
|
186
|
+
|
|
187
|
+
if nrandom:
|
|
188
|
+
data['nrandom'] = nrandom
|
|
189
|
+
|
|
190
|
+
data['resolve_archives'] = resolve_archives
|
|
191
|
+
|
|
192
|
+
data['resolve_parents'] = resolve_parents
|
|
193
|
+
|
|
194
|
+
url = build_url(choice(self.list_hosts),
|
|
195
|
+
path='/'.join([self.REPLICAS_BASEURL, 'list']))
|
|
196
|
+
|
|
197
|
+
headers = {}
|
|
198
|
+
if metalink:
|
|
199
|
+
headers['Accept'] = 'application/metalink4+xml'
|
|
200
|
+
|
|
201
|
+
# pass json dict in querystring
|
|
202
|
+
r = self._send_request(url, headers=headers, type_='POST', data=dumps(data), stream=True)
|
|
203
|
+
if r.status_code == codes.ok:
|
|
204
|
+
if not metalink:
|
|
205
|
+
return self._load_json_data(r)
|
|
206
|
+
return r.text
|
|
207
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
208
|
+
raise exc_cls(exc_msg)
|
|
209
|
+
|
|
210
|
+
def list_suspicious_replicas(self, rse_expression=None, younger_than=None, nattempts=None):
|
|
211
|
+
"""
|
|
212
|
+
List file replicas tagged as suspicious.
|
|
213
|
+
|
|
214
|
+
:param rse_expression: The RSE expression to restrict replicas on a set of RSEs.
|
|
215
|
+
:param younger_than: Datetime object to select the replicas which were declared since younger_than date. Default value = 10 days ago.
|
|
216
|
+
:param nattempts: The minimum number of replica appearances in the bad_replica DB table from younger_than date. Default value = 0.
|
|
217
|
+
:param state: State of the replica, either 'BAD' or 'SUSPICIOUS'. No value returns replicas with either state.
|
|
218
|
+
|
|
219
|
+
"""
|
|
220
|
+
params = {}
|
|
221
|
+
if rse_expression:
|
|
222
|
+
params['rse_expression'] = rse_expression
|
|
223
|
+
|
|
224
|
+
if younger_than:
|
|
225
|
+
params['younger_than'] = younger_than
|
|
226
|
+
|
|
227
|
+
if nattempts:
|
|
228
|
+
params['nattempts'] = nattempts
|
|
229
|
+
|
|
230
|
+
url = build_url(choice(self.list_hosts),
|
|
231
|
+
path='/'.join([self.REPLICAS_BASEURL, 'suspicious']))
|
|
232
|
+
r = self._send_request(url, type_='GET', params=params)
|
|
233
|
+
if r.status_code == codes.ok:
|
|
234
|
+
return self._load_json_data(r)
|
|
235
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
236
|
+
raise exc_cls(exc_msg)
|
|
237
|
+
|
|
238
|
+
def add_replica(self, rse, scope, name, bytes_, adler32, pfn=None, md5=None, meta={}):
|
|
239
|
+
"""
|
|
240
|
+
Add file replicas to a RSE.
|
|
241
|
+
|
|
242
|
+
:param rse: the RSE name.
|
|
243
|
+
:param scope: The scope of the file.
|
|
244
|
+
:param name: The name of the file.
|
|
245
|
+
:param bytes_: The size in bytes.
|
|
246
|
+
:param adler32: adler32 checksum.
|
|
247
|
+
:param pfn: PFN of the file for non deterministic RSE.
|
|
248
|
+
:param md5: md5 checksum.
|
|
249
|
+
:param meta: Metadata attributes.
|
|
250
|
+
|
|
251
|
+
:return: True if files were created successfully.
|
|
252
|
+
|
|
253
|
+
"""
|
|
254
|
+
dict_ = {'scope': scope, 'name': name, 'bytes': bytes_, 'meta': meta, 'adler32': adler32}
|
|
255
|
+
if md5:
|
|
256
|
+
dict_['md5'] = md5
|
|
257
|
+
if pfn:
|
|
258
|
+
dict_['pfn'] = pfn
|
|
259
|
+
return self.add_replicas(rse=rse, files=[dict_])
|
|
260
|
+
|
|
261
|
+
def add_replicas(self, rse, files, ignore_availability=True):
|
|
262
|
+
"""
|
|
263
|
+
Bulk add file replicas to a RSE.
|
|
264
|
+
|
|
265
|
+
:param rse: the RSE name.
|
|
266
|
+
:param files: The list of files. This is a list of DIDs like :
|
|
267
|
+
[{'scope': <scope1>, 'name': <name1>}, {'scope': <scope2>, 'name': <name2>}, ...]
|
|
268
|
+
:param ignore_availability: Ignore the RSE blocklsit.
|
|
269
|
+
|
|
270
|
+
:return: True if files were created successfully.
|
|
271
|
+
|
|
272
|
+
"""
|
|
273
|
+
url = build_url(choice(self.list_hosts), path=self.REPLICAS_BASEURL)
|
|
274
|
+
data = {'rse': rse, 'files': files, 'ignore_availability': ignore_availability}
|
|
275
|
+
r = self._send_request(url, type_='POST', data=render_json(**data))
|
|
276
|
+
if r.status_code == codes.created:
|
|
277
|
+
return True
|
|
278
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
279
|
+
raise exc_cls(exc_msg)
|
|
280
|
+
|
|
281
|
+
def delete_replicas(self, rse, files, ignore_availability=True):
|
|
282
|
+
"""
|
|
283
|
+
Bulk delete file replicas from a RSE.
|
|
284
|
+
|
|
285
|
+
:param rse: the RSE name.
|
|
286
|
+
:param files: The list of files. This is a list of DIDs like :
|
|
287
|
+
[{'scope': <scope1>, 'name': <name1>}, {'scope': <scope2>, 'name': <name2>}, ...]
|
|
288
|
+
:param ignore_availability: Ignore the RSE blocklist.
|
|
289
|
+
|
|
290
|
+
:return: True if files have been deleted successfully.
|
|
291
|
+
|
|
292
|
+
"""
|
|
293
|
+
url = build_url(choice(self.list_hosts), path=self.REPLICAS_BASEURL)
|
|
294
|
+
data = {'rse': rse, 'files': files, 'ignore_availability': ignore_availability}
|
|
295
|
+
r = self._send_request(url, type_='DEL', data=render_json(**data))
|
|
296
|
+
if r.status_code == codes.ok:
|
|
297
|
+
return True
|
|
298
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
299
|
+
raise exc_cls(exc_msg)
|
|
300
|
+
|
|
301
|
+
def update_replicas_states(self, rse, files):
|
|
302
|
+
"""
|
|
303
|
+
Bulk update the file replicas states from a RSE.
|
|
304
|
+
|
|
305
|
+
:param rse: the RSE name.
|
|
306
|
+
:param files: The list of files. This is a list of DIDs like :
|
|
307
|
+
[{'scope': <scope1>, 'name': <name1>, 'state': <state1>}, {'scope': <scope2>, 'name': <name2>, 'state': <state2>}, ...],
|
|
308
|
+
where a state value can be either of:
|
|
309
|
+
'A' (AVAILABLE)
|
|
310
|
+
'U' (UNAVAILABLE)
|
|
311
|
+
'C' (COPYING)
|
|
312
|
+
'B' (BEING_DELETED)
|
|
313
|
+
'D' (BAD)
|
|
314
|
+
'T' (TEMPORARY_UNAVAILABLE)
|
|
315
|
+
:return: True if replica states have been updated successfully, otherwise an exception is raised.
|
|
316
|
+
|
|
317
|
+
"""
|
|
318
|
+
url = build_url(choice(self.list_hosts), path=self.REPLICAS_BASEURL)
|
|
319
|
+
data = {'rse': rse, 'files': files}
|
|
320
|
+
r = self._send_request(url, type_='PUT', data=render_json(**data))
|
|
321
|
+
if r.status_code == codes.ok:
|
|
322
|
+
return True
|
|
323
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
324
|
+
raise exc_cls(exc_msg)
|
|
325
|
+
|
|
326
|
+
def list_dataset_replicas(self, scope, name, deep=False):
|
|
327
|
+
"""
|
|
328
|
+
List dataset replicas for a did (scope:name).
|
|
329
|
+
|
|
330
|
+
:param scope: The scope of the dataset.
|
|
331
|
+
:param name: The name of the dataset.
|
|
332
|
+
:param deep: Lookup at the file level.
|
|
333
|
+
|
|
334
|
+
:returns: A list of dict dataset replicas.
|
|
335
|
+
|
|
336
|
+
"""
|
|
337
|
+
payload = {}
|
|
338
|
+
if deep:
|
|
339
|
+
payload = {'deep': True}
|
|
340
|
+
|
|
341
|
+
url = build_url(self.host,
|
|
342
|
+
path='/'.join([self.REPLICAS_BASEURL, quote_plus(scope), quote_plus(name), 'datasets']),
|
|
343
|
+
params=payload)
|
|
344
|
+
r = self._send_request(url, type_='GET')
|
|
345
|
+
if r.status_code == codes.ok:
|
|
346
|
+
return self._load_json_data(r)
|
|
347
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
348
|
+
raise exc_cls(exc_msg)
|
|
349
|
+
|
|
350
|
+
def list_dataset_replicas_bulk(self, dids):
|
|
351
|
+
"""
|
|
352
|
+
List dataset replicas for a did (scope:name).
|
|
353
|
+
|
|
354
|
+
:param dids: The list of DIDs of the datasets.
|
|
355
|
+
|
|
356
|
+
:returns: A list of dict dataset replicas.
|
|
357
|
+
"""
|
|
358
|
+
payload = {'dids': list(dids)}
|
|
359
|
+
url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'datasets_bulk']))
|
|
360
|
+
r = self._send_request(url, type_='POST', data=dumps(payload))
|
|
361
|
+
if r.status_code == codes.ok:
|
|
362
|
+
return self._load_json_data(r)
|
|
363
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
364
|
+
raise exc_cls(exc_msg)
|
|
365
|
+
|
|
366
|
+
def list_dataset_replicas_vp(self, scope, name, deep=False):
|
|
367
|
+
"""
|
|
368
|
+
List dataset replicas for a DID (scope:name) using the
|
|
369
|
+
Virtual Placement service.
|
|
370
|
+
|
|
371
|
+
NOTICE: This is an RnD function and might change or go away at any time.
|
|
372
|
+
|
|
373
|
+
:param scope: The scope of the dataset.
|
|
374
|
+
:param name: The name of the dataset.
|
|
375
|
+
:param deep: Lookup at the file level.
|
|
376
|
+
|
|
377
|
+
:returns: If VP exists a list of dicts of sites
|
|
378
|
+
"""
|
|
379
|
+
payload = {}
|
|
380
|
+
if deep:
|
|
381
|
+
payload = {'deep': True}
|
|
382
|
+
|
|
383
|
+
url = build_url(self.host,
|
|
384
|
+
path='/'.join([self.REPLICAS_BASEURL, quote_plus(scope), quote_plus(name), 'datasets_vp']),
|
|
385
|
+
params=payload)
|
|
386
|
+
r = self._send_request(url, type_='GET')
|
|
387
|
+
if r.status_code == codes.ok:
|
|
388
|
+
return self._load_json_data(r)
|
|
389
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
390
|
+
raise exc_cls(exc_msg)
|
|
391
|
+
|
|
392
|
+
def list_datasets_per_rse(self, rse, filters=None, limit=None):
|
|
393
|
+
"""
|
|
394
|
+
List datasets at a RSE.
|
|
395
|
+
|
|
396
|
+
:param rse: the rse name.
|
|
397
|
+
:param filters: dictionary of attributes by which the results should be filtered.
|
|
398
|
+
:param limit: limit number.
|
|
399
|
+
|
|
400
|
+
:returns: A list of dict dataset replicas.
|
|
401
|
+
|
|
402
|
+
"""
|
|
403
|
+
url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'rse', rse]))
|
|
404
|
+
r = self._send_request(url, type_='GET')
|
|
405
|
+
if r.status_code == codes.ok:
|
|
406
|
+
return self._load_json_data(r)
|
|
407
|
+
|
|
408
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
409
|
+
raise exc_cls(exc_msg)
|
|
410
|
+
|
|
411
|
+
def add_bad_pfns(self, pfns, reason, state, expires_at):
|
|
412
|
+
"""
|
|
413
|
+
Declare a list of bad replicas.
|
|
414
|
+
|
|
415
|
+
:param pfns: The list of PFNs.
|
|
416
|
+
:param reason: The reason of the loss.
|
|
417
|
+
:param state: The state of the replica. Either BAD, SUSPICIOUS, TEMPORARY_UNAVAILABLE
|
|
418
|
+
:param expires_at: Specify a timeout for the TEMPORARY_UNAVAILABLE replicas. None for BAD files.
|
|
419
|
+
|
|
420
|
+
:return: True if PFNs were created successfully.
|
|
421
|
+
|
|
422
|
+
"""
|
|
423
|
+
data = {'reason': reason, 'pfns': pfns, 'state': state, 'expires_at': expires_at}
|
|
424
|
+
url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'bad/pfns']))
|
|
425
|
+
headers = {}
|
|
426
|
+
r = self._send_request(url, headers=headers, type_='POST', data=dumps(data))
|
|
427
|
+
if r.status_code == codes.created:
|
|
428
|
+
return True
|
|
429
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
430
|
+
raise exc_cls(exc_msg)
|
|
431
|
+
|
|
432
|
+
def set_tombstone(self, replicas):
|
|
433
|
+
"""
|
|
434
|
+
Set a tombstone on a list of replicas.
|
|
435
|
+
|
|
436
|
+
:param replicas: list of replicas.
|
|
437
|
+
"""
|
|
438
|
+
url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'tombstone']))
|
|
439
|
+
data = {'replicas': replicas}
|
|
440
|
+
r = self._send_request(url, type_='POST', data=render_json(**data))
|
|
441
|
+
if r.status_code == codes.created:
|
|
442
|
+
return True
|
|
443
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
444
|
+
raise exc_cls(exc_msg)
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
from urllib.parse import quote_plus
|
|
17
|
+
|
|
18
|
+
from requests.status_codes import codes
|
|
19
|
+
|
|
20
|
+
from rucio.client.baseclient import BaseClient
|
|
21
|
+
from rucio.client.baseclient import choice
|
|
22
|
+
from rucio.common.utils import build_url
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RequestClient(BaseClient):
|
|
26
|
+
|
|
27
|
+
REQUEST_BASEURL = 'requests'
|
|
28
|
+
|
|
29
|
+
def list_requests(self, src_rse, dst_rse, request_states):
|
|
30
|
+
"""Return latest request details
|
|
31
|
+
|
|
32
|
+
:return: request information
|
|
33
|
+
:rtype: dict
|
|
34
|
+
"""
|
|
35
|
+
path = '/'.join([self.REQUEST_BASEURL, 'list']) + '?' + '&'.join(['src_rse={}'.format(src_rse), 'dst_rse={}'.format(
|
|
36
|
+
dst_rse), 'request_states={}'.format(request_states)])
|
|
37
|
+
url = build_url(choice(self.list_hosts), path=path)
|
|
38
|
+
r = self._send_request(url, type_='GET')
|
|
39
|
+
|
|
40
|
+
if r.status_code == codes.ok:
|
|
41
|
+
return self._load_json_data(r)
|
|
42
|
+
else:
|
|
43
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
44
|
+
raise exc_cls(exc_msg)
|
|
45
|
+
|
|
46
|
+
def list_requests_history(self, src_rse, dst_rse, request_states, offset=0, limit=100):
|
|
47
|
+
"""Return historical request details
|
|
48
|
+
|
|
49
|
+
:return: request information
|
|
50
|
+
:rtype: dict
|
|
51
|
+
"""
|
|
52
|
+
path = '/'.join([self.REQUEST_BASEURL, 'history', 'list']) + '?' + '&'.join(['src_rse={}'.format(src_rse), 'dst_rse={}'.format(
|
|
53
|
+
dst_rse), 'request_states={}'.format(request_states), 'offset={}'.format(offset), 'limit={}'.format(limit)])
|
|
54
|
+
url = build_url(choice(self.list_hosts), path=path)
|
|
55
|
+
r = self._send_request(url, type_='GET')
|
|
56
|
+
|
|
57
|
+
if r.status_code == codes.ok:
|
|
58
|
+
return self._load_json_data(r)
|
|
59
|
+
else:
|
|
60
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
61
|
+
raise exc_cls(exc_msg)
|
|
62
|
+
|
|
63
|
+
def list_request_by_did(self, name, rse, scope=None):
|
|
64
|
+
"""Return latest request details for a DID
|
|
65
|
+
|
|
66
|
+
:param name: DID
|
|
67
|
+
:type name: str
|
|
68
|
+
:param rse: Destination RSE name
|
|
69
|
+
:type rse: str
|
|
70
|
+
:param scope: rucio scope, defaults to None
|
|
71
|
+
:param scope: str, optional
|
|
72
|
+
:raises exc_cls: from BaseClient._get_exception
|
|
73
|
+
:return: request information
|
|
74
|
+
:rtype: dict
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
path = '/'.join([self.REQUEST_BASEURL, quote_plus(scope), quote_plus(name), rse])
|
|
78
|
+
url = build_url(choice(self.list_hosts), path=path)
|
|
79
|
+
r = self._send_request(url, type_='GET')
|
|
80
|
+
|
|
81
|
+
if r.status_code == codes.ok:
|
|
82
|
+
return next(self._load_json_data(r))
|
|
83
|
+
else:
|
|
84
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
85
|
+
raise exc_cls(exc_msg)
|
|
86
|
+
|
|
87
|
+
def list_request_history_by_did(self, name, rse, scope=None):
|
|
88
|
+
"""Return latest request details for a DID
|
|
89
|
+
|
|
90
|
+
:param name: DID
|
|
91
|
+
:type name: str
|
|
92
|
+
:param rse: Destination RSE name
|
|
93
|
+
:type rse: str
|
|
94
|
+
:param scope: rucio scope, defaults to None
|
|
95
|
+
:param scope: str, optional
|
|
96
|
+
:raises exc_cls: from BaseClient._get_exception
|
|
97
|
+
:return: request information
|
|
98
|
+
:rtype: dict
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
path = '/'.join([self.REQUEST_BASEURL, 'history', quote_plus(scope), quote_plus(name), rse])
|
|
102
|
+
url = build_url(choice(self.list_hosts), path=path)
|
|
103
|
+
r = self._send_request(url, type_='GET')
|
|
104
|
+
|
|
105
|
+
if r.status_code == codes.ok:
|
|
106
|
+
return next(self._load_json_data(r))
|
|
107
|
+
else:
|
|
108
|
+
exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
|
|
109
|
+
raise exc_cls(exc_msg)
|