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.

Files changed (88) hide show
  1. rucio/__init__.py +18 -0
  2. rucio/alembicrevision.py +16 -0
  3. rucio/client/__init__.py +16 -0
  4. rucio/client/accountclient.py +413 -0
  5. rucio/client/accountlimitclient.py +155 -0
  6. rucio/client/baseclient.py +929 -0
  7. rucio/client/client.py +77 -0
  8. rucio/client/configclient.py +113 -0
  9. rucio/client/credentialclient.py +54 -0
  10. rucio/client/didclient.py +691 -0
  11. rucio/client/diracclient.py +48 -0
  12. rucio/client/downloadclient.py +1674 -0
  13. rucio/client/exportclient.py +44 -0
  14. rucio/client/fileclient.py +51 -0
  15. rucio/client/importclient.py +42 -0
  16. rucio/client/lifetimeclient.py +74 -0
  17. rucio/client/lockclient.py +99 -0
  18. rucio/client/metaclient.py +137 -0
  19. rucio/client/pingclient.py +45 -0
  20. rucio/client/replicaclient.py +444 -0
  21. rucio/client/requestclient.py +109 -0
  22. rucio/client/rseclient.py +664 -0
  23. rucio/client/ruleclient.py +287 -0
  24. rucio/client/scopeclient.py +88 -0
  25. rucio/client/subscriptionclient.py +161 -0
  26. rucio/client/touchclient.py +78 -0
  27. rucio/client/uploadclient.py +871 -0
  28. rucio/common/__init__.py +14 -0
  29. rucio/common/cache.py +74 -0
  30. rucio/common/config.py +796 -0
  31. rucio/common/constants.py +92 -0
  32. rucio/common/constraints.py +18 -0
  33. rucio/common/didtype.py +187 -0
  34. rucio/common/exception.py +1092 -0
  35. rucio/common/extra.py +37 -0
  36. rucio/common/logging.py +404 -0
  37. rucio/common/pcache.py +1387 -0
  38. rucio/common/policy.py +84 -0
  39. rucio/common/schema/__init__.py +143 -0
  40. rucio/common/schema/atlas.py +411 -0
  41. rucio/common/schema/belleii.py +406 -0
  42. rucio/common/schema/cms.py +478 -0
  43. rucio/common/schema/domatpc.py +399 -0
  44. rucio/common/schema/escape.py +424 -0
  45. rucio/common/schema/generic.py +431 -0
  46. rucio/common/schema/generic_multi_vo.py +410 -0
  47. rucio/common/schema/icecube.py +404 -0
  48. rucio/common/schema/lsst.py +423 -0
  49. rucio/common/stomp_utils.py +160 -0
  50. rucio/common/stopwatch.py +56 -0
  51. rucio/common/test_rucio_server.py +148 -0
  52. rucio/common/types.py +158 -0
  53. rucio/common/utils.py +1946 -0
  54. rucio/rse/__init__.py +97 -0
  55. rucio/rse/protocols/__init__.py +14 -0
  56. rucio/rse/protocols/cache.py +123 -0
  57. rucio/rse/protocols/dummy.py +112 -0
  58. rucio/rse/protocols/gfal.py +701 -0
  59. rucio/rse/protocols/globus.py +243 -0
  60. rucio/rse/protocols/gsiftp.py +93 -0
  61. rucio/rse/protocols/http_cache.py +83 -0
  62. rucio/rse/protocols/mock.py +124 -0
  63. rucio/rse/protocols/ngarc.py +210 -0
  64. rucio/rse/protocols/posix.py +251 -0
  65. rucio/rse/protocols/protocol.py +530 -0
  66. rucio/rse/protocols/rclone.py +365 -0
  67. rucio/rse/protocols/rfio.py +137 -0
  68. rucio/rse/protocols/srm.py +339 -0
  69. rucio/rse/protocols/ssh.py +414 -0
  70. rucio/rse/protocols/storm.py +207 -0
  71. rucio/rse/protocols/webdav.py +547 -0
  72. rucio/rse/protocols/xrootd.py +295 -0
  73. rucio/rse/rsemanager.py +752 -0
  74. rucio/vcsversion.py +11 -0
  75. rucio/version.py +46 -0
  76. rucio_clients-32.8.6.data/data/etc/rse-accounts.cfg.template +25 -0
  77. rucio_clients-32.8.6.data/data/etc/rucio.cfg.atlas.client.template +42 -0
  78. rucio_clients-32.8.6.data/data/etc/rucio.cfg.template +257 -0
  79. rucio_clients-32.8.6.data/data/requirements.txt +55 -0
  80. rucio_clients-32.8.6.data/data/rucio_client/merge_rucio_configs.py +147 -0
  81. rucio_clients-32.8.6.data/scripts/rucio +2540 -0
  82. rucio_clients-32.8.6.data/scripts/rucio-admin +2434 -0
  83. rucio_clients-32.8.6.dist-info/METADATA +50 -0
  84. rucio_clients-32.8.6.dist-info/RECORD +88 -0
  85. rucio_clients-32.8.6.dist-info/WHEEL +5 -0
  86. rucio_clients-32.8.6.dist-info/licenses/AUTHORS.rst +94 -0
  87. rucio_clients-32.8.6.dist-info/licenses/LICENSE +201 -0
  88. 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)