rucio-clients 35.7.0__py3-none-any.whl → 37.0.0rc2__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/alembicrevision.py +1 -1
- rucio/cli/__init__.py +14 -0
- rucio/cli/account.py +216 -0
- rucio/cli/bin_legacy/__init__.py +13 -0
- rucio_clients-35.7.0.data/scripts/rucio → rucio/cli/bin_legacy/rucio.py +769 -486
- rucio_clients-35.7.0.data/scripts/rucio-admin → rucio/cli/bin_legacy/rucio_admin.py +476 -423
- 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/accountclient.py +0 -1
- rucio/client/baseclient.py +33 -24
- rucio/client/client.py +45 -1
- rucio/client/didclient.py +5 -3
- rucio/client/downloadclient.py +6 -8
- rucio/client/replicaclient.py +0 -2
- rucio/client/richclient.py +317 -0
- rucio/client/rseclient.py +4 -4
- rucio/client/uploadclient.py +26 -12
- rucio/common/bittorrent.py +234 -0
- rucio/common/cache.py +66 -29
- rucio/common/checksum.py +168 -0
- rucio/common/client.py +122 -0
- rucio/common/config.py +22 -35
- rucio/common/constants.py +61 -3
- rucio/common/didtype.py +72 -24
- rucio/common/exception.py +65 -8
- rucio/common/extra.py +5 -10
- rucio/common/logging.py +13 -13
- rucio/common/pcache.py +8 -7
- rucio/common/plugins.py +59 -27
- rucio/common/policy.py +12 -3
- rucio/common/schema/__init__.py +84 -34
- rucio/common/schema/generic.py +0 -17
- rucio/common/schema/generic_multi_vo.py +0 -17
- rucio/common/stomp_utils.py +383 -119
- rucio/common/test_rucio_server.py +12 -6
- rucio/common/types.py +132 -52
- rucio/common/utils.py +93 -643
- rucio/rse/__init__.py +3 -3
- rucio/rse/protocols/bittorrent.py +11 -1
- rucio/rse/protocols/cache.py +0 -11
- rucio/rse/protocols/dummy.py +0 -11
- rucio/rse/protocols/gfal.py +14 -9
- rucio/rse/protocols/globus.py +1 -1
- rucio/rse/protocols/http_cache.py +1 -1
- rucio/rse/protocols/posix.py +2 -2
- rucio/rse/protocols/protocol.py +84 -317
- rucio/rse/protocols/rclone.py +2 -1
- rucio/rse/protocols/rfio.py +10 -1
- rucio/rse/protocols/ssh.py +2 -1
- rucio/rse/protocols/storm.py +2 -13
- rucio/rse/protocols/webdav.py +74 -30
- rucio/rse/protocols/xrootd.py +2 -1
- rucio/rse/rsemanager.py +170 -53
- rucio/rse/translation.py +260 -0
- rucio/vcsversion.py +4 -4
- rucio/version.py +7 -0
- {rucio_clients-35.7.0.data → rucio_clients-37.0.0rc2.data}/data/etc/rucio.cfg.atlas.client.template +3 -2
- {rucio_clients-35.7.0.data → rucio_clients-37.0.0rc2.data}/data/etc/rucio.cfg.template +3 -19
- {rucio_clients-35.7.0.data → rucio_clients-37.0.0rc2.data}/data/requirements.client.txt +11 -7
- rucio_clients-37.0.0rc2.data/scripts/rucio +133 -0
- rucio_clients-37.0.0rc2.data/scripts/rucio-admin +97 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0rc2.dist-info}/METADATA +18 -14
- rucio_clients-37.0.0rc2.dist-info/RECORD +104 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0rc2.dist-info}/licenses/AUTHORS.rst +3 -0
- rucio/common/schema/atlas.py +0 -413
- rucio/common/schema/belleii.py +0 -408
- rucio/common/schema/domatpc.py +0 -401
- rucio/common/schema/escape.py +0 -426
- rucio/common/schema/icecube.py +0 -406
- rucio/rse/protocols/gsiftp.py +0 -92
- rucio_clients-35.7.0.dist-info/RECORD +0 -88
- {rucio_clients-35.7.0.data → rucio_clients-37.0.0rc2.data}/data/etc/rse-accounts.cfg.template +0 -0
- {rucio_clients-35.7.0.data → rucio_clients-37.0.0rc2.data}/data/rucio_client/merge_rucio_configs.py +0 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0rc2.dist-info}/WHEEL +0 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0rc2.dist-info}/licenses/LICENSE +0 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0rc2.dist-info}/top_level.txt +0 -0
rucio/common/utils.py
CHANGED
|
@@ -14,18 +14,13 @@
|
|
|
14
14
|
|
|
15
15
|
import argparse
|
|
16
16
|
import base64
|
|
17
|
-
import copy
|
|
18
17
|
import datetime
|
|
19
18
|
import errno
|
|
20
19
|
import getpass
|
|
21
|
-
import hashlib
|
|
22
|
-
import io
|
|
23
20
|
import ipaddress
|
|
24
21
|
import itertools
|
|
25
22
|
import json
|
|
26
23
|
import logging
|
|
27
|
-
import math
|
|
28
|
-
import mmap
|
|
29
24
|
import os
|
|
30
25
|
import os.path
|
|
31
26
|
import re
|
|
@@ -35,11 +30,9 @@ import subprocess
|
|
|
35
30
|
import tempfile
|
|
36
31
|
import threading
|
|
37
32
|
import time
|
|
38
|
-
import zlib
|
|
39
33
|
from collections import OrderedDict
|
|
40
|
-
from collections.abc import Callable, Iterable, Iterator, Sequence
|
|
41
34
|
from enum import Enum
|
|
42
|
-
from functools import
|
|
35
|
+
from functools import wraps
|
|
43
36
|
from io import StringIO
|
|
44
37
|
from itertools import zip_longest
|
|
45
38
|
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
|
|
@@ -49,11 +42,11 @@ from xml.etree import ElementTree
|
|
|
49
42
|
|
|
50
43
|
import requests
|
|
51
44
|
|
|
52
|
-
from rucio.common.config import config_get
|
|
53
|
-
from rucio.common.exception import
|
|
45
|
+
from rucio.common.config import config_get
|
|
46
|
+
from rucio.common.exception import DIDFilterSyntaxError, DuplicateCriteriaInDIDFilter, InputValidationError, InvalidType, MetalinkJsonParsingError, MissingModuleException, RucioException
|
|
54
47
|
from rucio.common.extra import import_extras
|
|
55
48
|
from rucio.common.plugins import PolicyPackageAlgorithms
|
|
56
|
-
from rucio.common.types import InternalAccount, InternalScope, TraceDict
|
|
49
|
+
from rucio.common.types import InternalAccount, InternalScope, LFNDict, TraceDict
|
|
57
50
|
|
|
58
51
|
EXTRA_MODULES = import_extras(['paramiko'])
|
|
59
52
|
|
|
@@ -61,14 +54,17 @@ if EXTRA_MODULES['paramiko']:
|
|
|
61
54
|
try:
|
|
62
55
|
from paramiko import RSAKey
|
|
63
56
|
except Exception:
|
|
64
|
-
EXTRA_MODULES['paramiko'] =
|
|
57
|
+
EXTRA_MODULES['paramiko'] = None
|
|
65
58
|
|
|
66
59
|
if TYPE_CHECKING:
|
|
60
|
+
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
|
|
67
61
|
T = TypeVar('T')
|
|
62
|
+
HashableKT = TypeVar('HashableKT')
|
|
63
|
+
HashableVT = TypeVar('HashableVT')
|
|
68
64
|
from _typeshed import FileDescriptorOrPath
|
|
69
65
|
from sqlalchemy.orm import Session
|
|
70
66
|
|
|
71
|
-
from rucio.common.types import
|
|
67
|
+
from rucio.common.types import LoggerFunction
|
|
72
68
|
|
|
73
69
|
|
|
74
70
|
# HTTP code dictionary. Not complete. Can be extended if needed.
|
|
@@ -101,7 +97,7 @@ codes = {
|
|
|
101
97
|
DATE_FORMAT = '%a, %d %b %Y %H:%M:%S UTC'
|
|
102
98
|
|
|
103
99
|
|
|
104
|
-
def invert_dict(d:
|
|
100
|
+
def invert_dict(d: "Mapping[HashableKT, HashableVT]") -> "Mapping[HashableVT, HashableKT]":
|
|
105
101
|
"""
|
|
106
102
|
Invert the dictionary.
|
|
107
103
|
CAUTION: this function is not deterministic unless the input dictionary is one-to-one mapping.
|
|
@@ -112,26 +108,6 @@ def invert_dict(d: dict[Any, Any]) -> dict[Any, Any]:
|
|
|
112
108
|
return {value: key for key, value in d.items()}
|
|
113
109
|
|
|
114
110
|
|
|
115
|
-
def dids_as_dicts(did_list: Iterable[Union[str, dict[str, str]]]) -> list[dict[str, str]]:
|
|
116
|
-
"""
|
|
117
|
-
Converts list of DIDs to list of dictionaries
|
|
118
|
-
:param did_list: list of DIDs as either "scope:name" or {"scope":"scope", "name":"name"}
|
|
119
|
-
:returns: list of dictionaries {"scope":"scope", "name":"name"}
|
|
120
|
-
"""
|
|
121
|
-
out = []
|
|
122
|
-
for did in did_list:
|
|
123
|
-
if isinstance(did, str):
|
|
124
|
-
scope, name = did.split(":", 1)
|
|
125
|
-
did = dict(scope=scope, name=name)
|
|
126
|
-
if isinstance(did, dict):
|
|
127
|
-
if not ("name" in did and "scope" in did):
|
|
128
|
-
raise ValueError("Scope or name missing in: %s" % (did,))
|
|
129
|
-
else:
|
|
130
|
-
raise ValueError("Can not convert item %s (%s) to a DID" % (did, type(did)))
|
|
131
|
-
out.append(did)
|
|
132
|
-
return out
|
|
133
|
-
|
|
134
|
-
|
|
135
111
|
def build_url(
|
|
136
112
|
url: str,
|
|
137
113
|
path: Optional[str] = None,
|
|
@@ -148,11 +124,27 @@ def build_url(
|
|
|
148
124
|
if path is not None:
|
|
149
125
|
complete_url += "/" + path
|
|
150
126
|
if params is not None:
|
|
151
|
-
complete_url +=
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
127
|
+
complete_url += _encode_params_as_url_query_string(params, doseq)
|
|
128
|
+
return complete_url
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _encode_params_as_url_query_string(
|
|
132
|
+
params: Union[str, dict[Any, Any], list[tuple[Any, Any]]],
|
|
133
|
+
doseq: bool
|
|
134
|
+
) -> str:
|
|
135
|
+
"""
|
|
136
|
+
Encode params into a URL query string.
|
|
137
|
+
|
|
138
|
+
:param params: the parameters to encode
|
|
139
|
+
:param doseq: if True, individual key=value pairs separated by '&' are generated for each element of the value sequence for the key
|
|
140
|
+
|
|
141
|
+
:returns: params as a URL query string
|
|
142
|
+
"""
|
|
143
|
+
complete_url = "?"
|
|
144
|
+
if isinstance(params, str):
|
|
145
|
+
complete_url += quote(params)
|
|
146
|
+
else:
|
|
147
|
+
complete_url += urlencode(params, doseq=doseq)
|
|
156
148
|
return complete_url
|
|
157
149
|
|
|
158
150
|
|
|
@@ -229,349 +221,6 @@ def generate_uuid_bytes() -> bytes:
|
|
|
229
221
|
return uuid().bytes
|
|
230
222
|
|
|
231
223
|
|
|
232
|
-
# GLOBALLY_SUPPORTED_CHECKSUMS = ['adler32', 'md5', 'sha256', 'crc32']
|
|
233
|
-
GLOBALLY_SUPPORTED_CHECKSUMS = ['adler32', 'md5']
|
|
234
|
-
CHECKSUM_ALGO_DICT = {}
|
|
235
|
-
PREFERRED_CHECKSUM = GLOBALLY_SUPPORTED_CHECKSUMS[0]
|
|
236
|
-
CHECKSUM_KEY = 'supported_checksums'
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def is_checksum_valid(checksum_name: str) -> bool:
|
|
240
|
-
"""
|
|
241
|
-
A simple function to check whether a checksum algorithm is supported.
|
|
242
|
-
Relies on GLOBALLY_SUPPORTED_CHECKSUMS to allow for expandability.
|
|
243
|
-
|
|
244
|
-
:param checksum_name: The name of the checksum to be verified.
|
|
245
|
-
:returns: True if checksum_name is in GLOBALLY_SUPPORTED_CHECKSUMS list, False otherwise.
|
|
246
|
-
"""
|
|
247
|
-
|
|
248
|
-
return checksum_name in GLOBALLY_SUPPORTED_CHECKSUMS
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def set_preferred_checksum(checksum_name: str) -> None:
|
|
252
|
-
"""
|
|
253
|
-
If the input checksum name is valid,
|
|
254
|
-
set it as PREFERRED_CHECKSUM.
|
|
255
|
-
|
|
256
|
-
:param checksum_name: The name of the checksum to be verified.
|
|
257
|
-
"""
|
|
258
|
-
if is_checksum_valid(checksum_name):
|
|
259
|
-
global PREFERRED_CHECKSUM
|
|
260
|
-
PREFERRED_CHECKSUM = checksum_name
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def adler32(file: "FileDescriptorOrPath") -> str:
|
|
264
|
-
"""
|
|
265
|
-
An Adler-32 checksum is obtained by calculating two 16-bit checksums A and B
|
|
266
|
-
and concatenating their bits into a 32-bit integer. A is the sum of all bytes in the
|
|
267
|
-
stream plus one, and B is the sum of the individual values of A from each step.
|
|
268
|
-
|
|
269
|
-
:param file: file name
|
|
270
|
-
:returns: Hexified string, padded to 8 values.
|
|
271
|
-
"""
|
|
272
|
-
|
|
273
|
-
# adler starting value is _not_ 0
|
|
274
|
-
adler = 1
|
|
275
|
-
|
|
276
|
-
can_mmap = False
|
|
277
|
-
# try:
|
|
278
|
-
# with open(file, 'r+b') as f:
|
|
279
|
-
# can_mmap = True
|
|
280
|
-
# except:
|
|
281
|
-
# pass
|
|
282
|
-
|
|
283
|
-
try:
|
|
284
|
-
# use mmap if possible
|
|
285
|
-
if can_mmap:
|
|
286
|
-
with open(file, 'r+b') as f:
|
|
287
|
-
m = mmap.mmap(f.fileno(), 0)
|
|
288
|
-
# partial block reads at slightly increased buffer sizes
|
|
289
|
-
for block in iter(partial(m.read, io.DEFAULT_BUFFER_SIZE * 8), b''):
|
|
290
|
-
adler = zlib.adler32(block, adler)
|
|
291
|
-
else:
|
|
292
|
-
with open(file, 'rb') as f:
|
|
293
|
-
# partial block reads at slightly increased buffer sizes
|
|
294
|
-
for block in iter(partial(f.read, io.DEFAULT_BUFFER_SIZE * 8), b''):
|
|
295
|
-
adler = zlib.adler32(block, adler)
|
|
296
|
-
|
|
297
|
-
except Exception as e:
|
|
298
|
-
raise Exception('FATAL - could not get Adler-32 checksum of file %s: %s' % (file, e))
|
|
299
|
-
|
|
300
|
-
# backflip on 32bit -- can be removed once everything is fully migrated to 64bit
|
|
301
|
-
if adler < 0:
|
|
302
|
-
adler = adler + 2 ** 32
|
|
303
|
-
|
|
304
|
-
return str('%08x' % adler)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
CHECKSUM_ALGO_DICT['adler32'] = adler32
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
def md5(file: "FileDescriptorOrPath") -> str:
|
|
311
|
-
"""
|
|
312
|
-
Runs the MD5 algorithm (RFC-1321) on the binary content of the file named file and returns the hexadecimal digest
|
|
313
|
-
|
|
314
|
-
:param file: file name
|
|
315
|
-
:returns: string of 32 hexadecimal digits
|
|
316
|
-
"""
|
|
317
|
-
hash_md5 = hashlib.md5()
|
|
318
|
-
try:
|
|
319
|
-
with open(file, "rb") as f:
|
|
320
|
-
list(map(hash_md5.update, iter(lambda: f.read(4096), b"")))
|
|
321
|
-
except Exception as e:
|
|
322
|
-
raise Exception('FATAL - could not get MD5 checksum of file %s - %s' % (file, e))
|
|
323
|
-
|
|
324
|
-
return hash_md5.hexdigest()
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
CHECKSUM_ALGO_DICT['md5'] = md5
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
def sha256(file: "FileDescriptorOrPath") -> str:
|
|
331
|
-
"""
|
|
332
|
-
Runs the SHA256 algorithm on the binary content of the file named file and returns the hexadecimal digest
|
|
333
|
-
|
|
334
|
-
:param file: file name
|
|
335
|
-
:returns: string of 32 hexadecimal digits
|
|
336
|
-
"""
|
|
337
|
-
with open(file, "rb") as f:
|
|
338
|
-
bytes_ = f.read() # read entire file as bytes
|
|
339
|
-
readable_hash = hashlib.sha256(bytes_).hexdigest()
|
|
340
|
-
print(readable_hash)
|
|
341
|
-
return readable_hash
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
CHECKSUM_ALGO_DICT['sha256'] = sha256
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
def crc32(file: "FileDescriptorOrPath") -> str:
|
|
348
|
-
"""
|
|
349
|
-
Runs the CRC32 algorithm on the binary content of the file named file and returns the hexadecimal digest
|
|
350
|
-
|
|
351
|
-
:param file: file name
|
|
352
|
-
:returns: string of 32 hexadecimal digits
|
|
353
|
-
"""
|
|
354
|
-
prev = 0
|
|
355
|
-
for eachLine in open(file, "rb"):
|
|
356
|
-
prev = zlib.crc32(eachLine, prev)
|
|
357
|
-
return "%X" % (prev & 0xFFFFFFFF)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
CHECKSUM_ALGO_DICT['crc32'] = crc32
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
def _next_pow2(num: int) -> int:
|
|
364
|
-
if not num:
|
|
365
|
-
return 0
|
|
366
|
-
return math.ceil(math.log2(num))
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
def _bittorrent_v2_piece_length_pow2(file_size: int) -> int:
|
|
370
|
-
"""
|
|
371
|
-
Automatically chooses the `piece size` so that `piece layers`
|
|
372
|
-
is kept small(er) than usually. This is a balancing act:
|
|
373
|
-
having a big piece_length requires more work on bittorrent client
|
|
374
|
-
side to validate hashes, but having it small requires more
|
|
375
|
-
place to store the `piece layers` in the database.
|
|
376
|
-
|
|
377
|
-
Returns the result as the exponent 'x' for power of 2.
|
|
378
|
-
To get the actual length in bytes, the caller should compute 2^x.
|
|
379
|
-
"""
|
|
380
|
-
|
|
381
|
-
# by the bittorrent v2 specification, the piece size is equal to block size = 16KiB
|
|
382
|
-
min_piece_len_pow2 = 14 # 2 ** 14 == 16 KiB
|
|
383
|
-
if not file_size:
|
|
384
|
-
return min_piece_len_pow2
|
|
385
|
-
# Limit the maximum size of pieces_layers hash chain for bittorrent v2,
|
|
386
|
-
# because we'll have to store it in the database
|
|
387
|
-
max_pieces_layers_size_pow2 = 20 # 2 ** 20 == 1 MiB
|
|
388
|
-
# sha256 requires 2 ** 5 == 32 Bytes == 256 bits
|
|
389
|
-
hash_size_pow2 = 5
|
|
390
|
-
|
|
391
|
-
# The closest power of two bigger than the file size
|
|
392
|
-
file_size_pow2 = _next_pow2(file_size)
|
|
393
|
-
|
|
394
|
-
# Compute the target size for the 'pieces layers' in the torrent
|
|
395
|
-
# (as power of two: the closest power-of-two smaller than the number)
|
|
396
|
-
# Will cap at max_pieces_layers_size for files larger than 1TB.
|
|
397
|
-
target_pieces_layers_size = math.sqrt(file_size)
|
|
398
|
-
target_pieces_layers_size_pow2 = min(math.floor(math.log2(target_pieces_layers_size)), max_pieces_layers_size_pow2)
|
|
399
|
-
target_piece_num_pow2 = max(target_pieces_layers_size_pow2 - hash_size_pow2, 0)
|
|
400
|
-
|
|
401
|
-
piece_length_pow2 = max(file_size_pow2 - target_piece_num_pow2, min_piece_len_pow2)
|
|
402
|
-
return piece_length_pow2
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
def bittorrent_v2_piece_length(file_size: int) -> int:
|
|
406
|
-
return 2 ** _bittorrent_v2_piece_length_pow2(file_size)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
def bittorrent_v2_merkle_sha256(file: "FileDescriptorOrPath") -> tuple[bytes, bytes, int]:
|
|
410
|
-
"""
|
|
411
|
-
Compute the .torrent v2 hash tree for the given file.
|
|
412
|
-
(http://www.bittorrent.org/beps/bep_0052.html)
|
|
413
|
-
In particular, it will return the root of the merkle hash
|
|
414
|
-
tree of the file, the 'piece layers' as described in the
|
|
415
|
-
previous BEP, and the chosen `piece size`
|
|
416
|
-
|
|
417
|
-
This function will read the file in chunks of 16KiB
|
|
418
|
-
(which is the imposed block size by bittorrent v2) and compute
|
|
419
|
-
the sha256 hash of each block. When enough blocks are read
|
|
420
|
-
to form a `piece`, will compute the merkle hash root of the
|
|
421
|
-
piece from the hashes of its blocks. At the end, the hashes
|
|
422
|
-
of pieces are combined to create the global pieces_root.
|
|
423
|
-
"""
|
|
424
|
-
|
|
425
|
-
# by the bittorrent v2 specification, the block size and the
|
|
426
|
-
# minimum piece size are both fixed to 16KiB
|
|
427
|
-
block_size = 16384
|
|
428
|
-
block_size_pow2 = 14 # 2 ** 14 == 16 KiB
|
|
429
|
-
# sha256 requires 2 ** 5 == 32 Bytes == 256 bits
|
|
430
|
-
hash_size = 32
|
|
431
|
-
|
|
432
|
-
def _merkle_root(leafs: list[bytes], nb_levels: int, padding: bytes) -> bytes:
|
|
433
|
-
"""
|
|
434
|
-
Build the root of the merkle hash tree from the (possibly incomplete) leafs layer.
|
|
435
|
-
If len(leafs) < 2 ** nb_levels, it will be padded with the padding repeated as many times
|
|
436
|
-
as needed to have 2 ** nb_levels leafs in total.
|
|
437
|
-
"""
|
|
438
|
-
nodes = copy.copy(leafs)
|
|
439
|
-
level = nb_levels
|
|
440
|
-
|
|
441
|
-
while level > 0:
|
|
442
|
-
for i in range(2 ** (level - 1)):
|
|
443
|
-
node1 = nodes[2 * i] if 2 * i < len(nodes) else padding
|
|
444
|
-
node2 = nodes[2 * i + 1] if 2 * i + 1 < len(nodes) else padding
|
|
445
|
-
h = hashlib.sha256(node1)
|
|
446
|
-
h.update(node2)
|
|
447
|
-
if i < len(nodes):
|
|
448
|
-
nodes[i] = h.digest()
|
|
449
|
-
else:
|
|
450
|
-
nodes.append(h.digest())
|
|
451
|
-
level -= 1
|
|
452
|
-
return nodes[0] if nodes else padding
|
|
453
|
-
|
|
454
|
-
file_size = os.stat(file).st_size
|
|
455
|
-
piece_length_pow2 = _bittorrent_v2_piece_length_pow2(file_size)
|
|
456
|
-
|
|
457
|
-
block_per_piece_pow2 = piece_length_pow2 - block_size_pow2
|
|
458
|
-
piece_length = 2 ** piece_length_pow2
|
|
459
|
-
block_per_piece = 2 ** block_per_piece_pow2
|
|
460
|
-
piece_num = math.ceil(file_size / piece_length)
|
|
461
|
-
|
|
462
|
-
remaining = file_size
|
|
463
|
-
remaining_in_block = min(file_size, block_size)
|
|
464
|
-
block_hashes = []
|
|
465
|
-
piece_hashes = []
|
|
466
|
-
current_hash = hashlib.sha256()
|
|
467
|
-
block_padding = bytes(hash_size)
|
|
468
|
-
with open(file, 'rb') as f:
|
|
469
|
-
while True:
|
|
470
|
-
data = f.read(remaining_in_block)
|
|
471
|
-
if not data:
|
|
472
|
-
break
|
|
473
|
-
|
|
474
|
-
current_hash.update(data)
|
|
475
|
-
|
|
476
|
-
remaining_in_block -= len(data)
|
|
477
|
-
remaining -= len(data)
|
|
478
|
-
|
|
479
|
-
if not remaining_in_block:
|
|
480
|
-
block_hashes.append(current_hash.digest())
|
|
481
|
-
if len(block_hashes) == block_per_piece or not remaining:
|
|
482
|
-
piece_hashes.append(_merkle_root(block_hashes, nb_levels=block_per_piece_pow2, padding=block_padding))
|
|
483
|
-
block_hashes = []
|
|
484
|
-
current_hash = hashlib.sha256()
|
|
485
|
-
remaining_in_block = min(block_size, remaining)
|
|
486
|
-
|
|
487
|
-
if not remaining:
|
|
488
|
-
break
|
|
489
|
-
|
|
490
|
-
if remaining or remaining_in_block or len(piece_hashes) != piece_num:
|
|
491
|
-
raise RucioException(f'Error while computing merkle sha256 of {file}')
|
|
492
|
-
|
|
493
|
-
piece_padding = _merkle_root([], nb_levels=block_per_piece_pow2, padding=block_padding)
|
|
494
|
-
pieces_root = _merkle_root(piece_hashes, nb_levels=_next_pow2(piece_num), padding=piece_padding)
|
|
495
|
-
pieces_layers = b''.join(piece_hashes) if len(piece_hashes) > 1 else b''
|
|
496
|
-
|
|
497
|
-
return pieces_root, pieces_layers, piece_length
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
def merkle_sha256(file: "FileDescriptorOrPath") -> str:
|
|
501
|
-
"""
|
|
502
|
-
The root of the sha256 merkle hash tree with leaf size of 16 KiB.
|
|
503
|
-
"""
|
|
504
|
-
pieces_root, _, _ = bittorrent_v2_merkle_sha256(file)
|
|
505
|
-
return pieces_root.hex()
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
CHECKSUM_ALGO_DICT['merkle_sha256'] = merkle_sha256
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
def bencode(obj: Union[int, bytes, str, list, dict[bytes, Any]]) -> bytes:
|
|
512
|
-
"""
|
|
513
|
-
Copied from the reference implementation of v2 bittorrent:
|
|
514
|
-
http://bittorrent.org/beps/bep_0052_torrent_creator.py
|
|
515
|
-
"""
|
|
516
|
-
|
|
517
|
-
if isinstance(obj, int):
|
|
518
|
-
return b"i" + str(obj).encode() + b"e"
|
|
519
|
-
elif isinstance(obj, bytes):
|
|
520
|
-
return str(len(obj)).encode() + b":" + obj
|
|
521
|
-
elif isinstance(obj, str):
|
|
522
|
-
return bencode(obj.encode("utf-8"))
|
|
523
|
-
elif isinstance(obj, list):
|
|
524
|
-
return b"l" + b"".join(map(bencode, obj)) + b"e"
|
|
525
|
-
elif isinstance(obj, dict):
|
|
526
|
-
if all(isinstance(i, bytes) for i in obj.keys()):
|
|
527
|
-
items = list(obj.items())
|
|
528
|
-
items.sort()
|
|
529
|
-
return b"d" + b"".join(map(bencode, itertools.chain(*items))) + b"e"
|
|
530
|
-
else:
|
|
531
|
-
raise ValueError("dict keys should be bytes " + str(obj.keys()))
|
|
532
|
-
raise ValueError("Allowed types: int, bytes, str, list, dict; not %s", type(obj))
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
def construct_torrent(
|
|
536
|
-
scope: str,
|
|
537
|
-
name: str,
|
|
538
|
-
length: int,
|
|
539
|
-
piece_length: int,
|
|
540
|
-
pieces_root: bytes,
|
|
541
|
-
pieces_layers: "Optional[bytes]" = None,
|
|
542
|
-
trackers: "Optional[list[str]]" = None,
|
|
543
|
-
) -> "tuple[str, bytes]":
|
|
544
|
-
|
|
545
|
-
torrent_dict = {
|
|
546
|
-
b'creation date': int(time.time()),
|
|
547
|
-
b'info': {
|
|
548
|
-
b'meta version': 2,
|
|
549
|
-
b'private': 1,
|
|
550
|
-
b'name': f'{scope}:{name}'.encode(),
|
|
551
|
-
b'piece length': piece_length,
|
|
552
|
-
b'file tree': {
|
|
553
|
-
name.encode(): {
|
|
554
|
-
b'': {
|
|
555
|
-
b'length': length,
|
|
556
|
-
b'pieces root': pieces_root,
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
},
|
|
561
|
-
b'piece layers': {},
|
|
562
|
-
}
|
|
563
|
-
if trackers:
|
|
564
|
-
torrent_dict[b'announce'] = trackers[0].encode()
|
|
565
|
-
if len(trackers) > 1:
|
|
566
|
-
torrent_dict[b'announce-list'] = [t.encode() for t in trackers]
|
|
567
|
-
if pieces_layers:
|
|
568
|
-
torrent_dict[b'piece layers'][pieces_root] = pieces_layers
|
|
569
|
-
|
|
570
|
-
torrent_id = hashlib.sha256(bencode(torrent_dict[b'info'])).hexdigest()[:40]
|
|
571
|
-
torrent = bencode(torrent_dict)
|
|
572
|
-
return torrent_id, torrent
|
|
573
|
-
|
|
574
|
-
|
|
575
224
|
def str_to_date(string: str) -> Optional[datetime.datetime]:
|
|
576
225
|
""" Converts a RFC-1123 string to the corresponding datetime value.
|
|
577
226
|
|
|
@@ -583,7 +232,7 @@ def str_to_date(string: str) -> Optional[datetime.datetime]:
|
|
|
583
232
|
def val_to_space_sep_str(vallist: list[str]) -> str:
|
|
584
233
|
""" Converts a list of values into a string of space separated values
|
|
585
234
|
|
|
586
|
-
:param vallist: the list of values to
|
|
235
|
+
:param vallist: the list of values to convert into string
|
|
587
236
|
:return: the string of space separated values or the value initially passed as parameter
|
|
588
237
|
"""
|
|
589
238
|
try:
|
|
@@ -591,7 +240,7 @@ def val_to_space_sep_str(vallist: list[str]) -> str:
|
|
|
591
240
|
return str(" ".join(vallist))
|
|
592
241
|
else:
|
|
593
242
|
return str(vallist)
|
|
594
|
-
except:
|
|
243
|
+
except Exception:
|
|
595
244
|
return ''
|
|
596
245
|
|
|
597
246
|
|
|
@@ -685,7 +334,7 @@ def rse_supported_protocol_domains() -> list[str]:
|
|
|
685
334
|
return ['lan', 'wan']
|
|
686
335
|
|
|
687
336
|
|
|
688
|
-
def grouper(iterable: Iterable[Any], n: int, fillvalue: Optional[object] = None) -> zip_longest:
|
|
337
|
+
def grouper(iterable: 'Iterable[Any]', n: int, fillvalue: Optional[object] = None) -> zip_longest:
|
|
689
338
|
""" Collect data into fixed-length chunks or blocks """
|
|
690
339
|
# grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx
|
|
691
340
|
args = [iter(iterable)] * n
|
|
@@ -708,7 +357,7 @@ def chunks(iterable, n):
|
|
|
708
357
|
yield chunk
|
|
709
358
|
|
|
710
359
|
|
|
711
|
-
def dict_chunks(dict_: dict[Any, Any], n: int) -> Iterator[dict[Any, Any]]:
|
|
360
|
+
def dict_chunks(dict_: dict[Any, Any], n: int) -> 'Iterator[dict[Any, Any]]':
|
|
712
361
|
"""
|
|
713
362
|
Iterate over the dictionary in groups of the requested size
|
|
714
363
|
"""
|
|
@@ -717,7 +366,7 @@ def dict_chunks(dict_: dict[Any, Any], n: int) -> Iterator[dict[Any, Any]]:
|
|
|
717
366
|
yield {k: dict_[k] for k in itertools.islice(it, n)}
|
|
718
367
|
|
|
719
368
|
|
|
720
|
-
def my_key_generator(namespace: str, fn: Callable, **kw) -> Callable[..., str]:
|
|
369
|
+
def my_key_generator(namespace: str, fn: 'Callable', **kw) -> 'Callable[..., str]':
|
|
721
370
|
"""
|
|
722
371
|
Customized key generator for dogpile
|
|
723
372
|
"""
|
|
@@ -764,19 +413,17 @@ class NonDeterministicPFNAlgorithms(PolicyPackageAlgorithms):
|
|
|
764
413
|
"""
|
|
765
414
|
Registers the included non-deterministic PFN algorithms
|
|
766
415
|
"""
|
|
767
|
-
cls.register('
|
|
768
|
-
cls.register('DQ2', cls.construct_non_deterministic_pfn_DQ2)
|
|
769
|
-
cls.register('BelleII', cls.construct_non_deterministic_pfn_BelleII)
|
|
416
|
+
cls.register('def', cls.construct_non_deterministic_pfn_default)
|
|
770
417
|
|
|
771
418
|
@classmethod
|
|
772
|
-
def get_algorithm(cls: type[NonDeterministicPFNAlgorithmsT], naming_convention: str) -> Callable[[str, Optional[str], str], str]:
|
|
419
|
+
def get_algorithm(cls: type[NonDeterministicPFNAlgorithmsT], naming_convention: str) -> 'Callable[[str, Optional[str], str], str]':
|
|
773
420
|
"""
|
|
774
421
|
Looks up a non-deterministic PFN algorithm by name
|
|
775
422
|
"""
|
|
776
423
|
return super()._get_one_algorithm(cls._algorithm_type, naming_convention)
|
|
777
424
|
|
|
778
425
|
@classmethod
|
|
779
|
-
def register(cls: type[NonDeterministicPFNAlgorithmsT], name: str, fn_construct_non_deterministic_pfn: Callable[[str, Optional[str], str], Optional[str]]) -> None:
|
|
426
|
+
def register(cls: type[NonDeterministicPFNAlgorithmsT], name: str, fn_construct_non_deterministic_pfn: 'Callable[[str, Optional[str], str], Optional[str]]') -> None:
|
|
780
427
|
"""
|
|
781
428
|
Register a new non-deterministic PFN algorithm
|
|
782
429
|
"""
|
|
@@ -820,7 +467,7 @@ class NonDeterministicPFNAlgorithms(PolicyPackageAlgorithms):
|
|
|
820
467
|
return stripped_tag
|
|
821
468
|
|
|
822
469
|
@staticmethod
|
|
823
|
-
def
|
|
470
|
+
def construct_non_deterministic_pfn_default(dsn: str, scope: Optional[str], filename: str) -> str:
|
|
824
471
|
"""
|
|
825
472
|
Defines relative PFN for new replicas. This method
|
|
826
473
|
contains DQ2 convention. To be used for non-deterministic sites.
|
|
@@ -858,44 +505,7 @@ class NonDeterministicPFNAlgorithms(PolicyPackageAlgorithms):
|
|
|
858
505
|
stripped_dsn = NonDeterministicPFNAlgorithms.__strip_dsn(dsn)
|
|
859
506
|
return '/%s/%s/%s/%s/%s' % (project, dataset_type, tag, stripped_dsn, filename)
|
|
860
507
|
|
|
861
|
-
@staticmethod
|
|
862
|
-
def construct_non_deterministic_pfn_T0(dsn: str, scope: Optional[str], filename: str) -> Optional[str]:
|
|
863
|
-
"""
|
|
864
|
-
Defines relative PFN for new replicas. This method
|
|
865
|
-
contains Tier0 convention. To be used for non-deterministic sites.
|
|
866
|
-
|
|
867
|
-
@return: relative PFN for new replica.
|
|
868
|
-
@rtype: str
|
|
869
|
-
"""
|
|
870
|
-
fields = dsn.split('.')
|
|
871
|
-
nfields = len(fields)
|
|
872
|
-
if nfields >= 3:
|
|
873
|
-
return '/%s/%s/%s/%s/%s' % (fields[0], fields[2], fields[1], dsn, filename)
|
|
874
|
-
elif nfields == 1:
|
|
875
|
-
return '/%s/%s/%s/%s/%s' % (fields[0], 'other', 'other', dsn, filename)
|
|
876
|
-
elif nfields == 2:
|
|
877
|
-
return '/%s/%s/%s/%s/%s' % (fields[0], fields[2], 'other', dsn, filename)
|
|
878
|
-
elif nfields == 0:
|
|
879
|
-
return '/other/other/other/other/%s' % (filename)
|
|
880
|
-
|
|
881
|
-
@staticmethod
|
|
882
|
-
def construct_non_deterministic_pfn_BelleII(dsn: str, scope: Optional[str], filename: str) -> str:
|
|
883
|
-
"""
|
|
884
|
-
Defines relative PFN for Belle II specific replicas.
|
|
885
|
-
This method contains the Belle II convention.
|
|
886
|
-
To be used for non-deterministic Belle II sites.
|
|
887
|
-
DSN (or datablock in the Belle II naming) contains /
|
|
888
|
-
"""
|
|
889
508
|
|
|
890
|
-
fields = dsn.split("/")
|
|
891
|
-
nfields = len(fields)
|
|
892
|
-
if nfields == 0:
|
|
893
|
-
return '/other/%s' % (filename)
|
|
894
|
-
else:
|
|
895
|
-
return '%s/%s' % (dsn, filename)
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
_DEFAULT_NON_DETERMINISTIC_PFN = 'DQ2'
|
|
899
509
|
NonDeterministicPFNAlgorithms._module_init_()
|
|
900
510
|
|
|
901
511
|
|
|
@@ -909,11 +519,11 @@ def construct_non_deterministic_pfn(dsn: str, scope: Optional[str], filename: st
|
|
|
909
519
|
"""
|
|
910
520
|
pfn_algorithms = NonDeterministicPFNAlgorithms()
|
|
911
521
|
if naming_convention is None or not NonDeterministicPFNAlgorithms.supports(naming_convention):
|
|
912
|
-
naming_convention =
|
|
522
|
+
naming_convention = 'def'
|
|
913
523
|
return pfn_algorithms.construct_non_deterministic_pfn(dsn, scope, filename, naming_convention)
|
|
914
524
|
|
|
915
525
|
|
|
916
|
-
def clean_pfns(pfns: Iterable[str]) -> list[str]:
|
|
526
|
+
def clean_pfns(pfns: 'Iterable[str]') -> list[str]:
|
|
917
527
|
res = []
|
|
918
528
|
for pfn in pfns:
|
|
919
529
|
if pfn.startswith('srm'):
|
|
@@ -946,7 +556,7 @@ class ScopeExtractionAlgorithms(PolicyPackageAlgorithms):
|
|
|
946
556
|
"""
|
|
947
557
|
super().__init__()
|
|
948
558
|
|
|
949
|
-
def extract_scope(self, did: str, scopes: Optional[Sequence[str]], extract_scope_convention: str) -> Sequence[str]:
|
|
559
|
+
def extract_scope(self, did: str, scopes: Optional['Sequence[str]'], extract_scope_convention: str) -> 'Sequence[str]':
|
|
950
560
|
"""
|
|
951
561
|
Calls the correct algorithm for scope extraction
|
|
952
562
|
"""
|
|
@@ -964,19 +574,18 @@ class ScopeExtractionAlgorithms(PolicyPackageAlgorithms):
|
|
|
964
574
|
"""
|
|
965
575
|
Registers the included scope extraction algorithms
|
|
966
576
|
"""
|
|
967
|
-
cls.register('
|
|
968
|
-
cls.register('belleii', cls.extract_scope_belleii)
|
|
577
|
+
cls.register('def', cls.extract_scope_default)
|
|
969
578
|
cls.register('dirac', cls.extract_scope_dirac)
|
|
970
579
|
|
|
971
580
|
@classmethod
|
|
972
|
-
def get_algorithm(cls: type[ScopeExtractionAlgorithmsT], extract_scope_convention: str) -> Callable[[str, Optional[Sequence[str]]], Sequence[str]]:
|
|
581
|
+
def get_algorithm(cls: type[ScopeExtractionAlgorithmsT], extract_scope_convention: str) -> 'Callable[[str, Optional[Sequence[str]]], Sequence[str]]':
|
|
973
582
|
"""
|
|
974
583
|
Looks up a scope extraction algorithm by name
|
|
975
584
|
"""
|
|
976
585
|
return super()._get_one_algorithm(cls._algorithm_type, extract_scope_convention)
|
|
977
586
|
|
|
978
587
|
@classmethod
|
|
979
|
-
def register(cls: type[ScopeExtractionAlgorithmsT], name: str, fn_extract_scope: Callable[[str, Optional[Sequence[str]]], Sequence[str]]) -> None:
|
|
588
|
+
def register(cls: type[ScopeExtractionAlgorithmsT], name: str, fn_extract_scope: 'Callable[[str, Optional[Sequence[str]]], Sequence[str]]') -> None:
|
|
980
589
|
"""
|
|
981
590
|
Registers a new scope extraction algorithm
|
|
982
591
|
"""
|
|
@@ -984,8 +593,14 @@ class ScopeExtractionAlgorithms(PolicyPackageAlgorithms):
|
|
|
984
593
|
super()._register(cls._algorithm_type, algorithm_dict)
|
|
985
594
|
|
|
986
595
|
@staticmethod
|
|
987
|
-
def
|
|
988
|
-
|
|
596
|
+
def extract_scope_default(did: str, scopes: Optional['Sequence[str]']) -> 'Sequence[str]':
|
|
597
|
+
"""
|
|
598
|
+
Default fallback scope extraction algorithm, based on the ATLAS scope extraction algorithm.
|
|
599
|
+
|
|
600
|
+
:param did: The DID to extract the scope from.
|
|
601
|
+
|
|
602
|
+
:returns: A tuple containing the extracted scope and the DID.
|
|
603
|
+
"""
|
|
989
604
|
if did.find(':') > -1:
|
|
990
605
|
if len(did.split(':')) > 2:
|
|
991
606
|
raise RucioException('Too many colons. Cannot extract scope and name')
|
|
@@ -1002,7 +617,7 @@ class ScopeExtractionAlgorithms(PolicyPackageAlgorithms):
|
|
|
1002
617
|
return scope, did
|
|
1003
618
|
|
|
1004
619
|
@staticmethod
|
|
1005
|
-
def extract_scope_dirac(did: str, scopes: Optional[Sequence[str]]) -> Sequence[str]:
|
|
620
|
+
def extract_scope_dirac(did: str, scopes: Optional['Sequence[str]']) -> 'Sequence[str]':
|
|
1006
621
|
# Default dirac scope extract algorithm. Scope is the second element in the LFN or the first one (VO name)
|
|
1007
622
|
# if only one element is the result of a split.
|
|
1008
623
|
elem = did.rstrip('/').split('/')
|
|
@@ -1012,91 +627,15 @@ class ScopeExtractionAlgorithms(PolicyPackageAlgorithms):
|
|
|
1012
627
|
scope = elem[1]
|
|
1013
628
|
return scope, did
|
|
1014
629
|
|
|
1015
|
-
|
|
1016
|
-
def extract_scope_belleii(did: str, scopes: Optional[Sequence[str]]) -> Sequence[str]:
|
|
1017
|
-
split_did = did.split('/')
|
|
1018
|
-
if did.startswith('/belle/mock/'):
|
|
1019
|
-
return 'mock', did
|
|
1020
|
-
if did.startswith('/belle/MC/'):
|
|
1021
|
-
if did.startswith('/belle/MC/BG') or \
|
|
1022
|
-
did.startswith('/belle/MC/build') or \
|
|
1023
|
-
did.startswith('/belle/MC/generic') or \
|
|
1024
|
-
did.startswith('/belle/MC/log') or \
|
|
1025
|
-
did.startswith('/belle/MC/mcprod') or \
|
|
1026
|
-
did.startswith('/belle/MC/prerelease') or \
|
|
1027
|
-
did.startswith('/belle/MC/release'):
|
|
1028
|
-
return 'mc', did
|
|
1029
|
-
if did.startswith('/belle/MC/cert') or \
|
|
1030
|
-
did.startswith('/belle/MC/dirac') or \
|
|
1031
|
-
did.startswith('/belle/MC/dr3') or \
|
|
1032
|
-
did.startswith('/belle/MC/fab') or \
|
|
1033
|
-
did.startswith('/belle/MC/hideki') or \
|
|
1034
|
-
did.startswith('/belle/MC/merge') or \
|
|
1035
|
-
did.startswith('/belle/MC/migration') or \
|
|
1036
|
-
did.startswith('/belle/MC/skim') or \
|
|
1037
|
-
did.startswith('/belle/MC/test'):
|
|
1038
|
-
return 'mc_tmp', did
|
|
1039
|
-
if len(split_did) > 4:
|
|
1040
|
-
if split_did[3].find('fab') > -1 or split_did[3].find('merge') > -1 or split_did[3].find('skim') > -1:
|
|
1041
|
-
return 'mc_tmp', did
|
|
1042
|
-
if split_did[3].find('release') > -1:
|
|
1043
|
-
return 'mc', did
|
|
1044
|
-
return 'mc_tmp', did
|
|
1045
|
-
if did.startswith('/belle/Raw/'):
|
|
1046
|
-
return 'raw', did
|
|
1047
|
-
if did.startswith('/belle/hRaw'):
|
|
1048
|
-
return 'hraw', did
|
|
1049
|
-
if did.startswith('/belle/user/'):
|
|
1050
|
-
if len(split_did) > 4:
|
|
1051
|
-
if len(split_did[3]) == 1 and scopes is not None and 'user.%s' % (split_did[4]) in scopes:
|
|
1052
|
-
return 'user.%s' % split_did[4], did
|
|
1053
|
-
if len(split_did) > 3:
|
|
1054
|
-
if scopes is not None and 'user.%s' % (split_did[3]) in scopes:
|
|
1055
|
-
return 'user.%s' % split_did[3], did
|
|
1056
|
-
return 'user', did
|
|
1057
|
-
if did.startswith('/belle/group/'):
|
|
1058
|
-
if len(split_did) > 4:
|
|
1059
|
-
if scopes is not None and 'group.%s' % (split_did[4]) in scopes:
|
|
1060
|
-
return 'group.%s' % split_did[4], did
|
|
1061
|
-
return 'group', did
|
|
1062
|
-
if did.startswith('/belle/data/') or did.startswith('/belle/Data/'):
|
|
1063
|
-
if len(split_did) > 4:
|
|
1064
|
-
if split_did[3] in ['fab', 'skim']: # /belle/Data/fab --> data_tmp
|
|
1065
|
-
return 'data_tmp', did
|
|
1066
|
-
if split_did[3].find('release') > -1: # /belle/Data/release --> data
|
|
1067
|
-
return 'data', did
|
|
1068
|
-
if len(split_did) > 5:
|
|
1069
|
-
if split_did[3] in ['proc']: # /belle/Data/proc
|
|
1070
|
-
if split_did[4].find('release') > -1: # /belle/Data/proc/release*
|
|
1071
|
-
if len(split_did) > 7 and split_did[6] in ['GCR2c', 'prod00000007', 'prod6b', 'proc7b',
|
|
1072
|
-
'proc8b', 'Bucket4', 'Bucket6test', 'bucket6',
|
|
1073
|
-
'proc9', 'bucket7', 'SKIMDATAx1', 'proc10Valid',
|
|
1074
|
-
'proc10', 'SkimP10x1', 'SkimP11x1', 'SkimB9x1',
|
|
1075
|
-
'SkimB10x1', 'SkimB11x1']: # /belle/Data/proc/release*/*/proc10/* --> data_tmp (Old convention)
|
|
1076
|
-
return 'data_tmp', did
|
|
1077
|
-
else: # /belle/Data/proc/release*/*/proc11/* --> data (New convention)
|
|
1078
|
-
return 'data', did
|
|
1079
|
-
if split_did[4].find('fab') > -1: # /belle/Data/proc/fab* --> data_tmp
|
|
1080
|
-
return 'data_tmp', did
|
|
1081
|
-
return 'data_tmp', did
|
|
1082
|
-
if did.startswith('/belle/ddm/functional_tests/') or did.startswith('/belle/ddm/tests/') or did.startswith('/belle/test/ddm_test'):
|
|
1083
|
-
return 'test', did
|
|
1084
|
-
if did.startswith('/belle/BG/'):
|
|
1085
|
-
return 'data', did
|
|
1086
|
-
if did.startswith('/belle/collection'):
|
|
1087
|
-
return 'collection', did
|
|
1088
|
-
return 'other', did
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
_DEFAULT_EXTRACT = 'atlas'
|
|
630
|
+
|
|
1092
631
|
ScopeExtractionAlgorithms._module_init_()
|
|
1093
632
|
|
|
1094
633
|
|
|
1095
634
|
def extract_scope(
|
|
1096
635
|
did: str,
|
|
1097
|
-
scopes: Optional[Sequence[str]] = None,
|
|
1098
|
-
default_extract: str =
|
|
1099
|
-
) -> Sequence[str]:
|
|
636
|
+
scopes: Optional['Sequence[str]'] = None,
|
|
637
|
+
default_extract: str = 'def'
|
|
638
|
+
) -> 'Sequence[str]':
|
|
1100
639
|
scope_extraction_algorithms = ScopeExtractionAlgorithms()
|
|
1101
640
|
extract_scope_convention = config_get('common', 'extract_scope', False, None) or config_get('policy', 'extract_scope', False, None)
|
|
1102
641
|
if extract_scope_convention is None or not ScopeExtractionAlgorithms.supports(extract_scope_convention):
|
|
@@ -1228,62 +767,6 @@ def resolve_ip(hostname: str) -> str:
|
|
|
1228
767
|
return hostname
|
|
1229
768
|
|
|
1230
769
|
|
|
1231
|
-
def detect_client_location() -> "IPDict":
|
|
1232
|
-
"""
|
|
1233
|
-
Normally client IP will be set on the server side (request.remote_addr)
|
|
1234
|
-
Here setting ip on the one seen by the host itself. There is no connection
|
|
1235
|
-
to Google DNS servers.
|
|
1236
|
-
Try to determine the sitename automatically from common environment variables,
|
|
1237
|
-
in this order: SITE_NAME, ATLAS_SITE_NAME, OSG_SITE_NAME. If none of these exist
|
|
1238
|
-
use the fixed string 'ROAMING'.
|
|
1239
|
-
|
|
1240
|
-
If environment variables sets location, it uses it.
|
|
1241
|
-
"""
|
|
1242
|
-
|
|
1243
|
-
ip = None
|
|
1244
|
-
|
|
1245
|
-
try:
|
|
1246
|
-
with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as s:
|
|
1247
|
-
s.connect(("2001:4860:4860:0:0:0:0:8888", 80))
|
|
1248
|
-
ip = s.getsockname()[0]
|
|
1249
|
-
except Exception:
|
|
1250
|
-
pass
|
|
1251
|
-
|
|
1252
|
-
if not ip:
|
|
1253
|
-
try:
|
|
1254
|
-
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
|
1255
|
-
s.connect(("8.8.8.8", 80))
|
|
1256
|
-
ip = s.getsockname()[0]
|
|
1257
|
-
except Exception:
|
|
1258
|
-
pass
|
|
1259
|
-
|
|
1260
|
-
if not ip:
|
|
1261
|
-
ip = '0.0.0.0' # noqa: S104
|
|
1262
|
-
|
|
1263
|
-
site = os.environ.get('SITE_NAME',
|
|
1264
|
-
os.environ.get('ATLAS_SITE_NAME',
|
|
1265
|
-
os.environ.get('OSG_SITE_NAME',
|
|
1266
|
-
'ROAMING')))
|
|
1267
|
-
|
|
1268
|
-
latitude = os.environ.get('RUCIO_LATITUDE')
|
|
1269
|
-
longitude = os.environ.get('RUCIO_LONGITUDE')
|
|
1270
|
-
if latitude and longitude:
|
|
1271
|
-
try:
|
|
1272
|
-
latitude = float(latitude)
|
|
1273
|
-
longitude = float(longitude)
|
|
1274
|
-
except ValueError:
|
|
1275
|
-
latitude = longitude = 0
|
|
1276
|
-
print('Client set latitude and longitude are not valid.')
|
|
1277
|
-
else:
|
|
1278
|
-
latitude = longitude = None
|
|
1279
|
-
|
|
1280
|
-
return {'ip': ip,
|
|
1281
|
-
'fqdn': socket.getfqdn(),
|
|
1282
|
-
'site': site,
|
|
1283
|
-
'latitude': latitude,
|
|
1284
|
-
'longitude': longitude}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
770
|
def ssh_sign(private_key: str, message: str) -> str:
|
|
1288
771
|
"""
|
|
1289
772
|
Sign a string message using the private key.
|
|
@@ -1305,7 +788,7 @@ def ssh_sign(private_key: str, message: str) -> str:
|
|
|
1305
788
|
return base64_encoded
|
|
1306
789
|
|
|
1307
790
|
|
|
1308
|
-
def make_valid_did(lfn_dict:
|
|
791
|
+
def make_valid_did(lfn_dict: LFNDict) -> LFNDict:
|
|
1309
792
|
"""
|
|
1310
793
|
When managing information about a LFN (such as in `rucio upload` or
|
|
1311
794
|
the RSE manager's upload), we add the `filename` attribute to record
|
|
@@ -1322,7 +805,7 @@ def make_valid_did(lfn_dict: dict[str, Any]) -> dict[str, Any]:
|
|
|
1322
805
|
lfn_copy = dict(lfn_dict)
|
|
1323
806
|
lfn_copy['name'] = lfn_copy.get('name', lfn_copy['filename'])
|
|
1324
807
|
del lfn_copy['filename']
|
|
1325
|
-
return lfn_copy
|
|
808
|
+
return lfn_copy # type: ignore
|
|
1326
809
|
|
|
1327
810
|
|
|
1328
811
|
def send_trace(trace: TraceDict, trace_endpoint: str, user_agent: str, retries: int = 5) -> int:
|
|
@@ -1479,7 +962,7 @@ def parse_did_filter_from_string_fe(
|
|
|
1479
962
|
"""
|
|
1480
963
|
# lookup table unifying all comprehended operators to a nominal suffix.
|
|
1481
964
|
# note that the order matters as the regex engine is eager, e.g. don't want to evaluate '<=' as '<' and '='.
|
|
1482
|
-
|
|
965
|
+
operators_suffix_lut = OrderedDict({
|
|
1483
966
|
'<=': 'lte',
|
|
1484
967
|
'>=': 'gte',
|
|
1485
968
|
'==': '',
|
|
@@ -1490,11 +973,11 @@ def parse_did_filter_from_string_fe(
|
|
|
1490
973
|
})
|
|
1491
974
|
|
|
1492
975
|
# lookup table mapping operator opposites, used to reverse compound inequalities.
|
|
1493
|
-
|
|
976
|
+
operator_opposites_lut = {
|
|
1494
977
|
'lt': 'gt',
|
|
1495
978
|
'lte': 'gte'
|
|
1496
979
|
}
|
|
1497
|
-
|
|
980
|
+
operator_opposites_lut.update({op2: op1 for op1, op2 in operator_opposites_lut.items()})
|
|
1498
981
|
|
|
1499
982
|
filters = []
|
|
1500
983
|
if input_string:
|
|
@@ -1506,13 +989,13 @@ def parse_did_filter_from_string_fe(
|
|
|
1506
989
|
for and_group in and_groups:
|
|
1507
990
|
and_group = and_group.strip()
|
|
1508
991
|
# tokenise this AND clause using operators as delimiters.
|
|
1509
|
-
tokenisation_regex = "({})".format('|'.join(
|
|
992
|
+
tokenisation_regex = "({})".format('|'.join(operators_suffix_lut.keys()))
|
|
1510
993
|
and_group_split_by_operator = list(filter(None, re.split(tokenisation_regex, and_group)))
|
|
1511
994
|
if len(and_group_split_by_operator) == 3: # this is a one-sided inequality or expression
|
|
1512
995
|
key, operator, value = [token.strip() for token in and_group_split_by_operator]
|
|
1513
996
|
|
|
1514
997
|
# substitute input operator with the nominal operator defined by the LUT, <operators_suffix_LUT>.
|
|
1515
|
-
operator_mapped =
|
|
998
|
+
operator_mapped = operators_suffix_lut.get(operator)
|
|
1516
999
|
|
|
1517
1000
|
filter_key_full = key
|
|
1518
1001
|
if operator_mapped is not None:
|
|
@@ -1529,8 +1012,8 @@ def parse_did_filter_from_string_fe(
|
|
|
1529
1012
|
value1, operator1, key, operator2, value2 = [token.strip() for token in and_group_split_by_operator]
|
|
1530
1013
|
|
|
1531
1014
|
# substitute input operator with the nominal operator defined by the LUT, <operators_suffix_LUT>.
|
|
1532
|
-
operator1_mapped =
|
|
1533
|
-
operator2_mapped =
|
|
1015
|
+
operator1_mapped = operator_opposites_lut.get(operators_suffix_lut.get(operator1))
|
|
1016
|
+
operator2_mapped = operators_suffix_lut.get(operator2)
|
|
1534
1017
|
|
|
1535
1018
|
filter_key1_full = filter_key2_full = key
|
|
1536
1019
|
if operator1_mapped is not None and operator2_mapped is not None:
|
|
@@ -1665,7 +1148,7 @@ def parse_replicas_metalink(root: ElementTree.Element) -> list[dict[str, Any]]:
|
|
|
1665
1148
|
|
|
1666
1149
|
def get_thread_with_periodic_running_function(
|
|
1667
1150
|
interval: Union[int, float],
|
|
1668
|
-
action: Callable[..., Any],
|
|
1151
|
+
action: 'Callable[..., Any]',
|
|
1669
1152
|
graceful_stop: threading.Event
|
|
1670
1153
|
) -> threading.Thread:
|
|
1671
1154
|
"""
|
|
@@ -1816,8 +1299,8 @@ def setup_logger(
|
|
|
1816
1299
|
def add_handler(logger: logging.Logger) -> None:
|
|
1817
1300
|
hdlr = logging.StreamHandler()
|
|
1818
1301
|
|
|
1819
|
-
def emit_decorator(fnc: Callable[..., Any]) -> Callable[..., Any]:
|
|
1820
|
-
def func(*args) -> Callable[..., Any]:
|
|
1302
|
+
def emit_decorator(fnc: 'Callable[..., Any]') -> 'Callable[..., Any]':
|
|
1303
|
+
def func(*args) -> 'Callable[..., Any]':
|
|
1821
1304
|
if 'RUCIO_LOGGING_FORMAT' not in os.environ:
|
|
1822
1305
|
levelno = args[0].levelno
|
|
1823
1306
|
format_str = '%(asctime)s\t%(levelname)s\t%(message)s\033[0m'
|
|
@@ -1864,36 +1347,10 @@ def daemon_sleep(
|
|
|
1864
1347
|
graceful_stop.wait(sleep_time - time_diff)
|
|
1865
1348
|
|
|
1866
1349
|
|
|
1867
|
-
|
|
1868
|
-
""""
|
|
1869
|
-
Checks if the function is called from a client or from a server/daemon
|
|
1870
|
-
|
|
1871
|
-
:returns client_mode: True if is called from a client, False if it is called from a server/daemon
|
|
1872
|
-
"""
|
|
1873
|
-
if 'RUCIO_CLIENT_MODE' not in os.environ:
|
|
1874
|
-
try:
|
|
1875
|
-
if config_has_section('database'):
|
|
1876
|
-
client_mode = False
|
|
1877
|
-
elif config_has_section('client'):
|
|
1878
|
-
client_mode = True
|
|
1879
|
-
else:
|
|
1880
|
-
client_mode = False
|
|
1881
|
-
except (RuntimeError, ConfigNotFound):
|
|
1882
|
-
# If no configuration file is found the default value should be True
|
|
1883
|
-
client_mode = True
|
|
1884
|
-
else:
|
|
1885
|
-
if os.environ['RUCIO_CLIENT_MODE']:
|
|
1886
|
-
client_mode = True
|
|
1887
|
-
else:
|
|
1888
|
-
client_mode = False
|
|
1889
|
-
|
|
1890
|
-
return client_mode
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
class retry:
|
|
1350
|
+
class retry: # noqa: N801
|
|
1894
1351
|
"""Retry callable object with configuragle number of attempts"""
|
|
1895
1352
|
|
|
1896
|
-
def __init__(self, func: Callable[..., Any], *args, **kwargs):
|
|
1353
|
+
def __init__(self, func: 'Callable[..., Any]', *args, **kwargs):
|
|
1897
1354
|
'''
|
|
1898
1355
|
:param func: a method that should be executed with retries
|
|
1899
1356
|
:param args: parameters of the func
|
|
@@ -1901,7 +1358,7 @@ class retry:
|
|
|
1901
1358
|
'''
|
|
1902
1359
|
self.func, self.args, self.kwargs = func, args, kwargs
|
|
1903
1360
|
|
|
1904
|
-
def __call__(self, mtries: int = 3, logger: "LoggerFunction" = logging.log) -> Callable[..., Any]:
|
|
1361
|
+
def __call__(self, mtries: int = 3, logger: "LoggerFunction" = logging.log) -> 'Callable[..., Any]':
|
|
1905
1362
|
'''
|
|
1906
1363
|
:param mtries: maximum number of attempts to execute the function
|
|
1907
1364
|
:param logger: preferred logger
|
|
@@ -1927,7 +1384,7 @@ class StoreAndDeprecateWarningAction(argparse.Action):
|
|
|
1927
1384
|
'''
|
|
1928
1385
|
|
|
1929
1386
|
def __init__(self,
|
|
1930
|
-
option_strings: Sequence[str],
|
|
1387
|
+
option_strings: 'Sequence[str]',
|
|
1931
1388
|
new_option_string: str,
|
|
1932
1389
|
dest: str,
|
|
1933
1390
|
**kwargs):
|
|
@@ -1961,7 +1418,7 @@ class StoreTrueAndDeprecateWarningAction(argparse._StoreConstAction):
|
|
|
1961
1418
|
'''
|
|
1962
1419
|
|
|
1963
1420
|
def __init__(self,
|
|
1964
|
-
option_strings: Sequence[str],
|
|
1421
|
+
option_strings: 'Sequence[str]',
|
|
1965
1422
|
new_option_string: str,
|
|
1966
1423
|
dest: str,
|
|
1967
1424
|
default: bool = False,
|
|
@@ -2095,30 +1552,6 @@ class PriorityQueue:
|
|
|
2095
1552
|
return heap_changed
|
|
2096
1553
|
|
|
2097
1554
|
|
|
2098
|
-
def check_policy_package_version(package: str) -> None:
|
|
2099
|
-
import importlib
|
|
2100
|
-
|
|
2101
|
-
from rucio.version import version_string
|
|
2102
|
-
'''
|
|
2103
|
-
Checks that the Rucio version supported by the policy package is compatible
|
|
2104
|
-
with this version. Raises an exception if not.
|
|
2105
|
-
:param package: the fully qualified name of the policy package
|
|
2106
|
-
'''
|
|
2107
|
-
try:
|
|
2108
|
-
module = importlib.import_module(package)
|
|
2109
|
-
except ImportError:
|
|
2110
|
-
# package not found. Will be picked up elsewhere
|
|
2111
|
-
return
|
|
2112
|
-
if not hasattr(module, 'SUPPORTED_VERSION'):
|
|
2113
|
-
# package is not versioned
|
|
2114
|
-
return
|
|
2115
|
-
supported_version = module.SUPPORTED_VERSION if isinstance(module.SUPPORTED_VERSION, list) else [module.SUPPORTED_VERSION]
|
|
2116
|
-
components = 2 if version_string().startswith("1.") else 1
|
|
2117
|
-
current_version = ".".join(version_string().split(".")[:components])
|
|
2118
|
-
if current_version not in supported_version:
|
|
2119
|
-
raise PolicyPackageVersionError(package)
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
1555
|
class Availability:
|
|
2123
1556
|
"""
|
|
2124
1557
|
This util class acts as a translator between the availability stored as
|
|
@@ -2236,3 +1669,20 @@ def deep_merge_dict(source: dict, destination: dict) -> dict:
|
|
|
2236
1669
|
destination[key] = value
|
|
2237
1670
|
|
|
2238
1671
|
return destination
|
|
1672
|
+
|
|
1673
|
+
|
|
1674
|
+
def is_method_overridden(obj, base_cls, method_name):
|
|
1675
|
+
"""
|
|
1676
|
+
Return True if `obj` (an instance of a subclass of `base_cls`) has overridden the given method_name from base_cls.
|
|
1677
|
+
That is, `type(obj).<method_name>` is not the same function object as `base_cls.<method_name>`.
|
|
1678
|
+
|
|
1679
|
+
:param obj: An instance of (a subclass of) base_cls.
|
|
1680
|
+
:param base_cls: The base class which may define the method.
|
|
1681
|
+
:param method_name: Name of the method (str) to check.
|
|
1682
|
+
:returns: Boolean, True if the subclass provides a real override.
|
|
1683
|
+
"""
|
|
1684
|
+
if not hasattr(obj, method_name):
|
|
1685
|
+
return False
|
|
1686
|
+
if getattr(type(obj), method_name, None) is getattr(base_cls, method_name, None): # Caring for bound/unbound cases
|
|
1687
|
+
return False
|
|
1688
|
+
return True
|