rucio-clients 32.8.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rucio-clients might be problematic. Click here for more details.

Files changed (88) hide show
  1. rucio/__init__.py +18 -0
  2. rucio/alembicrevision.py +16 -0
  3. rucio/client/__init__.py +16 -0
  4. rucio/client/accountclient.py +413 -0
  5. rucio/client/accountlimitclient.py +155 -0
  6. rucio/client/baseclient.py +929 -0
  7. rucio/client/client.py +77 -0
  8. rucio/client/configclient.py +113 -0
  9. rucio/client/credentialclient.py +54 -0
  10. rucio/client/didclient.py +691 -0
  11. rucio/client/diracclient.py +48 -0
  12. rucio/client/downloadclient.py +1674 -0
  13. rucio/client/exportclient.py +44 -0
  14. rucio/client/fileclient.py +51 -0
  15. rucio/client/importclient.py +42 -0
  16. rucio/client/lifetimeclient.py +74 -0
  17. rucio/client/lockclient.py +99 -0
  18. rucio/client/metaclient.py +137 -0
  19. rucio/client/pingclient.py +45 -0
  20. rucio/client/replicaclient.py +444 -0
  21. rucio/client/requestclient.py +109 -0
  22. rucio/client/rseclient.py +664 -0
  23. rucio/client/ruleclient.py +287 -0
  24. rucio/client/scopeclient.py +88 -0
  25. rucio/client/subscriptionclient.py +161 -0
  26. rucio/client/touchclient.py +78 -0
  27. rucio/client/uploadclient.py +871 -0
  28. rucio/common/__init__.py +14 -0
  29. rucio/common/cache.py +74 -0
  30. rucio/common/config.py +796 -0
  31. rucio/common/constants.py +92 -0
  32. rucio/common/constraints.py +18 -0
  33. rucio/common/didtype.py +187 -0
  34. rucio/common/exception.py +1092 -0
  35. rucio/common/extra.py +37 -0
  36. rucio/common/logging.py +404 -0
  37. rucio/common/pcache.py +1387 -0
  38. rucio/common/policy.py +84 -0
  39. rucio/common/schema/__init__.py +143 -0
  40. rucio/common/schema/atlas.py +411 -0
  41. rucio/common/schema/belleii.py +406 -0
  42. rucio/common/schema/cms.py +478 -0
  43. rucio/common/schema/domatpc.py +399 -0
  44. rucio/common/schema/escape.py +424 -0
  45. rucio/common/schema/generic.py +431 -0
  46. rucio/common/schema/generic_multi_vo.py +410 -0
  47. rucio/common/schema/icecube.py +404 -0
  48. rucio/common/schema/lsst.py +423 -0
  49. rucio/common/stomp_utils.py +160 -0
  50. rucio/common/stopwatch.py +56 -0
  51. rucio/common/test_rucio_server.py +148 -0
  52. rucio/common/types.py +158 -0
  53. rucio/common/utils.py +1946 -0
  54. rucio/rse/__init__.py +97 -0
  55. rucio/rse/protocols/__init__.py +14 -0
  56. rucio/rse/protocols/cache.py +123 -0
  57. rucio/rse/protocols/dummy.py +112 -0
  58. rucio/rse/protocols/gfal.py +701 -0
  59. rucio/rse/protocols/globus.py +243 -0
  60. rucio/rse/protocols/gsiftp.py +93 -0
  61. rucio/rse/protocols/http_cache.py +83 -0
  62. rucio/rse/protocols/mock.py +124 -0
  63. rucio/rse/protocols/ngarc.py +210 -0
  64. rucio/rse/protocols/posix.py +251 -0
  65. rucio/rse/protocols/protocol.py +530 -0
  66. rucio/rse/protocols/rclone.py +365 -0
  67. rucio/rse/protocols/rfio.py +137 -0
  68. rucio/rse/protocols/srm.py +339 -0
  69. rucio/rse/protocols/ssh.py +414 -0
  70. rucio/rse/protocols/storm.py +207 -0
  71. rucio/rse/protocols/webdav.py +547 -0
  72. rucio/rse/protocols/xrootd.py +295 -0
  73. rucio/rse/rsemanager.py +752 -0
  74. rucio/vcsversion.py +11 -0
  75. rucio/version.py +46 -0
  76. rucio_clients-32.8.6.data/data/etc/rse-accounts.cfg.template +25 -0
  77. rucio_clients-32.8.6.data/data/etc/rucio.cfg.atlas.client.template +42 -0
  78. rucio_clients-32.8.6.data/data/etc/rucio.cfg.template +257 -0
  79. rucio_clients-32.8.6.data/data/requirements.txt +55 -0
  80. rucio_clients-32.8.6.data/data/rucio_client/merge_rucio_configs.py +147 -0
  81. rucio_clients-32.8.6.data/scripts/rucio +2540 -0
  82. rucio_clients-32.8.6.data/scripts/rucio-admin +2434 -0
  83. rucio_clients-32.8.6.dist-info/METADATA +50 -0
  84. rucio_clients-32.8.6.dist-info/RECORD +88 -0
  85. rucio_clients-32.8.6.dist-info/WHEEL +5 -0
  86. rucio_clients-32.8.6.dist-info/licenses/AUTHORS.rst +94 -0
  87. rucio_clients-32.8.6.dist-info/licenses/LICENSE +201 -0
  88. rucio_clients-32.8.6.dist-info/top_level.txt +1 -0
rucio/common/extra.py ADDED
@@ -0,0 +1,37 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright European Organization for Nuclear Research (CERN) since 2012
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import importlib
17
+ import warnings
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ from typing import Any
22
+
23
+
24
+ def import_extras(module_list: list[str]) -> "dict[str, Any]":
25
+ out = dict()
26
+ for mod in module_list:
27
+ out[mod] = None
28
+ try:
29
+ with warnings.catch_warnings():
30
+ # TODO: remove when https://github.com/paramiko/paramiko/issues/2038 is fixed
31
+ warnings.filterwarnings('ignore', 'Blowfish has been deprecated', module='paramiko')
32
+ # TODO: deprecated python 2 and 3.6 too ...
33
+ warnings.filterwarnings('ignore', 'Python .* is no longer supported', module='paramiko')
34
+ out[mod] = importlib.import_module(mod)
35
+ except ImportError:
36
+ pass
37
+ return out
@@ -0,0 +1,404 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright European Organization for Nuclear Research (CERN) since 2012
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ import datetime
16
+ import functools
17
+ import itertools
18
+ import json
19
+ import logging
20
+ import re
21
+ import sys
22
+ from collections.abc import Callable, Iterator, Mapping, Sequence
23
+ from traceback import format_tb
24
+ from typing import TYPE_CHECKING, Any, Optional
25
+
26
+ from rucio.common.config import config_get, config_get_bool
27
+
28
+ if TYPE_CHECKING:
29
+ from logging import LogRecord
30
+
31
+
32
+ # Mapping from ECS field paths
33
+ # https://www.elastic.co/guide/en/ecs-logging/overview/current/intro.html#_field_mapping
34
+ # https://www.elastic.co/guide/en/ecs/8.5/ecs-field-reference.html
35
+ # to python log record attributes:
36
+ # https://docs.python.org/3/library/logging.html#logrecord-attributes
37
+ BUILTIN_FIELDS = (
38
+ ('@timestamp', 'asctime'),
39
+ ('message', 'message'),
40
+ ('log.level', 'levelname'),
41
+ ('log.origin.function', 'funcName'),
42
+ ('log.origin.file.line', 'lineno'),
43
+ ('log.origin.file.name', 'filename'),
44
+ ('log.logger', 'name'),
45
+ ('process.pid', 'process'),
46
+ ('process.name', 'processName'),
47
+ ('process.thread.id', 'thread'),
48
+ ('process.thread.name', 'threadName'),
49
+ )
50
+ ECS_TO_LOG_RECORD_MAP = dict(BUILTIN_FIELDS)
51
+ LOG_RECORD_TO_ECS_MAP = dict((f[1], f[0]) for f in BUILTIN_FIELDS)
52
+
53
+
54
+ def _json_serializable(obj: Any):
55
+ try:
56
+ return obj.__dict__
57
+ except AttributeError:
58
+ return str(obj)
59
+
60
+
61
+ def _navigate_path(obj: Any, path: Sequence[str]) -> Optional[Any]:
62
+ """
63
+ Traverse the path in the given object either via attributes or via dict-like subscriptions.
64
+ Returns the found value; None if navigation fails
65
+
66
+ For example, for an input
67
+ obj = request # flask "request" object https://flask.palletsprojects.com/en/2.1.x/api/#flask.Request
68
+ path = ['headers', 'X-Rucio-Auth-Token']
69
+ returns the value found in
70
+ request.headers['X-Rucio-Auth-Token']
71
+ """
72
+ value = obj
73
+ i = 0
74
+ while value and i < len(path):
75
+ p = path[i]
76
+ try:
77
+ value = getattr(value, p)
78
+ except AttributeError:
79
+ try:
80
+ # Allow integers for access into arrays
81
+ p = int(p)
82
+ except ValueError:
83
+ pass
84
+ try:
85
+ value = value[p]
86
+ except (TypeError, KeyError):
87
+ value = None
88
+ i += 1
89
+ if value is obj:
90
+ return None
91
+ return value
92
+
93
+
94
+ def _unflatten_dict(dictionary: dict[str, Any]) -> dict[str, Any]:
95
+ """
96
+ Transform a dict of the form
97
+ {'a.b.c': value1, 'a.b.d': value2, 'z': value3}
98
+ into
99
+ {'a': {'b': {'c': value1, 'd': value2}}, 'z': value3}
100
+
101
+ On incompatible input keys (for example: 'a.b.c', 'a', 'a.d'), the last key wins
102
+ """
103
+ ret = {}
104
+ for k, v in dictionary.items():
105
+ path = k.split('.')
106
+ d = ret
107
+ i = 0
108
+ while i < len(path) - 1:
109
+ existing_v = d.get(path[i])
110
+ if isinstance(existing_v, dict):
111
+ d = existing_v
112
+ else:
113
+ d[path[i]] = {}
114
+ d = d[path[i]]
115
+ i += 1
116
+ if i == len(path) - 1:
117
+ d[path[i]] = v
118
+ return ret
119
+
120
+
121
+ def _get_request_data(request_path: Sequence[str]) -> "Callable[[LogDataSource, LogRecord], Iterator[tuple[str, Optional[Any]]]]":
122
+ """
123
+ Returns a function which, when called, will resolve the value
124
+ in the flask request object at request_path
125
+ """
126
+
127
+ # The import fails if imported inside a client due to rsemanager.
128
+ # TODO: move to top of file once we got rid of/refactored rsemanager
129
+ from flask import has_request_context, request
130
+
131
+ def _request_data_formatter(record_formatter: "LogDataSource", record: "LogRecord") -> Iterator[tuple[str, Optional[Any]]]:
132
+ value = None
133
+ if has_request_context() and request_path:
134
+ value = _navigate_path(request, request_path)
135
+ yield record_formatter.ecs_fields[0], str(value) if value is not None else None
136
+
137
+ return _request_data_formatter
138
+
139
+
140
+ def _get_record_attribute(attribute: str) -> "Callable[[LogDataSource, LogRecord], Iterator[tuple[str, Optional[Any]]]]":
141
+ """
142
+ Returns a function which, when called, will generate the value of the desired attribute from
143
+ the record passed in argument.
144
+ """
145
+
146
+ def _record_attribute_formatter(record_formatter: "LogDataSource", record: "LogRecord") -> Iterator[tuple[str, Optional[Any]]]:
147
+ value = None
148
+ try:
149
+ value = getattr(record, attribute)
150
+ except AttributeError:
151
+ pass
152
+ yield record_formatter.ecs_fields[0], value
153
+
154
+ return _record_attribute_formatter
155
+
156
+
157
+ def _timestamp_formatter(record_formatter: "LogDataSource", record: "LogRecord") -> Iterator[tuple[str, Optional[Any]]]:
158
+ """
159
+ Format a timestamp
160
+ """
161
+ yield record_formatter.ecs_fields[0], datetime.datetime.utcfromtimestamp(record.created).isoformat(timespec='milliseconds') + 'Z'
162
+
163
+
164
+ def _ecs_field_to_record_attribute(field_name):
165
+ """
166
+ Sanitize the path-like field name into a symbol which can be the name of an object attribute.
167
+ """
168
+ record = ECS_TO_LOG_RECORD_MAP.get(field_name)
169
+ if record:
170
+ return record
171
+ return field_name.replace('-', '_').replace('.', '_')
172
+
173
+
174
+ class LogDataSource:
175
+ """
176
+ Represents one log data source and allows to format it into one or more json fields
177
+ """
178
+ def __init__(
179
+ self,
180
+ ecs_fields: tuple[str, ...],
181
+ formatter: "Optional[Callable[[LogDataSource, LogRecord], Iterator[tuple[str, Optional[Any]]]]]" = None,
182
+ dst_record_attr: Optional[str] = None
183
+ ):
184
+ self.ecs_fields = ecs_fields
185
+ self._formatter = formatter
186
+ self._dst_record_attr = dst_record_attr
187
+
188
+ def __hash__(self):
189
+ return hash(self.ecs_fields)
190
+
191
+ def __eq__(self, other: Any):
192
+ if not other or not isinstance(other, self.__class__):
193
+ return False
194
+ return self.ecs_fields == other.ecs_fields
195
+
196
+ def __str__(self):
197
+ return self.__class__.__name__ + '(' + ', '.join(self.ecs_fields) + ')'
198
+
199
+ def format(self, record: "LogRecord"):
200
+ if not self._formatter:
201
+ return
202
+ for field_name, field_value in self._formatter(self, record):
203
+ if self._dst_record_attr:
204
+ setattr(record, self._dst_record_attr, field_value)
205
+ yield field_name, field_value
206
+
207
+
208
+ class MessageLogDataSource(LogDataSource):
209
+ def __init__(self):
210
+ super().__init__(
211
+ ecs_fields=('message', 'error.type', 'error.message', 'error.stack_trace'),
212
+ formatter=None,
213
+ )
214
+
215
+ @staticmethod
216
+ def _get_exc_info(record):
217
+ exc_info = record.exc_info
218
+ if not exc_info:
219
+ return None
220
+ if isinstance(exc_info, bool):
221
+ exc_info = sys.exc_info()
222
+ if isinstance(exc_info, (list, tuple)):
223
+ return exc_info
224
+ return None
225
+
226
+ def format(self, record: "LogRecord"):
227
+ exc_info = self._get_exc_info(record)
228
+ message = record.getMessage()
229
+ error_type, error_message, stack_trace = None, None, None
230
+ if exc_info:
231
+ error_type = exc_info[0].__name__ if exc_info[0] else None
232
+ error_message = str(exc_info[1]) if exc_info[1] else None
233
+ stack_trace = "".join(format_tb(record.exc_info[2])) or None if exc_info[2] else None
234
+ if not stack_trace:
235
+ stack_trace = str(getattr(record, "stack_info", '')) or None
236
+
237
+ # Set the message into the record field
238
+ s = message
239
+ if error_message:
240
+ if s[-1:] != "\n":
241
+ s = s + "\n"
242
+ s = s + error_message
243
+ if stack_trace:
244
+ if s[-1:] != "\n":
245
+ s = s + "\n"
246
+ s = s + stack_trace
247
+ record.message = s
248
+
249
+ yield from zip(self.ecs_fields, (record.message, error_type, error_message, stack_trace))
250
+
251
+
252
+ class ConstantStrDataSource(LogDataSource):
253
+ """
254
+ Prints a constant string for the given ECS field.
255
+ """
256
+
257
+ def __init__(self, ecs_field, _str):
258
+ log_record = ECS_TO_LOG_RECORD_MAP.get(ecs_field, None)
259
+ self._str = _str
260
+
261
+ def _formatter(data_source: LogDataSource, record: "LogRecord"):
262
+ yield self.ecs_fields[0], self._str
263
+
264
+ super().__init__(ecs_fields=(ecs_field,), formatter=_formatter, dst_record_attr=log_record)
265
+
266
+
267
+ class RucioFormatter(logging.Formatter):
268
+ """
269
+ The formatter should be a drop-in replacement to the python builtin
270
+ formatter, with two additional additions:
271
+ - it can output directly to json
272
+ - it can include data from the flask 'request' object into the format
273
+
274
+ When the logger writes to a json format, it tries to respect the
275
+ Elastic Common Schema (ECS) specification, but without getting too
276
+ strict about it.
277
+
278
+ When the format string contains a dot-separated "path" starting with
279
+ `http.request.`, the rucio formatter will try to extract the given
280
+ path from the flask `request` object.
281
+ """
282
+
283
+ def __init__(
284
+ self,
285
+ fmt: Optional[str] = None,
286
+ validate: Optional[bool] = None,
287
+ output_json: bool = False,
288
+ additional_fields: Optional[Mapping[str, str]] = None
289
+ ):
290
+ _kwargs = {}
291
+ if validate is not None:
292
+ _kwargs["validate"] = validate
293
+
294
+ data_sources: dict[str, LogDataSource] = dict(
295
+ (ecs_field, LogDataSource((ecs_field,), formatter=_get_record_attribute(log_record)))
296
+ for ecs_field, log_record in BUILTIN_FIELDS
297
+ )
298
+ data_sources.update({
299
+ '@timestamp': LogDataSource(('@timestamp',), formatter=_timestamp_formatter),
300
+ 'message': MessageLogDataSource(), # ('message', 'error.type', 'error.message', 'error.stack_trace'),
301
+ })
302
+ data_sources.update(
303
+ (ecs_field, LogDataSource((ecs_field,),
304
+ dst_record_attr=_ecs_field_to_record_attribute(ecs_field),
305
+ formatter=_get_request_data(request_path=request_path.split('.'))))
306
+ for ecs_field, request_path in (
307
+ ('client.account.name', 'headers.X-Rucio-Account'), # this field is rucio-specific, not from the ECS specification
308
+ ('network.forwarded_ip', 'access_route.0'),
309
+ ('source.ip', 'remote_addr'),
310
+ ('url.full', 'url'),
311
+ ('user_agent.original', 'user_agent'),
312
+ )
313
+ )
314
+ if additional_fields:
315
+ data_sources.update({
316
+ ecs_field: ConstantStrDataSource(ecs_field, field_value)
317
+ for ecs_field, field_value in additional_fields.items()
318
+ })
319
+
320
+ self._desired_data_sources = []
321
+ if fmt:
322
+ # extract of field1, field2 from the printf format-string "%(field1)s %(field2)i"
323
+ # Allow simple path-like structures in fields (words separated with dots):
324
+ # - http.request.headers.X-Rucio-Auth-Token
325
+ # - http.request.url
326
+ _format_string_fields = set(t[0] for t in re.findall(r'%\((\w+(.\w+(-\w+)*)*)\)', fmt))
327
+
328
+ for field_name in _format_string_fields:
329
+ data_source = data_sources.get(LOG_RECORD_TO_ECS_MAP.get(field_name, field_name), None)
330
+
331
+ if '.' in field_name:
332
+ dst_record_attr = _ecs_field_to_record_attribute(field_name)
333
+ fmt = fmt.replace(f'%({field_name})', f'%({dst_record_attr})')
334
+ if field_name.startswith('http.request.'):
335
+ path = field_name.replace('http.request.', '', 1).split('.')
336
+ data_source = LogDataSource((field_name,), dst_record_attr=dst_record_attr, formatter=_get_request_data(path))
337
+ elif not data_source:
338
+ data_source = LogDataSource((field_name,), formatter=_get_record_attribute(field_name))
339
+
340
+ if data_source:
341
+ self._desired_data_sources.append(data_source)
342
+ else:
343
+ self._desired_data_sources = [data_sources['message']]
344
+
345
+ self.output_json = output_json
346
+ super().__init__(fmt=fmt, style='%', **_kwargs)
347
+
348
+ def format(self, record):
349
+ json_record = dict(itertools.chain.from_iterable(f.format(record) for f in self._desired_data_sources))
350
+ if self.output_json:
351
+ return self._to_json(_unflatten_dict(json_record))
352
+ else:
353
+ return super().format(record)
354
+
355
+ @staticmethod
356
+ def _to_json(record):
357
+ try:
358
+ return json.dumps(record, default=_json_serializable)
359
+ except (TypeError, ValueError, OverflowError):
360
+ try:
361
+ return json.dumps(record)
362
+ except (TypeError, ValueError, OverflowError):
363
+ return '{}'
364
+
365
+
366
+ def rucio_log_formatter(process_name: Optional[str] = None):
367
+ config_logformat = config_get('common', 'logformat', raise_exception=False, default='%(asctime)s\t%(name)s\t%(process)d\t%(levelname)s\t%(message)s')
368
+ output_json = config_get_bool('common', 'logjson', default=False)
369
+ additional_fields = {}
370
+ if process_name:
371
+ additional_fields['process.name'] = process_name
372
+ return RucioFormatter(fmt=config_logformat, output_json=output_json, additional_fields=additional_fields)
373
+
374
+
375
+ def setup_logging(application=None, process_name=None):
376
+ """
377
+ Configures the logging by setting the output stream to stdout and
378
+ configures log level and log format.
379
+ """
380
+ config_loglevel = getattr(logging, config_get('common', 'loglevel', raise_exception=False, default='DEBUG').upper())
381
+
382
+ stdouthandler = logging.StreamHandler(stream=sys.stdout)
383
+ stdouthandler.setFormatter(rucio_log_formatter(process_name=process_name))
384
+ stdouthandler.setLevel(config_loglevel)
385
+ logging.basicConfig(level=config_loglevel, handlers=[stdouthandler])
386
+
387
+ if application:
388
+ application.logger.addHandler(stdouthandler)
389
+
390
+
391
+ def formatted_logger(innerfunc, formatstr="%s"):
392
+ """
393
+ Decorates the passed function, formatting log input by
394
+ the passed formatstr. The format string must always include a %s.
395
+
396
+ :param innerfunc: function to be decorated. Must take (level, msg) arguments.
397
+ :type innerfunc: Callable
398
+ :param formatstr: format string with %s as placeholder.
399
+ :type formatstr: str
400
+ """
401
+ @functools.wraps(innerfunc)
402
+ def log_format(level, msg, *args, **kwargs):
403
+ return innerfunc(level, formatstr % msg, *args, **kwargs)
404
+ return log_format