rucio-clients 35.7.0__py3-none-any.whl → 37.0.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/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/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.0.data}/data/etc/rucio.cfg.atlas.client.template +3 -2
- {rucio_clients-35.7.0.data → rucio_clients-37.0.0.data}/data/etc/rucio.cfg.template +3 -19
- {rucio_clients-35.7.0.data → rucio_clients-37.0.0.data}/data/requirements.client.txt +11 -7
- rucio_clients-37.0.0.data/scripts/rucio +133 -0
- rucio_clients-37.0.0.data/scripts/rucio-admin +97 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.dist-info}/METADATA +18 -14
- rucio_clients-37.0.0.dist-info/RECORD +104 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.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.0.data}/data/etc/rse-accounts.cfg.template +0 -0
- {rucio_clients-35.7.0.data → rucio_clients-37.0.0.data}/data/rucio_client/merge_rucio_configs.py +0 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.dist-info}/WHEEL +0 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.dist-info}/licenses/LICENSE +0 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Copyright European Organization for Nuclear Research (CERN) since 2012
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import copy
|
|
15
|
+
import hashlib
|
|
16
|
+
import itertools
|
|
17
|
+
import math
|
|
18
|
+
import os
|
|
19
|
+
import time
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
21
|
+
|
|
22
|
+
from rucio.common.exception import RucioException
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from _typeshed import FileDescriptorOrPath
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _next_pow2(num: int) -> int:
|
|
29
|
+
if not num:
|
|
30
|
+
return 0
|
|
31
|
+
return math.ceil(math.log2(num))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _bittorrent_v2_piece_length_pow2(file_size: int) -> int:
|
|
35
|
+
"""
|
|
36
|
+
Automatically chooses the `piece size` so that `piece layers`
|
|
37
|
+
is kept small(er) than usually. This is a balancing act:
|
|
38
|
+
having a big piece_length requires more work on bittorrent client
|
|
39
|
+
side to validate hashes, but having it small requires more
|
|
40
|
+
place to store the `piece layers` in the database.
|
|
41
|
+
|
|
42
|
+
Returns the result as the exponent 'x' for power of 2.
|
|
43
|
+
To get the actual length in bytes, the caller should compute 2^x.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
# by the bittorrent v2 specification, the piece size is equal to block size = 16KiB
|
|
47
|
+
min_piece_len_pow2 = 14 # 2 ** 14 == 16 KiB
|
|
48
|
+
if not file_size:
|
|
49
|
+
return min_piece_len_pow2
|
|
50
|
+
# Limit the maximum size of pieces_layers hash chain for bittorrent v2,
|
|
51
|
+
# because we'll have to store it in the database
|
|
52
|
+
max_pieces_layers_size_pow2 = 20 # 2 ** 20 == 1 MiB
|
|
53
|
+
# sha256 requires 2 ** 5 == 32 Bytes == 256 bits
|
|
54
|
+
hash_size_pow2 = 5
|
|
55
|
+
|
|
56
|
+
# The closest power of two bigger than the file size
|
|
57
|
+
file_size_pow2 = _next_pow2(file_size)
|
|
58
|
+
|
|
59
|
+
# Compute the target size for the 'pieces layers' in the torrent
|
|
60
|
+
# (as power of two: the closest power-of-two smaller than the number)
|
|
61
|
+
# Will cap at max_pieces_layers_size for files larger than 1TB.
|
|
62
|
+
target_pieces_layers_size = math.sqrt(file_size)
|
|
63
|
+
target_pieces_layers_size_pow2 = min(math.floor(math.log2(target_pieces_layers_size)), max_pieces_layers_size_pow2)
|
|
64
|
+
target_piece_num_pow2 = max(target_pieces_layers_size_pow2 - hash_size_pow2, 0)
|
|
65
|
+
|
|
66
|
+
piece_length_pow2 = max(file_size_pow2 - target_piece_num_pow2, min_piece_len_pow2)
|
|
67
|
+
return piece_length_pow2
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def bittorrent_v2_piece_length(file_size: int) -> int:
|
|
71
|
+
return 2 ** _bittorrent_v2_piece_length_pow2(file_size)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def bittorrent_v2_merkle_sha256(file: "FileDescriptorOrPath") -> tuple[bytes, bytes, int]:
|
|
75
|
+
"""
|
|
76
|
+
Compute the .torrent v2 hash tree for the given file.
|
|
77
|
+
(http://www.bittorrent.org/beps/bep_0052.html)
|
|
78
|
+
In particular, it will return the root of the merkle hash
|
|
79
|
+
tree of the file, the 'piece layers' as described in the
|
|
80
|
+
previous BEP, and the chosen `piece size`
|
|
81
|
+
|
|
82
|
+
This function will read the file in chunks of 16KiB
|
|
83
|
+
(which is the imposed block size by bittorrent v2) and compute
|
|
84
|
+
the sha256 hash of each block. When enough blocks are read
|
|
85
|
+
to form a `piece`, will compute the merkle hash root of the
|
|
86
|
+
piece from the hashes of its blocks. At the end, the hashes
|
|
87
|
+
of pieces are combined to create the global pieces_root.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
# by the bittorrent v2 specification, the block size and the
|
|
91
|
+
# minimum piece size are both fixed to 16KiB
|
|
92
|
+
block_size = 16384
|
|
93
|
+
block_size_pow2 = 14 # 2 ** 14 == 16 KiB
|
|
94
|
+
# sha256 requires 2 ** 5 == 32 Bytes == 256 bits
|
|
95
|
+
hash_size = 32
|
|
96
|
+
|
|
97
|
+
def _merkle_root(leafs: list[bytes], nb_levels: int, padding: bytes) -> bytes:
|
|
98
|
+
"""
|
|
99
|
+
Build the root of the merkle hash tree from the (possibly incomplete) leafs layer.
|
|
100
|
+
If len(leafs) < 2 ** nb_levels, it will be padded with the padding repeated as many times
|
|
101
|
+
as needed to have 2 ** nb_levels leafs in total.
|
|
102
|
+
"""
|
|
103
|
+
nodes = copy.copy(leafs)
|
|
104
|
+
level = nb_levels
|
|
105
|
+
|
|
106
|
+
while level > 0:
|
|
107
|
+
for i in range(2 ** (level - 1)):
|
|
108
|
+
node1 = nodes[2 * i] if 2 * i < len(nodes) else padding
|
|
109
|
+
node2 = nodes[2 * i + 1] if 2 * i + 1 < len(nodes) else padding
|
|
110
|
+
h = hashlib.sha256(node1)
|
|
111
|
+
h.update(node2)
|
|
112
|
+
if i < len(nodes):
|
|
113
|
+
nodes[i] = h.digest()
|
|
114
|
+
else:
|
|
115
|
+
nodes.append(h.digest())
|
|
116
|
+
level -= 1
|
|
117
|
+
return nodes[0] if nodes else padding
|
|
118
|
+
|
|
119
|
+
file_size = os.stat(file).st_size
|
|
120
|
+
piece_length_pow2 = _bittorrent_v2_piece_length_pow2(file_size)
|
|
121
|
+
|
|
122
|
+
block_per_piece_pow2 = piece_length_pow2 - block_size_pow2
|
|
123
|
+
piece_length = 2 ** piece_length_pow2
|
|
124
|
+
block_per_piece = 2 ** block_per_piece_pow2
|
|
125
|
+
piece_num = math.ceil(file_size / piece_length)
|
|
126
|
+
|
|
127
|
+
remaining = file_size
|
|
128
|
+
remaining_in_block = min(file_size, block_size)
|
|
129
|
+
block_hashes = []
|
|
130
|
+
piece_hashes = []
|
|
131
|
+
current_hash = hashlib.sha256()
|
|
132
|
+
block_padding = bytes(hash_size)
|
|
133
|
+
with open(file, 'rb') as f:
|
|
134
|
+
while True:
|
|
135
|
+
data = f.read(remaining_in_block)
|
|
136
|
+
if not data:
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
current_hash.update(data)
|
|
140
|
+
|
|
141
|
+
remaining_in_block -= len(data)
|
|
142
|
+
remaining -= len(data)
|
|
143
|
+
|
|
144
|
+
if not remaining_in_block:
|
|
145
|
+
block_hashes.append(current_hash.digest())
|
|
146
|
+
if len(block_hashes) == block_per_piece or not remaining:
|
|
147
|
+
piece_hashes.append(_merkle_root(block_hashes, nb_levels=block_per_piece_pow2, padding=block_padding))
|
|
148
|
+
block_hashes = []
|
|
149
|
+
current_hash = hashlib.sha256()
|
|
150
|
+
remaining_in_block = min(block_size, remaining)
|
|
151
|
+
|
|
152
|
+
if not remaining:
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
if remaining or remaining_in_block or len(piece_hashes) != piece_num:
|
|
156
|
+
raise RucioException(f'Error while computing merkle sha256 of {file}')
|
|
157
|
+
|
|
158
|
+
piece_padding = _merkle_root([], nb_levels=block_per_piece_pow2, padding=block_padding)
|
|
159
|
+
pieces_root = _merkle_root(piece_hashes, nb_levels=_next_pow2(piece_num), padding=piece_padding)
|
|
160
|
+
pieces_layers = b''.join(piece_hashes) if len(piece_hashes) > 1 else b''
|
|
161
|
+
|
|
162
|
+
return pieces_root, pieces_layers, piece_length
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def bencode(obj: Union[int, bytes, str, list, dict[bytes, Any]]) -> bytes:
|
|
166
|
+
"""
|
|
167
|
+
Copied from the reference implementation of v2 bittorrent:
|
|
168
|
+
http://bittorrent.org/beps/bep_0052_torrent_creator.py
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
if isinstance(obj, int):
|
|
172
|
+
return b"i" + str(obj).encode() + b"e"
|
|
173
|
+
elif isinstance(obj, bytes):
|
|
174
|
+
return str(len(obj)).encode() + b":" + obj
|
|
175
|
+
elif isinstance(obj, str):
|
|
176
|
+
return bencode(obj.encode("utf-8"))
|
|
177
|
+
elif isinstance(obj, list):
|
|
178
|
+
return b"l" + b"".join(map(bencode, obj)) + b"e"
|
|
179
|
+
elif isinstance(obj, dict):
|
|
180
|
+
if all(isinstance(i, bytes) for i in obj.keys()):
|
|
181
|
+
items = list(obj.items())
|
|
182
|
+
items.sort()
|
|
183
|
+
return b"d" + b"".join(map(bencode, itertools.chain(*items))) + b"e"
|
|
184
|
+
else:
|
|
185
|
+
raise ValueError("dict keys should be bytes " + str(obj.keys()))
|
|
186
|
+
raise ValueError("Allowed types: int, bytes, str, list, dict; not %s", type(obj))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def construct_torrent(
|
|
190
|
+
scope: str,
|
|
191
|
+
name: str,
|
|
192
|
+
length: int,
|
|
193
|
+
piece_length: int,
|
|
194
|
+
pieces_root: bytes,
|
|
195
|
+
pieces_layers: "Optional[bytes]" = None,
|
|
196
|
+
trackers: "Optional[list[str]]" = None,
|
|
197
|
+
) -> "tuple[str, bytes]":
|
|
198
|
+
|
|
199
|
+
torrent_dict = {
|
|
200
|
+
b'creation date': int(time.time()),
|
|
201
|
+
b'info': {
|
|
202
|
+
b'meta version': 2,
|
|
203
|
+
b'private': 1,
|
|
204
|
+
b'name': f'{scope}:{name}'.encode(),
|
|
205
|
+
b'piece length': piece_length,
|
|
206
|
+
b'file tree': {
|
|
207
|
+
name.encode(): {
|
|
208
|
+
b'': {
|
|
209
|
+
b'length': length,
|
|
210
|
+
b'pieces root': pieces_root,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
b'piece layers': {},
|
|
216
|
+
}
|
|
217
|
+
if trackers:
|
|
218
|
+
torrent_dict[b'announce'] = trackers[0].encode()
|
|
219
|
+
if len(trackers) > 1:
|
|
220
|
+
torrent_dict[b'announce-list'] = [t.encode() for t in trackers]
|
|
221
|
+
if pieces_layers:
|
|
222
|
+
torrent_dict[b'piece layers'][pieces_root] = pieces_layers
|
|
223
|
+
|
|
224
|
+
torrent_id = hashlib.sha256(bencode(torrent_dict[b'info'])).hexdigest()[:40]
|
|
225
|
+
torrent = bencode(torrent_dict)
|
|
226
|
+
return torrent_id, torrent
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def merkle_sha256(file: "FileDescriptorOrPath") -> str:
|
|
230
|
+
"""
|
|
231
|
+
The root of the sha256 merkle hash tree with leaf size of 16 KiB.
|
|
232
|
+
"""
|
|
233
|
+
pieces_root, _, _ = bittorrent_v2_merkle_sha256(file)
|
|
234
|
+
return pieces_root.hex()
|
rucio/common/cache.py
CHANGED
|
@@ -12,18 +12,16 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
from typing import TYPE_CHECKING
|
|
15
|
+
from typing import TYPE_CHECKING, Optional
|
|
16
16
|
|
|
17
|
-
from dogpile.cache import
|
|
17
|
+
from dogpile.cache.region import CacheRegion
|
|
18
18
|
|
|
19
|
+
from rucio.common.client import is_client
|
|
19
20
|
from rucio.common.config import config_get
|
|
20
|
-
from rucio.common.utils import is_client
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
23
|
from collections.abc import Callable
|
|
24
|
-
from typing import Optional
|
|
25
24
|
|
|
26
|
-
from dogpile.cache.region import CacheRegion
|
|
27
25
|
|
|
28
26
|
CACHE_URL = config_get('cache', 'url', False, '127.0.0.1:11211', check_config_table=False)
|
|
29
27
|
|
|
@@ -45,30 +43,69 @@ finally:
|
|
|
45
43
|
_mc_client.close()
|
|
46
44
|
|
|
47
45
|
|
|
48
|
-
|
|
49
|
-
expiration_time: int,
|
|
50
|
-
function_key_generator: "Optional[Callable]" = None,
|
|
51
|
-
memcached_expire_time: "Optional[int]" = None
|
|
52
|
-
) -> "CacheRegion":
|
|
46
|
+
class MemcacheRegion(CacheRegion):
|
|
53
47
|
"""
|
|
54
|
-
|
|
48
|
+
Subclass of CacheRegion.
|
|
49
|
+
It uses pymemcache as backend if ENABLE_CACHING is True,
|
|
50
|
+
otherwise it it configured to null.
|
|
55
51
|
"""
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
expiration_time: int,
|
|
55
|
+
function_key_generator: Optional['Callable'] = None,
|
|
56
|
+
memcached_expire_time: Optional[int] = None
|
|
57
|
+
):
|
|
58
|
+
if function_key_generator:
|
|
59
|
+
super().__init__(function_key_generator=function_key_generator)
|
|
60
|
+
else:
|
|
61
|
+
super().__init__()
|
|
62
|
+
self._configure_region(expiration_time, memcached_expire_time)
|
|
63
|
+
|
|
64
|
+
def _configure_region(
|
|
65
|
+
self,
|
|
66
|
+
expiration_time: int,
|
|
67
|
+
memcached_expire_time: Optional[int]
|
|
68
|
+
) -> None:
|
|
69
|
+
if ENABLE_CACHING:
|
|
70
|
+
self.configure(
|
|
71
|
+
'dogpile.cache.pymemcache',
|
|
72
|
+
expiration_time=expiration_time,
|
|
73
|
+
arguments={
|
|
74
|
+
'url': CACHE_URL,
|
|
75
|
+
'distributed_lock': True,
|
|
76
|
+
'memcached_expire_time': memcached_expire_time if memcached_expire_time else expiration_time + 60, # must be bigger than expiration_time
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
self.configure('dogpile.cache.null')
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CacheKey:
|
|
84
|
+
"""
|
|
85
|
+
Helper class to generate cache keys
|
|
86
|
+
based on sections and options.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _generate_key(*args: str) -> str:
|
|
91
|
+
return '_'.join(args)
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def has_section(section: str) -> str:
|
|
95
|
+
return CacheKey._generate_key('has_section', section)
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def options(section: str) -> str:
|
|
99
|
+
return CacheKey._generate_key('options', section)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def has_option(section: str, option: str) -> str:
|
|
103
|
+
return CacheKey._generate_key('has_option', section, option)
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def items(section: str) -> str:
|
|
107
|
+
return CacheKey._generate_key('items', section)
|
|
73
108
|
|
|
74
|
-
|
|
109
|
+
@staticmethod
|
|
110
|
+
def value(section: str, option: str) -> str:
|
|
111
|
+
return CacheKey._generate_key('get', section, option)
|
rucio/common/checksum.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Copyright European Organization for Nuclear Research (CERN) since 2012
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import hashlib
|
|
15
|
+
import io
|
|
16
|
+
import mmap
|
|
17
|
+
import zlib
|
|
18
|
+
from functools import partial
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
from rucio.common.bittorrent import merkle_sha256
|
|
22
|
+
from rucio.common.exception import ChecksumCalculationError
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from _typeshed import FileDescriptorOrPath
|
|
26
|
+
|
|
27
|
+
# GLOBALLY_SUPPORTED_CHECKSUMS = ['adler32', 'md5', 'sha256', 'crc32']
|
|
28
|
+
GLOBALLY_SUPPORTED_CHECKSUMS = ['adler32', 'md5']
|
|
29
|
+
PREFERRED_CHECKSUM = GLOBALLY_SUPPORTED_CHECKSUMS[0]
|
|
30
|
+
CHECKSUM_KEY = 'supported_checksums'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_checksum_valid(checksum_name: str) -> bool:
|
|
34
|
+
"""
|
|
35
|
+
A simple function to check whether a checksum algorithm is supported.
|
|
36
|
+
Relies on GLOBALLY_SUPPORTED_CHECKSUMS to allow for expandability.
|
|
37
|
+
|
|
38
|
+
:param checksum_name: The name of the checksum to be verified.
|
|
39
|
+
:returns: True if checksum_name is in GLOBALLY_SUPPORTED_CHECKSUMS list, False otherwise.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
return checksum_name in GLOBALLY_SUPPORTED_CHECKSUMS
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def set_preferred_checksum(checksum_name: str) -> None:
|
|
46
|
+
"""
|
|
47
|
+
If the input checksum name is valid,
|
|
48
|
+
set it as PREFERRED_CHECKSUM.
|
|
49
|
+
|
|
50
|
+
:param checksum_name: The name of the checksum to be verified.
|
|
51
|
+
"""
|
|
52
|
+
if is_checksum_valid(checksum_name):
|
|
53
|
+
global PREFERRED_CHECKSUM
|
|
54
|
+
PREFERRED_CHECKSUM = checksum_name
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _iter_blocks(fobj):
|
|
58
|
+
"""Iterate over blocks in a binary file-like object.
|
|
59
|
+
|
|
60
|
+
Uses blocks of size ``io.DEFAULT_BUFFER_SIZE * 8``.
|
|
61
|
+
"""
|
|
62
|
+
block_size = io.DEFAULT_BUFFER_SIZE * 8
|
|
63
|
+
return iter(partial(fobj.read, block_size), b'')
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def adler32(file: "FileDescriptorOrPath") -> str:
|
|
67
|
+
"""
|
|
68
|
+
An Adler-32 checksum is obtained by calculating two 16-bit checksums A and B
|
|
69
|
+
and concatenating their bits into a 32-bit integer. A is the sum of all bytes in the
|
|
70
|
+
stream plus one, and B is the sum of the individual values of A from each step.
|
|
71
|
+
|
|
72
|
+
:param file: file name
|
|
73
|
+
:returns: Hexified string, padded to 8 values.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
# adler starting value is _not_ 0
|
|
77
|
+
adler = 1
|
|
78
|
+
|
|
79
|
+
can_mmap = False
|
|
80
|
+
# try:
|
|
81
|
+
# with open(file, 'r+b') as f:
|
|
82
|
+
# can_mmap = True
|
|
83
|
+
# except:
|
|
84
|
+
# pass
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# use mmap if possible
|
|
88
|
+
if can_mmap:
|
|
89
|
+
with open(file, 'r+b') as f:
|
|
90
|
+
m = mmap.mmap(f.fileno(), 0)
|
|
91
|
+
# partial block reads at slightly increased buffer sizes
|
|
92
|
+
for block in _iter_blocks(m):
|
|
93
|
+
adler = zlib.adler32(block, adler)
|
|
94
|
+
else:
|
|
95
|
+
with open(file, 'rb') as f:
|
|
96
|
+
# partial block reads at slightly increased buffer sizes
|
|
97
|
+
for block in _iter_blocks(f):
|
|
98
|
+
adler = zlib.adler32(block, adler)
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
raise ChecksumCalculationError('adler32', str(file), e)
|
|
102
|
+
|
|
103
|
+
# backflip on 32bit -- can be removed once everything is fully migrated to 64bit
|
|
104
|
+
if adler < 0:
|
|
105
|
+
adler = adler + 2 ** 32
|
|
106
|
+
|
|
107
|
+
return str('%08x' % adler)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def md5(file: "FileDescriptorOrPath") -> str:
|
|
111
|
+
"""
|
|
112
|
+
Runs the MD5 algorithm (RFC-1321) on the binary content of the file named file and returns the hexadecimal digest
|
|
113
|
+
|
|
114
|
+
:param file: file name
|
|
115
|
+
:returns: string of 32 hexadecimal digits
|
|
116
|
+
"""
|
|
117
|
+
hash_md5 = hashlib.md5()
|
|
118
|
+
try:
|
|
119
|
+
with open(file, "rb") as f:
|
|
120
|
+
for block in _iter_blocks(f):
|
|
121
|
+
hash_md5.update(block)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
raise ChecksumCalculationError('md5', str(file), e)
|
|
124
|
+
|
|
125
|
+
return hash_md5.hexdigest()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def sha256(file: "FileDescriptorOrPath") -> str:
|
|
129
|
+
"""
|
|
130
|
+
Runs the SHA256 algorithm on the binary content of the file named file and returns the hexadecimal digest
|
|
131
|
+
|
|
132
|
+
:param file: file name
|
|
133
|
+
:returns: string of 32 hexadecimal digits
|
|
134
|
+
"""
|
|
135
|
+
checksum = hashlib.sha256()
|
|
136
|
+
try:
|
|
137
|
+
with open(file, "rb") as f:
|
|
138
|
+
for block in _iter_blocks(f):
|
|
139
|
+
checksum.update(block)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
raise ChecksumCalculationError('sha256', str(file), e)
|
|
142
|
+
return checksum.hexdigest()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def crc32(file: "FileDescriptorOrPath") -> str:
|
|
146
|
+
"""
|
|
147
|
+
Runs the CRC32 algorithm on the binary content of the file named file and returns the hexadecimal digest
|
|
148
|
+
|
|
149
|
+
:param file: file name
|
|
150
|
+
:returns: string of 32 hexadecimal digits
|
|
151
|
+
"""
|
|
152
|
+
prev = 0
|
|
153
|
+
try:
|
|
154
|
+
with open(file, "rb") as f:
|
|
155
|
+
for block in _iter_blocks(f):
|
|
156
|
+
prev = zlib.crc32(block, prev)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
raise ChecksumCalculationError('crc32', str(file), e)
|
|
159
|
+
return "%X" % (prev & 0xFFFFFFFF)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
CHECKSUM_ALGO_DICT = {
|
|
163
|
+
'adler32': adler32,
|
|
164
|
+
'md5': md5,
|
|
165
|
+
'sha256': sha256,
|
|
166
|
+
'crc32': crc32,
|
|
167
|
+
'merkle_sha256': merkle_sha256
|
|
168
|
+
}
|
rucio/common/client.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Copyright European Organization for Nuclear Research (CERN) since 2012
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import socket
|
|
17
|
+
from configparser import NoOptionError, NoSectionError
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from rucio.common.config import config_get, config_has_section
|
|
21
|
+
from rucio.common.exception import ConfigNotFound
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from rucio.common.types import IPWithLocationDict
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_client() -> bool:
|
|
28
|
+
""""
|
|
29
|
+
Checks if the function is called from a client or from a server/daemon
|
|
30
|
+
|
|
31
|
+
:returns client_mode: True if is called from a client, False if it is called from a server/daemon
|
|
32
|
+
"""
|
|
33
|
+
if 'RUCIO_CLIENT_MODE' not in os.environ:
|
|
34
|
+
try:
|
|
35
|
+
if config_has_section('database'):
|
|
36
|
+
client_mode = False
|
|
37
|
+
elif config_has_section('client'):
|
|
38
|
+
client_mode = True
|
|
39
|
+
else:
|
|
40
|
+
client_mode = False
|
|
41
|
+
except (RuntimeError, ConfigNotFound):
|
|
42
|
+
# If no configuration file is found the default value should be True
|
|
43
|
+
client_mode = True
|
|
44
|
+
else:
|
|
45
|
+
if os.environ['RUCIO_CLIENT_MODE']:
|
|
46
|
+
client_mode = True
|
|
47
|
+
else:
|
|
48
|
+
client_mode = False
|
|
49
|
+
|
|
50
|
+
return client_mode
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_client_vo() -> str:
|
|
54
|
+
"""
|
|
55
|
+
Get the client VO from the environment or the configuration file.
|
|
56
|
+
|
|
57
|
+
:returns vo: The client VO as a string; default = 'def'.
|
|
58
|
+
"""
|
|
59
|
+
if 'RUCIO_VO' in os.environ:
|
|
60
|
+
vo = os.environ['RUCIO_VO']
|
|
61
|
+
else:
|
|
62
|
+
try:
|
|
63
|
+
vo = str(config_get('client', 'vo'))
|
|
64
|
+
except (NoOptionError, NoSectionError):
|
|
65
|
+
vo = 'def'
|
|
66
|
+
return vo
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def detect_client_location() -> "IPWithLocationDict":
|
|
70
|
+
"""
|
|
71
|
+
Normally client IP will be set on the server side (request.remote_addr)
|
|
72
|
+
Here setting ip on the one seen by the host itself. There is no connection
|
|
73
|
+
to Google DNS servers.
|
|
74
|
+
Try to determine the sitename automatically from common environment variables,
|
|
75
|
+
in this order: SITE_NAME, ATLAS_SITE_NAME, OSG_SITE_NAME. If none of these exist
|
|
76
|
+
use the fixed string 'ROAMING'.
|
|
77
|
+
|
|
78
|
+
If environment variables sets location, it uses it.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
ip = None
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as s:
|
|
85
|
+
s.connect(("2001:4860:4860:0:0:0:0:8888", 80))
|
|
86
|
+
ip = s.getsockname()[0]
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
if not ip:
|
|
91
|
+
try:
|
|
92
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
|
93
|
+
s.connect(("8.8.8.8", 80))
|
|
94
|
+
ip = s.getsockname()[0]
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
if not ip:
|
|
99
|
+
ip = '0.0.0.0' # noqa: S104
|
|
100
|
+
|
|
101
|
+
site = os.environ.get('SITE_NAME',
|
|
102
|
+
os.environ.get('ATLAS_SITE_NAME',
|
|
103
|
+
os.environ.get('OSG_SITE_NAME',
|
|
104
|
+
'ROAMING')))
|
|
105
|
+
|
|
106
|
+
latitude = os.environ.get('RUCIO_LATITUDE')
|
|
107
|
+
longitude = os.environ.get('RUCIO_LONGITUDE')
|
|
108
|
+
if latitude and longitude:
|
|
109
|
+
try:
|
|
110
|
+
latitude = float(latitude)
|
|
111
|
+
longitude = float(longitude)
|
|
112
|
+
except ValueError:
|
|
113
|
+
latitude = longitude = 0
|
|
114
|
+
print('Client set latitude and longitude are not valid.')
|
|
115
|
+
else:
|
|
116
|
+
latitude = longitude = None
|
|
117
|
+
|
|
118
|
+
return {'ip': ip,
|
|
119
|
+
'fqdn': socket.getfqdn(),
|
|
120
|
+
'site': site,
|
|
121
|
+
'latitude': latitude,
|
|
122
|
+
'longitude': longitude}
|