rucio-clients 32.8.6__py3-none-any.whl → 35.8.0__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 +0 -1
- rucio/alembicrevision.py +1 -2
- rucio/client/__init__.py +0 -1
- rucio/client/accountclient.py +45 -25
- rucio/client/accountlimitclient.py +37 -9
- rucio/client/baseclient.py +199 -154
- rucio/client/client.py +2 -3
- rucio/client/configclient.py +19 -6
- rucio/client/credentialclient.py +9 -4
- rucio/client/didclient.py +238 -63
- rucio/client/diracclient.py +13 -5
- rucio/client/downloadclient.py +162 -51
- rucio/client/exportclient.py +4 -4
- rucio/client/fileclient.py +3 -4
- rucio/client/importclient.py +4 -4
- rucio/client/lifetimeclient.py +21 -5
- rucio/client/lockclient.py +18 -8
- rucio/client/{metaclient.py → metaconventionsclient.py} +18 -15
- rucio/client/pingclient.py +0 -1
- rucio/client/replicaclient.py +15 -5
- rucio/client/requestclient.py +35 -19
- rucio/client/rseclient.py +133 -51
- rucio/client/ruleclient.py +29 -22
- rucio/client/scopeclient.py +8 -6
- rucio/client/subscriptionclient.py +47 -35
- rucio/client/touchclient.py +8 -4
- rucio/client/uploadclient.py +166 -82
- rucio/common/__init__.py +0 -1
- rucio/common/cache.py +4 -4
- rucio/common/config.py +52 -47
- rucio/common/constants.py +69 -2
- rucio/common/constraints.py +0 -1
- rucio/common/didtype.py +24 -22
- rucio/common/exception.py +281 -222
- rucio/common/extra.py +0 -1
- rucio/common/logging.py +54 -38
- rucio/common/pcache.py +122 -101
- rucio/common/plugins.py +153 -0
- rucio/common/policy.py +4 -4
- rucio/common/schema/__init__.py +17 -10
- rucio/common/schema/atlas.py +7 -5
- rucio/common/schema/belleii.py +7 -5
- rucio/common/schema/domatpc.py +7 -5
- rucio/common/schema/escape.py +7 -5
- rucio/common/schema/generic.py +8 -6
- rucio/common/schema/generic_multi_vo.py +7 -5
- rucio/common/schema/icecube.py +7 -5
- rucio/common/stomp_utils.py +0 -1
- rucio/common/stopwatch.py +0 -1
- rucio/common/test_rucio_server.py +2 -2
- rucio/common/types.py +262 -17
- rucio/common/utils.py +743 -451
- rucio/rse/__init__.py +3 -4
- rucio/rse/protocols/__init__.py +0 -1
- rucio/rse/protocols/bittorrent.py +184 -0
- rucio/rse/protocols/cache.py +1 -2
- rucio/rse/protocols/dummy.py +1 -2
- rucio/rse/protocols/gfal.py +12 -10
- rucio/rse/protocols/globus.py +7 -7
- rucio/rse/protocols/gsiftp.py +2 -3
- rucio/rse/protocols/http_cache.py +1 -2
- rucio/rse/protocols/mock.py +1 -2
- rucio/rse/protocols/ngarc.py +1 -2
- rucio/rse/protocols/posix.py +12 -13
- rucio/rse/protocols/protocol.py +116 -52
- rucio/rse/protocols/rclone.py +6 -7
- rucio/rse/protocols/rfio.py +4 -5
- rucio/rse/protocols/srm.py +9 -10
- rucio/rse/protocols/ssh.py +8 -9
- rucio/rse/protocols/storm.py +2 -3
- rucio/rse/protocols/webdav.py +17 -14
- rucio/rse/protocols/xrootd.py +23 -17
- rucio/rse/rsemanager.py +19 -7
- rucio/vcsversion.py +4 -4
- rucio/version.py +5 -13
- rucio_clients-35.8.0.data/data/requirements.client.txt +15 -0
- {rucio_clients-32.8.6.data → rucio_clients-35.8.0.data}/data/rucio_client/merge_rucio_configs.py +2 -5
- {rucio_clients-32.8.6.data → rucio_clients-35.8.0.data}/scripts/rucio +87 -85
- {rucio_clients-32.8.6.data → rucio_clients-35.8.0.data}/scripts/rucio-admin +45 -32
- {rucio_clients-32.8.6.dist-info → rucio_clients-35.8.0.dist-info}/METADATA +13 -13
- rucio_clients-35.8.0.dist-info/RECORD +88 -0
- {rucio_clients-32.8.6.dist-info → rucio_clients-35.8.0.dist-info}/WHEEL +1 -1
- {rucio_clients-32.8.6.dist-info → rucio_clients-35.8.0.dist-info}/licenses/AUTHORS.rst +3 -0
- rucio/common/schema/cms.py +0 -478
- rucio/common/schema/lsst.py +0 -423
- rucio_clients-32.8.6.data/data/requirements.txt +0 -55
- rucio_clients-32.8.6.dist-info/RECORD +0 -88
- {rucio_clients-32.8.6.data → rucio_clients-35.8.0.data}/data/etc/rse-accounts.cfg.template +0 -0
- {rucio_clients-32.8.6.data → rucio_clients-35.8.0.data}/data/etc/rucio.cfg.atlas.client.template +0 -0
- {rucio_clients-32.8.6.data → rucio_clients-35.8.0.data}/data/etc/rucio.cfg.template +0 -0
- {rucio_clients-32.8.6.dist-info → rucio_clients-35.8.0.dist-info}/licenses/LICENSE +0 -0
- {rucio_clients-32.8.6.dist-info → rucio_clients-35.8.0.dist-info}/top_level.txt +0 -0
rucio/common/utils.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
1
|
# Copyright European Organization for Nuclear Research (CERN) since 2012
|
|
3
2
|
#
|
|
4
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -15,14 +14,18 @@
|
|
|
15
14
|
|
|
16
15
|
import argparse
|
|
17
16
|
import base64
|
|
17
|
+
import copy
|
|
18
18
|
import datetime
|
|
19
19
|
import errno
|
|
20
20
|
import getpass
|
|
21
21
|
import hashlib
|
|
22
22
|
import io
|
|
23
|
+
import ipaddress
|
|
23
24
|
import itertools
|
|
24
25
|
import json
|
|
25
26
|
import logging
|
|
27
|
+
import math
|
|
28
|
+
import mmap
|
|
26
29
|
import os
|
|
27
30
|
import os.path
|
|
28
31
|
import re
|
|
@@ -32,26 +35,25 @@ import subprocess
|
|
|
32
35
|
import tempfile
|
|
33
36
|
import threading
|
|
34
37
|
import time
|
|
38
|
+
import zlib
|
|
35
39
|
from collections import OrderedDict
|
|
36
|
-
from
|
|
40
|
+
from collections.abc import Callable, Iterable, Iterator, Sequence
|
|
37
41
|
from enum import Enum
|
|
38
42
|
from functools import partial, wraps
|
|
39
43
|
from io import StringIO
|
|
40
44
|
from itertools import zip_longest
|
|
41
|
-
from typing import TYPE_CHECKING
|
|
42
|
-
from urllib.parse import
|
|
45
|
+
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
|
|
46
|
+
from urllib.parse import parse_qsl, quote, urlencode, urlparse, urlunparse
|
|
43
47
|
from uuid import uuid4 as uuid
|
|
44
48
|
from xml.etree import ElementTree
|
|
45
49
|
|
|
46
|
-
import mmap
|
|
47
50
|
import requests
|
|
48
|
-
import zlib
|
|
49
51
|
|
|
50
52
|
from rucio.common.config import config_get, config_has_section
|
|
51
|
-
from rucio.common.exception import
|
|
52
|
-
DuplicateCriteriaInDIDFilter, DIDFilterSyntaxError, InvalidAlgorithmName, PolicyPackageVersionError
|
|
53
|
+
from rucio.common.exception import ConfigNotFound, DIDFilterSyntaxError, DuplicateCriteriaInDIDFilter, InputValidationError, InvalidType, MetalinkJsonParsingError, MissingModuleException, PolicyPackageVersionError, RucioException
|
|
53
54
|
from rucio.common.extra import import_extras
|
|
54
|
-
from rucio.common.
|
|
55
|
+
from rucio.common.plugins import PolicyPackageAlgorithms
|
|
56
|
+
from rucio.common.types import InternalAccount, InternalScope, TraceDict
|
|
55
57
|
|
|
56
58
|
EXTRA_MODULES = import_extras(['paramiko'])
|
|
57
59
|
|
|
@@ -62,10 +64,11 @@ if EXTRA_MODULES['paramiko']:
|
|
|
62
64
|
EXTRA_MODULES['paramiko'] = False
|
|
63
65
|
|
|
64
66
|
if TYPE_CHECKING:
|
|
65
|
-
from collections.abc import Callable
|
|
66
|
-
from typing import TypeVar
|
|
67
|
-
|
|
68
67
|
T = TypeVar('T')
|
|
68
|
+
from _typeshed import FileDescriptorOrPath
|
|
69
|
+
from sqlalchemy.orm import Session
|
|
70
|
+
|
|
71
|
+
from rucio.common.types import IPDict, LoggerFunction
|
|
69
72
|
|
|
70
73
|
|
|
71
74
|
# HTTP code dictionary. Not complete. Can be extended if needed.
|
|
@@ -98,7 +101,7 @@ codes = {
|
|
|
98
101
|
DATE_FORMAT = '%a, %d %b %Y %H:%M:%S UTC'
|
|
99
102
|
|
|
100
103
|
|
|
101
|
-
def invert_dict(d):
|
|
104
|
+
def invert_dict(d: dict[Any, Any]) -> dict[Any, Any]:
|
|
102
105
|
"""
|
|
103
106
|
Invert the dictionary.
|
|
104
107
|
CAUTION: this function is not deterministic unless the input dictionary is one-to-one mapping.
|
|
@@ -109,11 +112,11 @@ def invert_dict(d):
|
|
|
109
112
|
return {value: key for key, value in d.items()}
|
|
110
113
|
|
|
111
114
|
|
|
112
|
-
def dids_as_dicts(did_list):
|
|
115
|
+
def dids_as_dicts(did_list: Iterable[Union[str, dict[str, str]]]) -> list[dict[str, str]]:
|
|
113
116
|
"""
|
|
114
117
|
Converts list of DIDs to list of dictionaries
|
|
115
|
-
:param did_list: list of DIDs as either "scope:name" or {"scope":"scope", "name"
|
|
116
|
-
:returns: list of dictionaries {"scope":"scope", "name"
|
|
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"}
|
|
117
120
|
"""
|
|
118
121
|
out = []
|
|
119
122
|
for did in did_list:
|
|
@@ -129,7 +132,12 @@ def dids_as_dicts(did_list):
|
|
|
129
132
|
return out
|
|
130
133
|
|
|
131
134
|
|
|
132
|
-
def build_url(
|
|
135
|
+
def build_url(
|
|
136
|
+
url: str,
|
|
137
|
+
path: Optional[str] = None,
|
|
138
|
+
params: Optional[Union[str, dict[Any, Any], list[tuple[Any, Any]]]] = None,
|
|
139
|
+
doseq: bool = False
|
|
140
|
+
) -> str:
|
|
133
141
|
"""
|
|
134
142
|
utitily function to build an url for requests to the rucio system.
|
|
135
143
|
|
|
@@ -148,7 +156,13 @@ def build_url(url, path=None, params=None, doseq=False):
|
|
|
148
156
|
return complete_url
|
|
149
157
|
|
|
150
158
|
|
|
151
|
-
def all_oidc_req_claims_present(
|
|
159
|
+
def all_oidc_req_claims_present(
|
|
160
|
+
scope: Optional[Union[str, list[str]]],
|
|
161
|
+
audience: Optional[Union[str, list[str]]],
|
|
162
|
+
required_scope: Optional[Union[str, list[str]]],
|
|
163
|
+
required_audience: Optional[Union[str, list[str]]],
|
|
164
|
+
separator: str = " "
|
|
165
|
+
) -> bool:
|
|
152
166
|
"""
|
|
153
167
|
Checks if both of the following statements are true:
|
|
154
168
|
- all items in required_scope are present in scope string
|
|
@@ -160,7 +174,7 @@ def all_oidc_req_claims_present(scope, audience, required_scope, required_audien
|
|
|
160
174
|
:params audience: list of strings or one string where items are separated by a separator input variable
|
|
161
175
|
:params required_scope: list of strings or one string where items are separated by a separator input variable
|
|
162
176
|
:params required_audience: list of strings or one string where items are separated by a separator input variable
|
|
163
|
-
:params
|
|
177
|
+
:params separator: separator string, space by default
|
|
164
178
|
:returns : True or False
|
|
165
179
|
"""
|
|
166
180
|
if not scope:
|
|
@@ -184,34 +198,34 @@ def all_oidc_req_claims_present(scope, audience, required_scope, required_audien
|
|
|
184
198
|
audience = str(audience)
|
|
185
199
|
required_scope = str(required_scope)
|
|
186
200
|
required_audience = str(required_audience)
|
|
187
|
-
req_scope_present = all(elem in scope.split(
|
|
188
|
-
req_audience_present = all(elem in audience.split(
|
|
201
|
+
req_scope_present = all(elem in scope.split(separator) for elem in required_scope.split(separator))
|
|
202
|
+
req_audience_present = all(elem in audience.split(separator) for elem in required_audience.split(separator))
|
|
189
203
|
return req_scope_present and req_audience_present
|
|
190
204
|
elif (isinstance(scope, list) and isinstance(audience, list) and isinstance(required_scope, str) and isinstance(required_audience, str)):
|
|
191
205
|
scope = [str(it) for it in scope]
|
|
192
206
|
audience = [str(it) for it in audience]
|
|
193
207
|
required_scope = str(required_scope)
|
|
194
208
|
required_audience = str(required_audience)
|
|
195
|
-
req_scope_present = all(elem in scope for elem in required_scope.split(
|
|
196
|
-
req_audience_present = all(elem in audience for elem in required_audience.split(
|
|
209
|
+
req_scope_present = all(elem in scope for elem in required_scope.split(separator))
|
|
210
|
+
req_audience_present = all(elem in audience for elem in required_audience.split(separator))
|
|
197
211
|
return req_scope_present and req_audience_present
|
|
198
212
|
elif (isinstance(scope, str) and isinstance(audience, str) and isinstance(required_scope, list) and isinstance(required_audience, list)):
|
|
199
213
|
scope = str(scope)
|
|
200
214
|
audience = str(audience)
|
|
201
215
|
required_scope = [str(it) for it in required_scope]
|
|
202
216
|
required_audience = [str(it) for it in required_audience]
|
|
203
|
-
req_scope_present = all(elem in scope.split(
|
|
204
|
-
req_audience_present = all(elem in audience.split(
|
|
217
|
+
req_scope_present = all(elem in scope.split(separator) for elem in required_scope)
|
|
218
|
+
req_audience_present = all(elem in audience.split(separator) for elem in required_audience)
|
|
205
219
|
return req_scope_present and req_audience_present
|
|
206
220
|
else:
|
|
207
221
|
return False
|
|
208
222
|
|
|
209
223
|
|
|
210
|
-
def generate_uuid():
|
|
224
|
+
def generate_uuid() -> str:
|
|
211
225
|
return str(uuid()).replace('-', '').lower()
|
|
212
226
|
|
|
213
227
|
|
|
214
|
-
def generate_uuid_bytes():
|
|
228
|
+
def generate_uuid_bytes() -> bytes:
|
|
215
229
|
return uuid().bytes
|
|
216
230
|
|
|
217
231
|
|
|
@@ -222,9 +236,9 @@ PREFERRED_CHECKSUM = GLOBALLY_SUPPORTED_CHECKSUMS[0]
|
|
|
222
236
|
CHECKSUM_KEY = 'supported_checksums'
|
|
223
237
|
|
|
224
238
|
|
|
225
|
-
def is_checksum_valid(checksum_name):
|
|
239
|
+
def is_checksum_valid(checksum_name: str) -> bool:
|
|
226
240
|
"""
|
|
227
|
-
A simple function to check
|
|
241
|
+
A simple function to check whether a checksum algorithm is supported.
|
|
228
242
|
Relies on GLOBALLY_SUPPORTED_CHECKSUMS to allow for expandability.
|
|
229
243
|
|
|
230
244
|
:param checksum_name: The name of the checksum to be verified.
|
|
@@ -234,20 +248,19 @@ def is_checksum_valid(checksum_name):
|
|
|
234
248
|
return checksum_name in GLOBALLY_SUPPORTED_CHECKSUMS
|
|
235
249
|
|
|
236
250
|
|
|
237
|
-
def set_preferred_checksum(checksum_name):
|
|
251
|
+
def set_preferred_checksum(checksum_name: str) -> None:
|
|
238
252
|
"""
|
|
239
|
-
|
|
240
|
-
|
|
253
|
+
If the input checksum name is valid,
|
|
254
|
+
set it as PREFERRED_CHECKSUM.
|
|
241
255
|
|
|
242
256
|
:param checksum_name: The name of the checksum to be verified.
|
|
243
|
-
:returns: True if checksum_name is in GLOBALLY_SUPPORTED_CHECKSUMS list, False otherwise.
|
|
244
257
|
"""
|
|
245
258
|
if is_checksum_valid(checksum_name):
|
|
246
259
|
global PREFERRED_CHECKSUM
|
|
247
260
|
PREFERRED_CHECKSUM = checksum_name
|
|
248
261
|
|
|
249
262
|
|
|
250
|
-
def adler32(file):
|
|
263
|
+
def adler32(file: "FileDescriptorOrPath") -> str:
|
|
251
264
|
"""
|
|
252
265
|
An Adler-32 checksum is obtained by calculating two 16-bit checksums A and B
|
|
253
266
|
and concatenating their bits into a 32-bit integer. A is the sum of all bytes in the
|
|
@@ -294,7 +307,7 @@ def adler32(file):
|
|
|
294
307
|
CHECKSUM_ALGO_DICT['adler32'] = adler32
|
|
295
308
|
|
|
296
309
|
|
|
297
|
-
def md5(file):
|
|
310
|
+
def md5(file: "FileDescriptorOrPath") -> str:
|
|
298
311
|
"""
|
|
299
312
|
Runs the MD5 algorithm (RFC-1321) on the binary content of the file named file and returns the hexadecimal digest
|
|
300
313
|
|
|
@@ -314,7 +327,7 @@ def md5(file):
|
|
|
314
327
|
CHECKSUM_ALGO_DICT['md5'] = md5
|
|
315
328
|
|
|
316
329
|
|
|
317
|
-
def sha256(file):
|
|
330
|
+
def sha256(file: "FileDescriptorOrPath") -> str:
|
|
318
331
|
"""
|
|
319
332
|
Runs the SHA256 algorithm on the binary content of the file named file and returns the hexadecimal digest
|
|
320
333
|
|
|
@@ -331,7 +344,7 @@ def sha256(file):
|
|
|
331
344
|
CHECKSUM_ALGO_DICT['sha256'] = sha256
|
|
332
345
|
|
|
333
346
|
|
|
334
|
-
def crc32(file):
|
|
347
|
+
def crc32(file: "FileDescriptorOrPath") -> str:
|
|
335
348
|
"""
|
|
336
349
|
Runs the CRC32 algorithm on the binary content of the file named file and returns the hexadecimal digest
|
|
337
350
|
|
|
@@ -347,7 +360,219 @@ def crc32(file):
|
|
|
347
360
|
CHECKSUM_ALGO_DICT['crc32'] = crc32
|
|
348
361
|
|
|
349
362
|
|
|
350
|
-
def
|
|
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
|
+
def str_to_date(string: str) -> Optional[datetime.datetime]:
|
|
351
576
|
""" Converts a RFC-1123 string to the corresponding datetime value.
|
|
352
577
|
|
|
353
578
|
:param string: the RFC-1123 string to convert to datetime value.
|
|
@@ -355,7 +580,7 @@ def str_to_date(string):
|
|
|
355
580
|
return datetime.datetime.strptime(string, DATE_FORMAT) if string else None
|
|
356
581
|
|
|
357
582
|
|
|
358
|
-
def val_to_space_sep_str(vallist):
|
|
583
|
+
def val_to_space_sep_str(vallist: list[str]) -> str:
|
|
359
584
|
""" Converts a list of values into a string of space separated values
|
|
360
585
|
|
|
361
586
|
:param vallist: the list of values to to convert into string
|
|
@@ -367,10 +592,10 @@ def val_to_space_sep_str(vallist):
|
|
|
367
592
|
else:
|
|
368
593
|
return str(vallist)
|
|
369
594
|
except:
|
|
370
|
-
return
|
|
595
|
+
return ''
|
|
371
596
|
|
|
372
597
|
|
|
373
|
-
def date_to_str(date):
|
|
598
|
+
def date_to_str(date: datetime.datetime) -> Optional[str]:
|
|
374
599
|
""" Converts a datetime value to the corresponding RFC-1123 string.
|
|
375
600
|
|
|
376
601
|
:param date: the datetime value to convert.
|
|
@@ -400,19 +625,18 @@ class APIEncoder(json.JSONEncoder):
|
|
|
400
625
|
return json.JSONEncoder.default(self, obj)
|
|
401
626
|
|
|
402
627
|
|
|
403
|
-
def render_json(**
|
|
404
|
-
""" JSON
|
|
405
|
-
|
|
628
|
+
def render_json(*args, **kwargs) -> str:
|
|
629
|
+
""" Render a list or a dict as a JSON-formatted string. """
|
|
630
|
+
if args and isinstance(args[0], list):
|
|
631
|
+
data = args[0]
|
|
632
|
+
elif isinstance(kwargs, dict):
|
|
633
|
+
data = kwargs
|
|
634
|
+
else:
|
|
635
|
+
raise ValueError("Error while serializing object to JSON-formatted string: supported input types are list or dict.")
|
|
406
636
|
return json.dumps(data, cls=APIEncoder)
|
|
407
637
|
|
|
408
638
|
|
|
409
|
-
def
|
|
410
|
-
""" JSON render function for list
|
|
411
|
-
"""
|
|
412
|
-
return json.dumps(list_, cls=APIEncoder)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
def datetime_parser(dct):
|
|
639
|
+
def datetime_parser(dct: dict[Any, Any]) -> dict[Any, Any]:
|
|
416
640
|
""" datetime parser
|
|
417
641
|
"""
|
|
418
642
|
for k, v in list(dct.items()):
|
|
@@ -424,17 +648,17 @@ def datetime_parser(dct):
|
|
|
424
648
|
return dct
|
|
425
649
|
|
|
426
650
|
|
|
427
|
-
def parse_response(data):
|
|
651
|
+
def parse_response(data: Union[str, bytes, bytearray]) -> Any:
|
|
428
652
|
"""
|
|
429
653
|
JSON render function
|
|
430
654
|
"""
|
|
431
|
-
if
|
|
655
|
+
if isinstance(data, (bytes, bytearray)):
|
|
432
656
|
data = data.decode('utf-8')
|
|
433
657
|
|
|
434
658
|
return json.loads(data, object_hook=datetime_parser)
|
|
435
659
|
|
|
436
660
|
|
|
437
|
-
def execute(cmd) -> tuple[int, str, str]:
|
|
661
|
+
def execute(cmd: str) -> tuple[int, str, str]:
|
|
438
662
|
"""
|
|
439
663
|
Executes a command in a subprocess. Returns a tuple
|
|
440
664
|
of (exitcode, out, err), where out is the string output
|
|
@@ -456,17 +680,12 @@ def execute(cmd) -> tuple[int, str, str]:
|
|
|
456
680
|
return exitcode, out.decode(encoding='utf-8'), err.decode(encoding='utf-8')
|
|
457
681
|
|
|
458
682
|
|
|
459
|
-
def
|
|
460
|
-
""" Returns a list with
|
|
461
|
-
return ['read', 'write', 'delete', 'third_party_copy_read', 'third_party_copy_write']
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
def rse_supported_protocol_domains():
|
|
465
|
-
""" Returns a list with all supoorted RSE protocol domains."""
|
|
683
|
+
def rse_supported_protocol_domains() -> list[str]:
|
|
684
|
+
""" Returns a list with all supported RSE protocol domains."""
|
|
466
685
|
return ['lan', 'wan']
|
|
467
686
|
|
|
468
687
|
|
|
469
|
-
def grouper(iterable, n, fillvalue=None):
|
|
688
|
+
def grouper(iterable: Iterable[Any], n: int, fillvalue: Optional[object] = None) -> zip_longest:
|
|
470
689
|
""" Collect data into fixed-length chunks or blocks """
|
|
471
690
|
# grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx
|
|
472
691
|
args = [iter(iterable)] * n
|
|
@@ -489,7 +708,7 @@ def chunks(iterable, n):
|
|
|
489
708
|
yield chunk
|
|
490
709
|
|
|
491
710
|
|
|
492
|
-
def dict_chunks(dict_, n):
|
|
711
|
+
def dict_chunks(dict_: dict[Any, Any], n: int) -> Iterator[dict[Any, Any]]:
|
|
493
712
|
"""
|
|
494
713
|
Iterate over the dictionary in groups of the requested size
|
|
495
714
|
"""
|
|
@@ -498,314 +717,394 @@ def dict_chunks(dict_, n):
|
|
|
498
717
|
yield {k: dict_[k] for k in itertools.islice(it, n)}
|
|
499
718
|
|
|
500
719
|
|
|
501
|
-
def my_key_generator(namespace, fn, **kw):
|
|
720
|
+
def my_key_generator(namespace: str, fn: Callable, **kw) -> Callable[..., str]:
|
|
502
721
|
"""
|
|
503
|
-
|
|
722
|
+
Customized key generator for dogpile
|
|
504
723
|
"""
|
|
505
724
|
fname = fn.__name__
|
|
506
725
|
|
|
507
|
-
def generate_key(*arg, **kw):
|
|
726
|
+
def generate_key(*arg, **kw) -> str:
|
|
508
727
|
return namespace + "_" + fname + "_".join(str(s) for s in filter(None, arg))
|
|
509
728
|
|
|
510
729
|
return generate_key
|
|
511
730
|
|
|
512
731
|
|
|
513
|
-
|
|
514
|
-
"""
|
|
515
|
-
Defines relative SURL for new replicas. This method
|
|
516
|
-
contains DQ2 convention. To be used for non-deterministic sites.
|
|
517
|
-
Method imported from DQ2.
|
|
518
|
-
|
|
519
|
-
@return: relative SURL for new replica.
|
|
520
|
-
@rtype: str
|
|
521
|
-
"""
|
|
522
|
-
# check how many dots in dsn
|
|
523
|
-
fields = dsn.split('.')
|
|
524
|
-
nfields = len(fields)
|
|
525
|
-
|
|
526
|
-
if nfields == 0:
|
|
527
|
-
return '/other/other/%s' % (filename)
|
|
528
|
-
elif nfields == 1:
|
|
529
|
-
stripped_dsn = __strip_dsn(dsn)
|
|
530
|
-
return '/other/%s/%s' % (stripped_dsn, filename)
|
|
531
|
-
elif nfields == 2:
|
|
532
|
-
project = fields[0]
|
|
533
|
-
stripped_dsn = __strip_dsn(dsn)
|
|
534
|
-
return '/%s/%s/%s' % (project, stripped_dsn, filename)
|
|
535
|
-
elif nfields < 5 or re.match('user*|group*', fields[0]):
|
|
536
|
-
project = fields[0]
|
|
537
|
-
f2 = fields[1]
|
|
538
|
-
f3 = fields[2]
|
|
539
|
-
stripped_dsn = __strip_dsn(dsn)
|
|
540
|
-
return '/%s/%s/%s/%s/%s' % (project, f2, f3, stripped_dsn, filename)
|
|
541
|
-
else:
|
|
542
|
-
project = fields[0]
|
|
543
|
-
dataset_type = fields[4]
|
|
544
|
-
if nfields == 5:
|
|
545
|
-
tag = 'other'
|
|
546
|
-
else:
|
|
547
|
-
tag = __strip_tag(fields[-1])
|
|
548
|
-
stripped_dsn = __strip_dsn(dsn)
|
|
549
|
-
return '/%s/%s/%s/%s/%s' % (project, dataset_type, tag, stripped_dsn, filename)
|
|
732
|
+
NonDeterministicPFNAlgorithmsT = TypeVar('NonDeterministicPFNAlgorithmsT', bound='NonDeterministicPFNAlgorithms')
|
|
550
733
|
|
|
551
734
|
|
|
552
|
-
|
|
735
|
+
class NonDeterministicPFNAlgorithms(PolicyPackageAlgorithms):
|
|
553
736
|
"""
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
@return: relative SURL for new replica.
|
|
558
|
-
@rtype: str
|
|
737
|
+
Handle PFN construction for non-deterministic RSEs, including registration of algorithms
|
|
738
|
+
from policy packages
|
|
559
739
|
"""
|
|
560
|
-
fields = dsn.split('.')
|
|
561
|
-
nfields = len(fields)
|
|
562
|
-
if nfields >= 3:
|
|
563
|
-
return '/%s/%s/%s/%s/%s' % (fields[0], fields[2], fields[1], dsn, filename)
|
|
564
|
-
elif nfields == 1:
|
|
565
|
-
return '/%s/%s/%s/%s/%s' % (fields[0], 'other', 'other', dsn, filename)
|
|
566
|
-
elif nfields == 2:
|
|
567
|
-
return '/%s/%s/%s/%s/%s' % (fields[0], fields[2], 'other', dsn, filename)
|
|
568
|
-
elif nfields == 0:
|
|
569
|
-
return '/other/other/other/other/%s' % (filename)
|
|
570
740
|
|
|
741
|
+
_algorithm_type = 'non_deterministic_pfn'
|
|
571
742
|
|
|
572
|
-
def
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
DSN (or datablock in the Belle II naming) contains /
|
|
743
|
+
def __init__(self) -> None:
|
|
744
|
+
"""
|
|
745
|
+
Initialises a non-deterministic PFN construction object
|
|
746
|
+
"""
|
|
747
|
+
super().__init__()
|
|
578
748
|
|
|
579
|
-
|
|
749
|
+
def construct_non_deterministic_pfn(self, dsn: str, scope: Optional[str], filename: str, naming_convention: str) -> str:
|
|
750
|
+
"""
|
|
751
|
+
Calls the correct algorithm to generate a non-deterministic PFN
|
|
752
|
+
"""
|
|
753
|
+
return self.get_algorithm(naming_convention)(dsn, scope, filename)
|
|
580
754
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
return
|
|
755
|
+
@classmethod
|
|
756
|
+
def supports(cls: type[NonDeterministicPFNAlgorithmsT], naming_convention: str) -> bool:
|
|
757
|
+
"""
|
|
758
|
+
Checks whether a non-deterministic PFN algorithm is supported
|
|
759
|
+
"""
|
|
760
|
+
return super()._supports(cls._algorithm_type, naming_convention)
|
|
587
761
|
|
|
762
|
+
@classmethod
|
|
763
|
+
def _module_init_(cls: type[NonDeterministicPFNAlgorithmsT]) -> None:
|
|
764
|
+
"""
|
|
765
|
+
Registers the included non-deterministic PFN algorithms
|
|
766
|
+
"""
|
|
767
|
+
cls.register('T0', cls.construct_non_deterministic_pfn_T0)
|
|
768
|
+
cls.register('DQ2', cls.construct_non_deterministic_pfn_DQ2)
|
|
769
|
+
cls.register('BelleII', cls.construct_non_deterministic_pfn_BelleII)
|
|
588
770
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
771
|
+
@classmethod
|
|
772
|
+
def get_algorithm(cls: type[NonDeterministicPFNAlgorithmsT], naming_convention: str) -> Callable[[str, Optional[str], str], str]:
|
|
773
|
+
"""
|
|
774
|
+
Looks up a non-deterministic PFN algorithm by name
|
|
775
|
+
"""
|
|
776
|
+
return super()._get_one_algorithm(cls._algorithm_type, naming_convention)
|
|
592
777
|
|
|
778
|
+
@classmethod
|
|
779
|
+
def register(cls: type[NonDeterministicPFNAlgorithmsT], name: str, fn_construct_non_deterministic_pfn: Callable[[str, Optional[str], str], Optional[str]]) -> None:
|
|
780
|
+
"""
|
|
781
|
+
Register a new non-deterministic PFN algorithm
|
|
782
|
+
"""
|
|
783
|
+
algorithm_dict = {name: fn_construct_non_deterministic_pfn}
|
|
784
|
+
super()._register(cls._algorithm_type, algorithm_dict)
|
|
593
785
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
786
|
+
@staticmethod
|
|
787
|
+
def __strip_dsn(dsn: str) -> str:
|
|
788
|
+
"""
|
|
789
|
+
Drop the _sub and _dis suffixes for panda datasets from the lfc path
|
|
790
|
+
they will be registered in.
|
|
791
|
+
Method imported from DQ2.
|
|
792
|
+
"""
|
|
598
793
|
|
|
794
|
+
suffixes_to_drop = ['_dis', '_sub', '_frag']
|
|
795
|
+
fields = dsn.split('.')
|
|
796
|
+
last_field = fields[-1]
|
|
797
|
+
try:
|
|
798
|
+
for suffix in suffixes_to_drop:
|
|
799
|
+
last_field = re.sub('%s.*$' % suffix, '', last_field)
|
|
800
|
+
except IndexError:
|
|
801
|
+
return dsn
|
|
802
|
+
fields[-1] = last_field
|
|
803
|
+
stripped_dsn = '.'.join(fields)
|
|
804
|
+
return stripped_dsn
|
|
805
|
+
|
|
806
|
+
@staticmethod
|
|
807
|
+
def __strip_tag(tag: str) -> str:
|
|
808
|
+
"""
|
|
809
|
+
Drop the _sub and _dis suffixes for panda datasets from the lfc path
|
|
810
|
+
they will be registered in
|
|
811
|
+
Method imported from DQ2.
|
|
812
|
+
"""
|
|
813
|
+
suffixes_to_drop = ['_dis', '_sub', '_tid']
|
|
814
|
+
stripped_tag = tag
|
|
815
|
+
try:
|
|
816
|
+
for suffix in suffixes_to_drop:
|
|
817
|
+
stripped_tag = re.sub('%s.*$' % suffix, '', stripped_tag)
|
|
818
|
+
except IndexError:
|
|
819
|
+
return stripped_tag
|
|
820
|
+
return stripped_tag
|
|
599
821
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
822
|
+
@staticmethod
|
|
823
|
+
def construct_non_deterministic_pfn_DQ2(dsn: str, scope: Optional[str], filename: str) -> str:
|
|
824
|
+
"""
|
|
825
|
+
Defines relative PFN for new replicas. This method
|
|
826
|
+
contains DQ2 convention. To be used for non-deterministic sites.
|
|
827
|
+
Method imported from DQ2.
|
|
828
|
+
|
|
829
|
+
@return: relative PFN for new replica.
|
|
830
|
+
@rtype: str
|
|
831
|
+
"""
|
|
832
|
+
# check how many dots in dsn
|
|
833
|
+
fields = dsn.split('.')
|
|
834
|
+
nfields = len(fields)
|
|
835
|
+
|
|
836
|
+
if nfields == 0:
|
|
837
|
+
return '/other/other/%s' % (filename)
|
|
838
|
+
elif nfields == 1:
|
|
839
|
+
stripped_dsn = NonDeterministicPFNAlgorithms.__strip_dsn(dsn)
|
|
840
|
+
return '/other/%s/%s' % (stripped_dsn, filename)
|
|
841
|
+
elif nfields == 2:
|
|
842
|
+
project = fields[0]
|
|
843
|
+
stripped_dsn = NonDeterministicPFNAlgorithms.__strip_dsn(dsn)
|
|
844
|
+
return '/%s/%s/%s' % (project, stripped_dsn, filename)
|
|
845
|
+
elif nfields < 5 or re.match('user*|group*', fields[0]):
|
|
846
|
+
project = fields[0]
|
|
847
|
+
f2 = fields[1]
|
|
848
|
+
f3 = fields[2]
|
|
849
|
+
stripped_dsn = NonDeterministicPFNAlgorithms.__strip_dsn(dsn)
|
|
850
|
+
return '/%s/%s/%s/%s/%s' % (project, f2, f3, stripped_dsn, filename)
|
|
851
|
+
else:
|
|
852
|
+
project = fields[0]
|
|
853
|
+
dataset_type = fields[4]
|
|
854
|
+
if nfields == 5:
|
|
855
|
+
tag = 'other'
|
|
856
|
+
else:
|
|
857
|
+
tag = NonDeterministicPFNAlgorithms.__strip_tag(fields[-1])
|
|
858
|
+
stripped_dsn = NonDeterministicPFNAlgorithms.__strip_dsn(dsn)
|
|
859
|
+
return '/%s/%s/%s/%s/%s' % (project, dataset_type, tag, stripped_dsn, filename)
|
|
860
|
+
|
|
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
|
+
|
|
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)
|
|
603
896
|
|
|
604
897
|
|
|
605
|
-
|
|
898
|
+
_DEFAULT_NON_DETERMINISTIC_PFN = 'DQ2'
|
|
899
|
+
NonDeterministicPFNAlgorithms._module_init_()
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def construct_non_deterministic_pfn(dsn: str, scope: Optional[str], filename: str, naming_convention: Optional[str] = None) -> str:
|
|
606
903
|
"""
|
|
607
|
-
Applies non-deterministic
|
|
904
|
+
Applies non-deterministic PFN convention to the given replica.
|
|
608
905
|
use the naming_convention to call the actual function which will do the job.
|
|
609
|
-
Rucio administrators can potentially register additional
|
|
906
|
+
Rucio administrators can potentially register additional PFN generation algorithms,
|
|
610
907
|
which are not implemented inside this main rucio repository, so changing the
|
|
611
908
|
argument list must be done with caution.
|
|
612
909
|
"""
|
|
613
|
-
|
|
614
|
-
if not
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
_loaded_policy_modules = True
|
|
910
|
+
pfn_algorithms = NonDeterministicPFNAlgorithms()
|
|
911
|
+
if naming_convention is None or not NonDeterministicPFNAlgorithms.supports(naming_convention):
|
|
912
|
+
naming_convention = _DEFAULT_NON_DETERMINISTIC_PFN
|
|
913
|
+
return pfn_algorithms.construct_non_deterministic_pfn(dsn, scope, filename, naming_convention)
|
|
618
914
|
|
|
619
|
-
if naming_convention is None or naming_convention not in _SURL_ALGORITHMS:
|
|
620
|
-
naming_convention = _DEFAULT_SURL
|
|
621
|
-
return _SURL_ALGORITHMS[naming_convention](dsn, scope, filename)
|
|
622
915
|
|
|
916
|
+
def clean_pfns(pfns: Iterable[str]) -> list[str]:
|
|
917
|
+
res = []
|
|
918
|
+
for pfn in pfns:
|
|
919
|
+
if pfn.startswith('srm'):
|
|
920
|
+
pfn = re.sub(':[0-9]+/', '/', pfn)
|
|
921
|
+
pfn = re.sub(r'/srm/managerv1\?SFN=', '', pfn)
|
|
922
|
+
pfn = re.sub(r'/srm/v2/server\?SFN=', '', pfn)
|
|
923
|
+
pfn = re.sub(r'/srm/managerv2\?SFN=', '', pfn)
|
|
924
|
+
if '?GoogleAccessId' in pfn:
|
|
925
|
+
pfn = pfn.split('?GoogleAccessId')[0]
|
|
926
|
+
if '?X-Amz' in pfn:
|
|
927
|
+
pfn = pfn.split('?X-Amz')[0]
|
|
928
|
+
res.append(pfn)
|
|
929
|
+
res.sort()
|
|
930
|
+
return res
|
|
623
931
|
|
|
624
|
-
def __strip_dsn(dsn):
|
|
625
|
-
"""
|
|
626
|
-
Drop the _sub and _dis suffixes for panda datasets from the lfc path
|
|
627
|
-
they will be registered in.
|
|
628
|
-
Method imported from DQ2.
|
|
629
|
-
"""
|
|
630
932
|
|
|
631
|
-
|
|
632
|
-
fields = dsn.split('.')
|
|
633
|
-
last_field = fields[-1]
|
|
634
|
-
try:
|
|
635
|
-
for suffix in suffixes_to_drop:
|
|
636
|
-
last_field = re.sub('%s.*$' % suffix, '', last_field)
|
|
637
|
-
except IndexError:
|
|
638
|
-
return dsn
|
|
639
|
-
fields[-1] = last_field
|
|
640
|
-
stripped_dsn = '.'.join(fields)
|
|
641
|
-
return stripped_dsn
|
|
933
|
+
ScopeExtractionAlgorithmsT = TypeVar('ScopeExtractionAlgorithmsT', bound='ScopeExtractionAlgorithms')
|
|
642
934
|
|
|
643
935
|
|
|
644
|
-
|
|
936
|
+
class ScopeExtractionAlgorithms(PolicyPackageAlgorithms):
|
|
645
937
|
"""
|
|
646
|
-
|
|
647
|
-
they will be registered in
|
|
648
|
-
Method imported from DQ2.
|
|
938
|
+
Handle scope extraction algorithms
|
|
649
939
|
"""
|
|
650
|
-
suffixes_to_drop = ['_dis', '_sub', '_tid']
|
|
651
|
-
stripped_tag = tag
|
|
652
|
-
try:
|
|
653
|
-
for suffix in suffixes_to_drop:
|
|
654
|
-
stripped_tag = re.sub('%s.*$' % suffix, '', stripped_tag)
|
|
655
|
-
except IndexError:
|
|
656
|
-
return stripped_tag
|
|
657
|
-
return stripped_tag
|
|
658
940
|
|
|
941
|
+
_algorithm_type = 'scope'
|
|
659
942
|
|
|
660
|
-
def
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
surl = re.sub(r'/srm/managerv1\?SFN=', '', surl)
|
|
666
|
-
surl = re.sub(r'/srm/v2/server\?SFN=', '', surl)
|
|
667
|
-
surl = re.sub(r'/srm/managerv2\?SFN=', '', surl)
|
|
668
|
-
if '?GoogleAccessId' in surl:
|
|
669
|
-
surl = surl.split('?GoogleAccessId')[0]
|
|
670
|
-
if '?X-Amz' in surl:
|
|
671
|
-
surl = surl.split('?X-Amz')[0]
|
|
672
|
-
res.append(surl)
|
|
673
|
-
res.sort()
|
|
674
|
-
return res
|
|
943
|
+
def __init__(self) -> None:
|
|
944
|
+
"""
|
|
945
|
+
Initialises scope extraction algorithms object
|
|
946
|
+
"""
|
|
947
|
+
super().__init__()
|
|
675
948
|
|
|
949
|
+
def extract_scope(self, did: str, scopes: Optional[Sequence[str]], extract_scope_convention: str) -> Sequence[str]:
|
|
950
|
+
"""
|
|
951
|
+
Calls the correct algorithm for scope extraction
|
|
952
|
+
"""
|
|
953
|
+
return self.get_algorithm(extract_scope_convention)(did, scopes)
|
|
676
954
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
# Try to extract the scope from the DSN
|
|
684
|
-
if did.find(':') > -1:
|
|
685
|
-
if len(did.split(':')) > 2:
|
|
686
|
-
raise RucioException('Too many colons. Cannot extract scope and name')
|
|
687
|
-
scope, name = did.split(':')[0], did.split(':')[1]
|
|
688
|
-
if name.endswith('/'):
|
|
689
|
-
name = name[:-1]
|
|
690
|
-
return scope, name
|
|
691
|
-
else:
|
|
692
|
-
scope = did.split('.')[0]
|
|
693
|
-
if did.startswith('user') or did.startswith('group'):
|
|
694
|
-
scope = ".".join(did.split('.')[0:2])
|
|
695
|
-
if did.endswith('/'):
|
|
696
|
-
did = did[:-1]
|
|
697
|
-
return scope, did
|
|
955
|
+
@classmethod
|
|
956
|
+
def supports(cls: type[ScopeExtractionAlgorithmsT], extract_scope_convention: str) -> bool:
|
|
957
|
+
"""
|
|
958
|
+
Checks whether the specified scope extraction algorithm is supported
|
|
959
|
+
"""
|
|
960
|
+
return super()._supports(cls._algorithm_type, extract_scope_convention)
|
|
698
961
|
|
|
962
|
+
@classmethod
|
|
963
|
+
def _module_init_(cls: type[ScopeExtractionAlgorithmsT]) -> None:
|
|
964
|
+
"""
|
|
965
|
+
Registers the included scope extraction algorithms
|
|
966
|
+
"""
|
|
967
|
+
cls.register('atlas', cls.extract_scope_atlas)
|
|
968
|
+
cls.register('belleii', cls.extract_scope_belleii)
|
|
969
|
+
cls.register('dirac', cls.extract_scope_dirac)
|
|
699
970
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
else:
|
|
707
|
-
scope = elem[1]
|
|
708
|
-
return scope, did
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
def extract_scope_belleii(did, scopes):
|
|
712
|
-
split_did = did.split('/')
|
|
713
|
-
if did.startswith('/belle/mock/'):
|
|
714
|
-
return 'mock', did
|
|
715
|
-
if did.startswith('/belle/MC/'):
|
|
716
|
-
if did.startswith('/belle/MC/BG') or \
|
|
717
|
-
did.startswith('/belle/MC/build') or \
|
|
718
|
-
did.startswith('/belle/MC/generic') or \
|
|
719
|
-
did.startswith('/belle/MC/log') or \
|
|
720
|
-
did.startswith('/belle/MC/mcprod') or \
|
|
721
|
-
did.startswith('/belle/MC/prerelease') or \
|
|
722
|
-
did.startswith('/belle/MC/release'):
|
|
723
|
-
return 'mc', did
|
|
724
|
-
if did.startswith('/belle/MC/cert') or \
|
|
725
|
-
did.startswith('/belle/MC/dirac') or \
|
|
726
|
-
did.startswith('/belle/MC/dr3') or \
|
|
727
|
-
did.startswith('/belle/MC/fab') or \
|
|
728
|
-
did.startswith('/belle/MC/hideki') or \
|
|
729
|
-
did.startswith('/belle/MC/merge') or \
|
|
730
|
-
did.startswith('/belle/MC/migration') or \
|
|
731
|
-
did.startswith('/belle/MC/skim') or \
|
|
732
|
-
did.startswith('/belle/MC/test'):
|
|
733
|
-
return 'mc_tmp', did
|
|
734
|
-
if len(split_did) > 4:
|
|
735
|
-
if split_did[3].find('fab') > -1 or split_did[3].find('merge') > -1 or split_did[3].find('skim') > -1:
|
|
736
|
-
return 'mc_tmp', did
|
|
737
|
-
if split_did[3].find('release') > -1:
|
|
738
|
-
return 'mc', did
|
|
739
|
-
return 'mc_tmp', did
|
|
740
|
-
if did.startswith('/belle/Raw/'):
|
|
741
|
-
return 'raw', did
|
|
742
|
-
if did.startswith('/belle/hRaw'):
|
|
743
|
-
return 'hraw', did
|
|
744
|
-
if did.startswith('/belle/user/'):
|
|
745
|
-
if len(split_did) > 4:
|
|
746
|
-
if len(split_did[3]) == 1 and 'user.%s' % (split_did[4]) in scopes:
|
|
747
|
-
return 'user.%s' % split_did[4], did
|
|
748
|
-
if len(split_did) > 3:
|
|
749
|
-
if 'user.%s' % (split_did[3]) in scopes:
|
|
750
|
-
return 'user.%s' % split_did[3], did
|
|
751
|
-
return 'user', did
|
|
752
|
-
if did.startswith('/belle/group/'):
|
|
753
|
-
if len(split_did) > 4:
|
|
754
|
-
if 'group.%s' % (split_did[4]) in scopes:
|
|
755
|
-
return 'group.%s' % split_did[4], did
|
|
756
|
-
return 'group', did
|
|
757
|
-
if did.startswith('/belle/data/') or did.startswith('/belle/Data/'):
|
|
758
|
-
if len(split_did) > 4:
|
|
759
|
-
if split_did[3] in ['fab', 'skim']: # /belle/Data/fab --> data_tmp
|
|
760
|
-
return 'data_tmp', did
|
|
761
|
-
if split_did[3].find('release') > -1: # /belle/Data/release --> data
|
|
762
|
-
return 'data', did
|
|
763
|
-
if len(split_did) > 5:
|
|
764
|
-
if split_did[3] in ['proc']: # /belle/Data/proc
|
|
765
|
-
if split_did[4].find('release') > -1: # /belle/Data/proc/release*
|
|
766
|
-
if len(split_did) > 7 and split_did[6] in ['GCR2c', 'prod00000007', 'prod6b', 'proc7b',
|
|
767
|
-
'proc8b', 'Bucket4', 'Bucket6test', 'bucket6',
|
|
768
|
-
'proc9', 'bucket7', 'SKIMDATAx1', 'proc10Valid',
|
|
769
|
-
'proc10', 'SkimP10x1', 'SkimP11x1', 'SkimB9x1',
|
|
770
|
-
'SkimB10x1', 'SkimB11x1']: # /belle/Data/proc/release*/*/proc10/* --> data_tmp (Old convention)
|
|
771
|
-
return 'data_tmp', did
|
|
772
|
-
else: # /belle/Data/proc/release*/*/proc11/* --> data (New convention)
|
|
773
|
-
return 'data', did
|
|
774
|
-
if split_did[4].find('fab') > -1: # /belle/Data/proc/fab* --> data_tmp
|
|
775
|
-
return 'data_tmp', did
|
|
776
|
-
return 'data_tmp', did
|
|
777
|
-
if did.startswith('/belle/ddm/functional_tests/') or did.startswith('/belle/ddm/tests/') or did.startswith('/belle/test/ddm_test'):
|
|
778
|
-
return 'test', did
|
|
779
|
-
if did.startswith('/belle/BG/'):
|
|
780
|
-
return 'data', did
|
|
781
|
-
if did.startswith('/belle/collection'):
|
|
782
|
-
return 'collection', did
|
|
783
|
-
return 'other', did
|
|
971
|
+
@classmethod
|
|
972
|
+
def get_algorithm(cls: type[ScopeExtractionAlgorithmsT], extract_scope_convention: str) -> Callable[[str, Optional[Sequence[str]]], Sequence[str]]:
|
|
973
|
+
"""
|
|
974
|
+
Looks up a scope extraction algorithm by name
|
|
975
|
+
"""
|
|
976
|
+
return super()._get_one_algorithm(cls._algorithm_type, extract_scope_convention)
|
|
784
977
|
|
|
978
|
+
@classmethod
|
|
979
|
+
def register(cls: type[ScopeExtractionAlgorithmsT], name: str, fn_extract_scope: Callable[[str, Optional[Sequence[str]]], Sequence[str]]) -> None:
|
|
980
|
+
"""
|
|
981
|
+
Registers a new scope extraction algorithm
|
|
982
|
+
"""
|
|
983
|
+
algorithm_dict = {name: fn_extract_scope}
|
|
984
|
+
super()._register(cls._algorithm_type, algorithm_dict)
|
|
985
|
+
|
|
986
|
+
@staticmethod
|
|
987
|
+
def extract_scope_atlas(did: str, scopes: Optional[Sequence[str]]) -> Sequence[str]:
|
|
988
|
+
# Try to extract the scope from the DSN
|
|
989
|
+
if did.find(':') > -1:
|
|
990
|
+
if len(did.split(':')) > 2:
|
|
991
|
+
raise RucioException('Too many colons. Cannot extract scope and name')
|
|
992
|
+
scope, name = did.split(':')[0], did.split(':')[1]
|
|
993
|
+
if name.endswith('/'):
|
|
994
|
+
name = name[:-1]
|
|
995
|
+
return scope, name
|
|
996
|
+
else:
|
|
997
|
+
scope = did.split('.')[0]
|
|
998
|
+
if did.startswith('user') or did.startswith('group'):
|
|
999
|
+
scope = ".".join(did.split('.')[0:2])
|
|
1000
|
+
if did.endswith('/'):
|
|
1001
|
+
did = did[:-1]
|
|
1002
|
+
return scope, did
|
|
1003
|
+
|
|
1004
|
+
@staticmethod
|
|
1005
|
+
def extract_scope_dirac(did: str, scopes: Optional[Sequence[str]]) -> Sequence[str]:
|
|
1006
|
+
# Default dirac scope extract algorithm. Scope is the second element in the LFN or the first one (VO name)
|
|
1007
|
+
# if only one element is the result of a split.
|
|
1008
|
+
elem = did.rstrip('/').split('/')
|
|
1009
|
+
if len(elem) > 2:
|
|
1010
|
+
scope = elem[2]
|
|
1011
|
+
else:
|
|
1012
|
+
scope = elem[1]
|
|
1013
|
+
return scope, did
|
|
785
1014
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1015
|
+
@staticmethod
|
|
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
|
|
790
1089
|
|
|
791
1090
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
register_extract_scope_algorithm(extract_scope_dirac, 'dirac')
|
|
1091
|
+
_DEFAULT_EXTRACT = 'atlas'
|
|
1092
|
+
ScopeExtractionAlgorithms._module_init_()
|
|
795
1093
|
|
|
796
1094
|
|
|
797
|
-
def extract_scope(
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1095
|
+
def extract_scope(
|
|
1096
|
+
did: str,
|
|
1097
|
+
scopes: Optional[Sequence[str]] = None,
|
|
1098
|
+
default_extract: str = _DEFAULT_EXTRACT
|
|
1099
|
+
) -> Sequence[str]:
|
|
1100
|
+
scope_extraction_algorithms = ScopeExtractionAlgorithms()
|
|
802
1101
|
extract_scope_convention = config_get('common', 'extract_scope', False, None) or config_get('policy', 'extract_scope', False, None)
|
|
803
|
-
if extract_scope_convention is None or
|
|
1102
|
+
if extract_scope_convention is None or not ScopeExtractionAlgorithms.supports(extract_scope_convention):
|
|
804
1103
|
extract_scope_convention = default_extract
|
|
805
|
-
return
|
|
1104
|
+
return scope_extraction_algorithms.extract_scope(did, scopes, extract_scope_convention)
|
|
806
1105
|
|
|
807
1106
|
|
|
808
|
-
def pid_exists(pid):
|
|
1107
|
+
def pid_exists(pid: int) -> bool:
|
|
809
1108
|
"""
|
|
810
1109
|
Check whether pid exists in the current process table.
|
|
811
1110
|
UNIX only.
|
|
@@ -835,7 +1134,7 @@ def pid_exists(pid):
|
|
|
835
1134
|
return True
|
|
836
1135
|
|
|
837
1136
|
|
|
838
|
-
def sizefmt(num, human=True):
|
|
1137
|
+
def sizefmt(num: Union[int, float, None], human: bool = True) -> str:
|
|
839
1138
|
"""
|
|
840
1139
|
Print human readable file sizes
|
|
841
1140
|
"""
|
|
@@ -855,7 +1154,7 @@ def sizefmt(num, human=True):
|
|
|
855
1154
|
return 'Inf'
|
|
856
1155
|
|
|
857
1156
|
|
|
858
|
-
def get_tmp_dir():
|
|
1157
|
+
def get_tmp_dir() -> str:
|
|
859
1158
|
"""
|
|
860
1159
|
Get a path where to store temporary files.
|
|
861
1160
|
|
|
@@ -883,7 +1182,7 @@ def get_tmp_dir():
|
|
|
883
1182
|
return base_dir
|
|
884
1183
|
|
|
885
1184
|
|
|
886
|
-
def is_archive(name):
|
|
1185
|
+
def is_archive(name: str) -> bool:
|
|
887
1186
|
'''
|
|
888
1187
|
Check if a file name is an archive file or not.
|
|
889
1188
|
|
|
@@ -908,7 +1207,28 @@ class Color:
|
|
|
908
1207
|
END = '\033[0m'
|
|
909
1208
|
|
|
910
1209
|
|
|
911
|
-
def
|
|
1210
|
+
def resolve_ips(hostname: str) -> list[str]:
|
|
1211
|
+
try:
|
|
1212
|
+
ipaddress.ip_address(hostname)
|
|
1213
|
+
return [hostname]
|
|
1214
|
+
except ValueError:
|
|
1215
|
+
pass
|
|
1216
|
+
try:
|
|
1217
|
+
addrinfo = socket.getaddrinfo(hostname, 0, socket.AF_INET, 0, socket.IPPROTO_TCP)
|
|
1218
|
+
return [ai[4][0] for ai in addrinfo]
|
|
1219
|
+
except socket.gaierror:
|
|
1220
|
+
pass
|
|
1221
|
+
return []
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def resolve_ip(hostname: str) -> str:
|
|
1225
|
+
ips = resolve_ips(hostname)
|
|
1226
|
+
if ips:
|
|
1227
|
+
return ips[0]
|
|
1228
|
+
return hostname
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def detect_client_location() -> "IPDict":
|
|
912
1232
|
"""
|
|
913
1233
|
Normally client IP will be set on the server side (request.remote_addr)
|
|
914
1234
|
Here setting ip on the one seen by the host itself. There is no connection
|
|
@@ -923,22 +1243,22 @@ def detect_client_location():
|
|
|
923
1243
|
ip = None
|
|
924
1244
|
|
|
925
1245
|
try:
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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]
|
|
929
1249
|
except Exception:
|
|
930
1250
|
pass
|
|
931
1251
|
|
|
932
1252
|
if not ip:
|
|
933
1253
|
try:
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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]
|
|
937
1257
|
except Exception:
|
|
938
1258
|
pass
|
|
939
1259
|
|
|
940
1260
|
if not ip:
|
|
941
|
-
ip = '0.0.0.0'
|
|
1261
|
+
ip = '0.0.0.0' # noqa: S104
|
|
942
1262
|
|
|
943
1263
|
site = os.environ.get('SITE_NAME',
|
|
944
1264
|
os.environ.get('ATLAS_SITE_NAME',
|
|
@@ -964,7 +1284,7 @@ def detect_client_location():
|
|
|
964
1284
|
'longitude': longitude}
|
|
965
1285
|
|
|
966
1286
|
|
|
967
|
-
def ssh_sign(private_key, message):
|
|
1287
|
+
def ssh_sign(private_key: str, message: str) -> str:
|
|
968
1288
|
"""
|
|
969
1289
|
Sign a string message using the private key.
|
|
970
1290
|
|
|
@@ -972,21 +1292,20 @@ def ssh_sign(private_key, message):
|
|
|
972
1292
|
:param message: The message to sign as a string.
|
|
973
1293
|
:return: Base64 encoded signature as a string.
|
|
974
1294
|
"""
|
|
975
|
-
|
|
976
|
-
message = message.encode()
|
|
1295
|
+
encoded_message = message.encode()
|
|
977
1296
|
if not EXTRA_MODULES['paramiko']:
|
|
978
1297
|
raise MissingModuleException('The paramiko module is not installed or faulty.')
|
|
979
1298
|
sio_private_key = StringIO(private_key)
|
|
980
1299
|
priv_k = RSAKey.from_private_key(sio_private_key)
|
|
981
1300
|
sio_private_key.close()
|
|
982
|
-
signature_stream = priv_k.sign_ssh_data(
|
|
1301
|
+
signature_stream = priv_k.sign_ssh_data(encoded_message)
|
|
983
1302
|
signature_stream.rewind()
|
|
984
1303
|
base64_encoded = base64.b64encode(signature_stream.get_remainder())
|
|
985
1304
|
base64_encoded = base64_encoded.decode()
|
|
986
1305
|
return base64_encoded
|
|
987
1306
|
|
|
988
1307
|
|
|
989
|
-
def make_valid_did(lfn_dict):
|
|
1308
|
+
def make_valid_did(lfn_dict: dict[str, Any]) -> dict[str, Any]:
|
|
990
1309
|
"""
|
|
991
1310
|
When managing information about a LFN (such as in `rucio upload` or
|
|
992
1311
|
the RSE manager's upload), we add the `filename` attribute to record
|
|
@@ -1006,7 +1325,7 @@ def make_valid_did(lfn_dict):
|
|
|
1006
1325
|
return lfn_copy
|
|
1007
1326
|
|
|
1008
1327
|
|
|
1009
|
-
def send_trace(trace, trace_endpoint, user_agent, retries=5):
|
|
1328
|
+
def send_trace(trace: TraceDict, trace_endpoint: str, user_agent: str, retries: int = 5) -> int:
|
|
1010
1329
|
"""
|
|
1011
1330
|
Send the given trace to the trace endpoint
|
|
1012
1331
|
|
|
@@ -1027,7 +1346,7 @@ def send_trace(trace, trace_endpoint, user_agent, retries=5):
|
|
|
1027
1346
|
return 1
|
|
1028
1347
|
|
|
1029
1348
|
|
|
1030
|
-
def add_url_query(url, query):
|
|
1349
|
+
def add_url_query(url: str, query: dict[str, str]) -> str:
|
|
1031
1350
|
"""
|
|
1032
1351
|
Add a new dictionary to URL parameters
|
|
1033
1352
|
|
|
@@ -1043,7 +1362,7 @@ def add_url_query(url, query):
|
|
|
1043
1362
|
return urlunparse(url_parts)
|
|
1044
1363
|
|
|
1045
1364
|
|
|
1046
|
-
def get_bytes_value_from_string(input_string):
|
|
1365
|
+
def get_bytes_value_from_string(input_string: str) -> Union[bool, int]:
|
|
1047
1366
|
"""
|
|
1048
1367
|
Get bytes from a string that represents a storage value and unit
|
|
1049
1368
|
|
|
@@ -1073,7 +1392,7 @@ def get_bytes_value_from_string(input_string):
|
|
|
1073
1392
|
return False
|
|
1074
1393
|
|
|
1075
1394
|
|
|
1076
|
-
def parse_did_filter_from_string(input_string):
|
|
1395
|
+
def parse_did_filter_from_string(input_string: str) -> tuple[dict[str, Any], str]:
|
|
1077
1396
|
"""
|
|
1078
1397
|
Parse DID filter options in format 'length<3,type=all' from string.
|
|
1079
1398
|
|
|
@@ -1110,13 +1429,13 @@ def parse_did_filter_from_string(input_string):
|
|
|
1110
1429
|
value = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ')
|
|
1111
1430
|
|
|
1112
1431
|
if key == 'type':
|
|
1113
|
-
if value.upper() in ['ALL', 'COLLECTION', 'CONTAINER', 'DATASET', 'FILE']:
|
|
1114
|
-
type_ = value.lower()
|
|
1432
|
+
if value.upper() in ['ALL', 'COLLECTION', 'CONTAINER', 'DATASET', 'FILE']: # type: ignore
|
|
1433
|
+
type_ = value.lower() # type: ignore
|
|
1115
1434
|
else:
|
|
1116
1435
|
raise InvalidType('{0} is not a valid type. Valid types are {1}'.format(value, ['ALL', 'COLLECTION', 'CONTAINER', 'DATASET', 'FILE']))
|
|
1117
1436
|
elif key in ('length.gt', 'length.lt', 'length.gte', 'length.lte', 'length'):
|
|
1118
1437
|
try:
|
|
1119
|
-
value = int(value)
|
|
1438
|
+
value = int(value) # type: ignore
|
|
1120
1439
|
filters[key] = value
|
|
1121
1440
|
except ValueError:
|
|
1122
1441
|
raise ValueError('Length has to be an integer value.')
|
|
@@ -1133,7 +1452,12 @@ def parse_did_filter_from_string(input_string):
|
|
|
1133
1452
|
return filters, type_
|
|
1134
1453
|
|
|
1135
1454
|
|
|
1136
|
-
def parse_did_filter_from_string_fe(
|
|
1455
|
+
def parse_did_filter_from_string_fe(
|
|
1456
|
+
input_string: str,
|
|
1457
|
+
name: str = '*',
|
|
1458
|
+
type: str = 'collection',
|
|
1459
|
+
omit_name: bool = False
|
|
1460
|
+
) -> tuple[list[dict[str, Any]], str]:
|
|
1137
1461
|
"""
|
|
1138
1462
|
Parse DID filter string for the filter engine (fe).
|
|
1139
1463
|
|
|
@@ -1241,7 +1565,7 @@ def parse_did_filter_from_string_fe(input_string, name='*', type='collection', o
|
|
|
1241
1565
|
return filters, type
|
|
1242
1566
|
|
|
1243
1567
|
|
|
1244
|
-
def parse_replicas_from_file(path):
|
|
1568
|
+
def parse_replicas_from_file(path: "FileDescriptorOrPath") -> Any:
|
|
1245
1569
|
"""
|
|
1246
1570
|
Parses the output of list_replicas from a json or metalink file
|
|
1247
1571
|
into a dictionary. Metalink parsing is tried first and if it fails
|
|
@@ -1253,7 +1577,7 @@ def parse_replicas_from_file(path):
|
|
|
1253
1577
|
"""
|
|
1254
1578
|
with open(path) as fp:
|
|
1255
1579
|
try:
|
|
1256
|
-
root = ElementTree.parse(fp).getroot()
|
|
1580
|
+
root = ElementTree.parse(fp).getroot() # noqa: S314
|
|
1257
1581
|
return parse_replicas_metalink(root)
|
|
1258
1582
|
except ElementTree.ParseError as xml_err:
|
|
1259
1583
|
try:
|
|
@@ -1262,7 +1586,7 @@ def parse_replicas_from_file(path):
|
|
|
1262
1586
|
raise MetalinkJsonParsingError(path, xml_err, json_err)
|
|
1263
1587
|
|
|
1264
1588
|
|
|
1265
|
-
def parse_replicas_from_string(string):
|
|
1589
|
+
def parse_replicas_from_string(string: str) -> Any:
|
|
1266
1590
|
"""
|
|
1267
1591
|
Parses the output of list_replicas from a json or metalink string
|
|
1268
1592
|
into a dictionary. Metalink parsing is tried first and if it fails
|
|
@@ -1273,7 +1597,7 @@ def parse_replicas_from_string(string):
|
|
|
1273
1597
|
:returns: a list with a dictionary for each file
|
|
1274
1598
|
"""
|
|
1275
1599
|
try:
|
|
1276
|
-
root = ElementTree.fromstring(string)
|
|
1600
|
+
root = ElementTree.fromstring(string) # noqa: S314
|
|
1277
1601
|
return parse_replicas_metalink(root)
|
|
1278
1602
|
except ElementTree.ParseError as xml_err:
|
|
1279
1603
|
try:
|
|
@@ -1282,7 +1606,7 @@ def parse_replicas_from_string(string):
|
|
|
1282
1606
|
raise MetalinkJsonParsingError(string, xml_err, json_err)
|
|
1283
1607
|
|
|
1284
1608
|
|
|
1285
|
-
def parse_replicas_metalink(root):
|
|
1609
|
+
def parse_replicas_metalink(root: ElementTree.Element) -> list[dict[str, Any]]:
|
|
1286
1610
|
"""
|
|
1287
1611
|
Transforms the metalink tree into a list of dictionaries where
|
|
1288
1612
|
each dictionary describes a file with its replicas.
|
|
@@ -1339,11 +1663,15 @@ def parse_replicas_metalink(root):
|
|
|
1339
1663
|
return files
|
|
1340
1664
|
|
|
1341
1665
|
|
|
1342
|
-
def get_thread_with_periodic_running_function(
|
|
1666
|
+
def get_thread_with_periodic_running_function(
|
|
1667
|
+
interval: Union[int, float],
|
|
1668
|
+
action: Callable[..., Any],
|
|
1669
|
+
graceful_stop: threading.Event
|
|
1670
|
+
) -> threading.Thread:
|
|
1343
1671
|
"""
|
|
1344
1672
|
Get a thread where a function runs periodically.
|
|
1345
1673
|
|
|
1346
|
-
:param interval: Interval in seconds when the action
|
|
1674
|
+
:param interval: Interval in seconds when the action function should run.
|
|
1347
1675
|
:param action: Function, that should run periodically.
|
|
1348
1676
|
:param graceful_stop: Threading event used to check for graceful stop.
|
|
1349
1677
|
"""
|
|
@@ -1351,12 +1679,12 @@ def get_thread_with_periodic_running_function(interval, action, graceful_stop):
|
|
|
1351
1679
|
while not graceful_stop.is_set():
|
|
1352
1680
|
starttime = time.time()
|
|
1353
1681
|
action()
|
|
1354
|
-
time.sleep(interval - (
|
|
1682
|
+
time.sleep(interval - (time.time() - starttime))
|
|
1355
1683
|
t = threading.Thread(target=start)
|
|
1356
1684
|
return t
|
|
1357
1685
|
|
|
1358
1686
|
|
|
1359
|
-
def run_cmd_process(cmd, timeout=3600):
|
|
1687
|
+
def run_cmd_process(cmd: str, timeout: int = 3600) -> tuple[int, str]:
|
|
1360
1688
|
"""
|
|
1361
1689
|
shell command parser with timeout
|
|
1362
1690
|
|
|
@@ -1397,7 +1725,10 @@ def run_cmd_process(cmd, timeout=3600):
|
|
|
1397
1725
|
return returncode, stdout
|
|
1398
1726
|
|
|
1399
1727
|
|
|
1400
|
-
def
|
|
1728
|
+
def gateway_update_return_dict(
|
|
1729
|
+
dictionary: dict[str, Any],
|
|
1730
|
+
session: Optional["Session"] = None
|
|
1731
|
+
) -> dict[str, Any]:
|
|
1401
1732
|
"""
|
|
1402
1733
|
Ensure that rse is in a dictionary returned from core
|
|
1403
1734
|
|
|
@@ -1435,7 +1766,12 @@ def api_update_return_dict(dictionary, session=None):
|
|
|
1435
1766
|
return dictionary
|
|
1436
1767
|
|
|
1437
1768
|
|
|
1438
|
-
def setup_logger(
|
|
1769
|
+
def setup_logger(
|
|
1770
|
+
module_name: Optional[str] = None,
|
|
1771
|
+
logger_name: Optional[str] = None,
|
|
1772
|
+
logger_level: Optional[int] = None,
|
|
1773
|
+
verbose: bool = False
|
|
1774
|
+
) -> logging.Logger:
|
|
1439
1775
|
'''
|
|
1440
1776
|
Factory method to set logger with handlers.
|
|
1441
1777
|
:param module_name: __name__ of the module that is calling this method
|
|
@@ -1444,10 +1780,10 @@ def setup_logger(module_name=None, logger_name=None, logger_level=None, verbose=
|
|
|
1444
1780
|
:param verbose: verbose option set in bin/rucio
|
|
1445
1781
|
'''
|
|
1446
1782
|
# helper method for cfg check
|
|
1447
|
-
def _force_cfg_log_level(cfg_option):
|
|
1783
|
+
def _force_cfg_log_level(cfg_option: str) -> bool:
|
|
1448
1784
|
cfg_forced_modules = config_get('logging', cfg_option, raise_exception=False, default=None, clean_cached=True,
|
|
1449
1785
|
check_config_table=False)
|
|
1450
|
-
if cfg_forced_modules:
|
|
1786
|
+
if cfg_forced_modules and module_name is not None:
|
|
1451
1787
|
if re.match(str(cfg_forced_modules), module_name):
|
|
1452
1788
|
return True
|
|
1453
1789
|
return False
|
|
@@ -1477,11 +1813,11 @@ def setup_logger(module_name=None, logger_name=None, logger_level=None, verbose=
|
|
|
1477
1813
|
logger.setLevel(logger_level)
|
|
1478
1814
|
|
|
1479
1815
|
# preferred logger handling
|
|
1480
|
-
def add_handler(logger):
|
|
1816
|
+
def add_handler(logger: logging.Logger) -> None:
|
|
1481
1817
|
hdlr = logging.StreamHandler()
|
|
1482
1818
|
|
|
1483
|
-
def emit_decorator(fnc):
|
|
1484
|
-
def func(*args):
|
|
1819
|
+
def emit_decorator(fnc: Callable[..., Any]) -> Callable[..., Any]:
|
|
1820
|
+
def func(*args) -> Callable[..., Any]:
|
|
1485
1821
|
if 'RUCIO_LOGGING_FORMAT' not in os.environ:
|
|
1486
1822
|
levelno = args[0].levelno
|
|
1487
1823
|
format_str = '%(asctime)s\t%(levelname)s\t%(message)s\033[0m'
|
|
@@ -1514,7 +1850,12 @@ def setup_logger(module_name=None, logger_name=None, logger_level=None, verbose=
|
|
|
1514
1850
|
return logger
|
|
1515
1851
|
|
|
1516
1852
|
|
|
1517
|
-
def daemon_sleep(
|
|
1853
|
+
def daemon_sleep(
|
|
1854
|
+
start_time: float,
|
|
1855
|
+
sleep_time: float,
|
|
1856
|
+
graceful_stop: threading.Event,
|
|
1857
|
+
logger: "LoggerFunction" = logging.log
|
|
1858
|
+
) -> None:
|
|
1518
1859
|
"""Sleeps a daemon the time provided by sleep_time"""
|
|
1519
1860
|
end_time = time.time()
|
|
1520
1861
|
time_diff = end_time - start_time
|
|
@@ -1523,7 +1864,7 @@ def daemon_sleep(start_time, sleep_time, graceful_stop, logger=logging.log):
|
|
|
1523
1864
|
graceful_stop.wait(sleep_time - time_diff)
|
|
1524
1865
|
|
|
1525
1866
|
|
|
1526
|
-
def is_client():
|
|
1867
|
+
def is_client() -> bool:
|
|
1527
1868
|
""""
|
|
1528
1869
|
Checks if the function is called from a client or from a server/daemon
|
|
1529
1870
|
|
|
@@ -1537,7 +1878,7 @@ def is_client():
|
|
|
1537
1878
|
client_mode = True
|
|
1538
1879
|
else:
|
|
1539
1880
|
client_mode = False
|
|
1540
|
-
except RuntimeError:
|
|
1881
|
+
except (RuntimeError, ConfigNotFound):
|
|
1541
1882
|
# If no configuration file is found the default value should be True
|
|
1542
1883
|
client_mode = True
|
|
1543
1884
|
else:
|
|
@@ -1552,15 +1893,15 @@ def is_client():
|
|
|
1552
1893
|
class retry:
|
|
1553
1894
|
"""Retry callable object with configuragle number of attempts"""
|
|
1554
1895
|
|
|
1555
|
-
def __init__(self, func, *args, **kwargs):
|
|
1896
|
+
def __init__(self, func: Callable[..., Any], *args, **kwargs):
|
|
1556
1897
|
'''
|
|
1557
1898
|
:param func: a method that should be executed with retries
|
|
1558
|
-
:param args:
|
|
1899
|
+
:param args: parameters of the func
|
|
1559
1900
|
:param kwargs: key word arguments of the func
|
|
1560
1901
|
'''
|
|
1561
1902
|
self.func, self.args, self.kwargs = func, args, kwargs
|
|
1562
1903
|
|
|
1563
|
-
def __call__(self, mtries=3, logger=logging.log):
|
|
1904
|
+
def __call__(self, mtries: int = 3, logger: "LoggerFunction" = logging.log) -> Callable[..., Any]:
|
|
1564
1905
|
'''
|
|
1565
1906
|
:param mtries: maximum number of attempts to execute the function
|
|
1566
1907
|
:param logger: preferred logger
|
|
@@ -1586,9 +1927,9 @@ class StoreAndDeprecateWarningAction(argparse.Action):
|
|
|
1586
1927
|
'''
|
|
1587
1928
|
|
|
1588
1929
|
def __init__(self,
|
|
1589
|
-
option_strings,
|
|
1590
|
-
new_option_string,
|
|
1591
|
-
dest,
|
|
1930
|
+
option_strings: Sequence[str],
|
|
1931
|
+
new_option_string: str,
|
|
1932
|
+
dest: str,
|
|
1592
1933
|
**kwargs):
|
|
1593
1934
|
"""
|
|
1594
1935
|
:param option_strings: all possible argument name strings
|
|
@@ -1600,10 +1941,11 @@ class StoreAndDeprecateWarningAction(argparse.Action):
|
|
|
1600
1941
|
option_strings=option_strings,
|
|
1601
1942
|
dest=dest,
|
|
1602
1943
|
**kwargs)
|
|
1603
|
-
|
|
1944
|
+
if new_option_string not in option_strings:
|
|
1945
|
+
raise ValueError("%s not supported as a string option." % new_option_string)
|
|
1604
1946
|
self.new_option_string = new_option_string
|
|
1605
1947
|
|
|
1606
|
-
def __call__(self, parser, namespace, values, option_string=None):
|
|
1948
|
+
def __call__(self, parser, namespace, values, option_string: Optional[str] = None):
|
|
1607
1949
|
if option_string and option_string != self.new_option_string:
|
|
1608
1950
|
# The logger gets typically initialized after the argument parser
|
|
1609
1951
|
# to set the verbosity of the logger. Thus using simple print to console.
|
|
@@ -1619,12 +1961,12 @@ class StoreTrueAndDeprecateWarningAction(argparse._StoreConstAction):
|
|
|
1619
1961
|
'''
|
|
1620
1962
|
|
|
1621
1963
|
def __init__(self,
|
|
1622
|
-
option_strings,
|
|
1623
|
-
new_option_string,
|
|
1624
|
-
dest,
|
|
1625
|
-
default=False,
|
|
1626
|
-
required=False,
|
|
1627
|
-
help=None):
|
|
1964
|
+
option_strings: Sequence[str],
|
|
1965
|
+
new_option_string: str,
|
|
1966
|
+
dest: str,
|
|
1967
|
+
default: bool = False,
|
|
1968
|
+
required: bool = False,
|
|
1969
|
+
help: Optional[str] = None):
|
|
1628
1970
|
"""
|
|
1629
1971
|
:param option_strings: all possible argument name strings
|
|
1630
1972
|
:param new_option_string: the new option string which replaces the old
|
|
@@ -1638,10 +1980,11 @@ class StoreTrueAndDeprecateWarningAction(argparse._StoreConstAction):
|
|
|
1638
1980
|
default=default,
|
|
1639
1981
|
required=required,
|
|
1640
1982
|
help=help)
|
|
1641
|
-
|
|
1983
|
+
if new_option_string not in option_strings:
|
|
1984
|
+
raise ValueError("%s not supported as a string option." % new_option_string)
|
|
1642
1985
|
self.new_option_string = new_option_string
|
|
1643
1986
|
|
|
1644
|
-
def __call__(self, parser, namespace, values, option_string=None):
|
|
1987
|
+
def __call__(self, parser, namespace, values, option_string: Optional[str] = None):
|
|
1645
1988
|
super(StoreTrueAndDeprecateWarningAction, self).__call__(parser, namespace, values, option_string=option_string)
|
|
1646
1989
|
if option_string and option_string != self.new_option_string:
|
|
1647
1990
|
# The logger gets typically initialized after the argument parser
|
|
@@ -1660,7 +2003,7 @@ class PriorityQueue:
|
|
|
1660
2003
|
[1] https://en.wikipedia.org/wiki/Heap_(data_structure)
|
|
1661
2004
|
"""
|
|
1662
2005
|
class ContainerSlot:
|
|
1663
|
-
def __init__(self, position, priority):
|
|
2006
|
+
def __init__(self, position: int, priority: int):
|
|
1664
2007
|
self.pos = position
|
|
1665
2008
|
self.prio = priority
|
|
1666
2009
|
|
|
@@ -1752,78 +2095,9 @@ class PriorityQueue:
|
|
|
1752
2095
|
return heap_changed
|
|
1753
2096
|
|
|
1754
2097
|
|
|
1755
|
-
def
|
|
1756
|
-
'''
|
|
1757
|
-
Loads all the algorithms of a given type from the policy package(s) and registers them
|
|
1758
|
-
:param algorithm_type: the type of algorithm to register (e.g. 'surl', 'lfn2pfn')
|
|
1759
|
-
:param dictionary: the dictionary to register them in
|
|
1760
|
-
:param vo: the name of the relevant VO (None for single VO)
|
|
1761
|
-
'''
|
|
1762
|
-
def try_importing_policy(algorithm_type, dictionary, vo=None):
|
|
1763
|
-
import importlib
|
|
1764
|
-
try:
|
|
1765
|
-
env_name = 'RUCIO_POLICY_PACKAGE' + ('' if not vo else '_' + vo.upper())
|
|
1766
|
-
if env_name in os.environ:
|
|
1767
|
-
package = os.environ[env_name]
|
|
1768
|
-
else:
|
|
1769
|
-
package = config.config_get('policy', 'package' + ('' if not vo else '-' + vo))
|
|
1770
|
-
check_policy_package_version(package)
|
|
1771
|
-
module = importlib.import_module(package)
|
|
1772
|
-
if hasattr(module, 'get_algorithms'):
|
|
1773
|
-
all_algorithms = module.get_algorithms()
|
|
1774
|
-
if algorithm_type in all_algorithms:
|
|
1775
|
-
algorithms = all_algorithms[algorithm_type]
|
|
1776
|
-
if not vo:
|
|
1777
|
-
dictionary.update(algorithms)
|
|
1778
|
-
else:
|
|
1779
|
-
# check that the names are correctly prefixed
|
|
1780
|
-
for k in algorithms.keys():
|
|
1781
|
-
if k.lower().startswith(vo.lower()):
|
|
1782
|
-
dictionary[k] = algorithms[k]
|
|
1783
|
-
else:
|
|
1784
|
-
raise InvalidAlgorithmName(k, vo)
|
|
1785
|
-
except (NoOptionError, NoSectionError, ImportError):
|
|
1786
|
-
pass
|
|
1787
|
-
|
|
1788
|
-
from rucio.common import config
|
|
1789
|
-
try:
|
|
1790
|
-
multivo = config.config_get_bool('common', 'multi_vo')
|
|
1791
|
-
except (NoOptionError, NoSectionError):
|
|
1792
|
-
multivo = False
|
|
1793
|
-
if not multivo:
|
|
1794
|
-
# single policy package
|
|
1795
|
-
try_importing_policy(algorithm_type, dictionary)
|
|
1796
|
-
else:
|
|
1797
|
-
# determine whether on client or server
|
|
1798
|
-
client = False
|
|
1799
|
-
if 'RUCIO_CLIENT_MODE' not in os.environ:
|
|
1800
|
-
if not config.config_has_section('database') and config.config_has_section('client'):
|
|
1801
|
-
client = True
|
|
1802
|
-
else:
|
|
1803
|
-
if os.environ['RUCIO_CLIENT_MODE']:
|
|
1804
|
-
client = True
|
|
1805
|
-
|
|
1806
|
-
# on client, only register algorithms for selected VO
|
|
1807
|
-
if client:
|
|
1808
|
-
if 'RUCIO_VO' in os.environ:
|
|
1809
|
-
vo = os.environ['RUCIO_VO']
|
|
1810
|
-
else:
|
|
1811
|
-
try:
|
|
1812
|
-
vo = config.config_get('client', 'vo')
|
|
1813
|
-
except (NoOptionError, NoSectionError):
|
|
1814
|
-
vo = 'def'
|
|
1815
|
-
try_importing_policy(algorithm_type, dictionary, vo)
|
|
1816
|
-
# on server, list all VOs and register their algorithms
|
|
1817
|
-
else:
|
|
1818
|
-
from rucio.core.vo import list_vos
|
|
1819
|
-
# policy package per VO
|
|
1820
|
-
vos = list_vos()
|
|
1821
|
-
for vo in vos:
|
|
1822
|
-
try_importing_policy(algorithm_type, dictionary, vo['vo'])
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
def check_policy_package_version(package):
|
|
2098
|
+
def check_policy_package_version(package: str) -> None:
|
|
1826
2099
|
import importlib
|
|
2100
|
+
|
|
1827
2101
|
from rucio.version import version_string
|
|
1828
2102
|
'''
|
|
1829
2103
|
Checks that the Rucio version supported by the policy package is compatible
|
|
@@ -1842,13 +2116,13 @@ def check_policy_package_version(package):
|
|
|
1842
2116
|
components = 2 if version_string().startswith("1.") else 1
|
|
1843
2117
|
current_version = ".".join(version_string().split(".")[:components])
|
|
1844
2118
|
if current_version not in supported_version:
|
|
1845
|
-
raise PolicyPackageVersionError(package)
|
|
2119
|
+
raise PolicyPackageVersionError(package, current_version, supported_version)
|
|
1846
2120
|
|
|
1847
2121
|
|
|
1848
2122
|
class Availability:
|
|
1849
2123
|
"""
|
|
1850
2124
|
This util class acts as a translator between the availability stored as
|
|
1851
|
-
integer and as
|
|
2125
|
+
integer and as boolean values.
|
|
1852
2126
|
|
|
1853
2127
|
`None` represents a missing value. This lets a user update a specific value
|
|
1854
2128
|
without altering the other ones. If it needs to be evaluated, it will
|
|
@@ -1859,7 +2133,12 @@ class Availability:
|
|
|
1859
2133
|
write = None
|
|
1860
2134
|
delete = None
|
|
1861
2135
|
|
|
1862
|
-
def __init__(
|
|
2136
|
+
def __init__(
|
|
2137
|
+
self,
|
|
2138
|
+
read: Optional[bool] = None,
|
|
2139
|
+
write: Optional[bool] = None,
|
|
2140
|
+
delete: Optional[bool] = None
|
|
2141
|
+
):
|
|
1863
2142
|
self.read = read
|
|
1864
2143
|
self.write = write
|
|
1865
2144
|
self.delete = delete
|
|
@@ -1944,3 +2223,16 @@ def retrying(
|
|
|
1944
2223
|
time.sleep(wait_fixed / 1000.0)
|
|
1945
2224
|
return _wrapper
|
|
1946
2225
|
return _decorator
|
|
2226
|
+
|
|
2227
|
+
|
|
2228
|
+
def deep_merge_dict(source: dict, destination: dict) -> dict:
|
|
2229
|
+
"""Merge two dictionaries together recursively"""
|
|
2230
|
+
for key, value in source.items():
|
|
2231
|
+
if isinstance(value, dict):
|
|
2232
|
+
# get node or create one
|
|
2233
|
+
node = destination.setdefault(key, {})
|
|
2234
|
+
deep_merge_dict(value, node)
|
|
2235
|
+
else:
|
|
2236
|
+
destination[key] = value
|
|
2237
|
+
|
|
2238
|
+
return destination
|