rucio-clients 37.0.0rc1__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 (104) hide show
  1. rucio/__init__.py +17 -0
  2. rucio/alembicrevision.py +15 -0
  3. rucio/cli/__init__.py +14 -0
  4. rucio/cli/account.py +216 -0
  5. rucio/cli/bin_legacy/__init__.py +13 -0
  6. rucio/cli/bin_legacy/rucio.py +2825 -0
  7. rucio/cli/bin_legacy/rucio_admin.py +2500 -0
  8. rucio/cli/command.py +272 -0
  9. rucio/cli/config.py +72 -0
  10. rucio/cli/did.py +191 -0
  11. rucio/cli/download.py +128 -0
  12. rucio/cli/lifetime_exception.py +33 -0
  13. rucio/cli/replica.py +162 -0
  14. rucio/cli/rse.py +293 -0
  15. rucio/cli/rule.py +158 -0
  16. rucio/cli/scope.py +40 -0
  17. rucio/cli/subscription.py +73 -0
  18. rucio/cli/upload.py +60 -0
  19. rucio/cli/utils.py +226 -0
  20. rucio/client/__init__.py +15 -0
  21. rucio/client/accountclient.py +432 -0
  22. rucio/client/accountlimitclient.py +183 -0
  23. rucio/client/baseclient.py +983 -0
  24. rucio/client/client.py +120 -0
  25. rucio/client/configclient.py +126 -0
  26. rucio/client/credentialclient.py +59 -0
  27. rucio/client/didclient.py +868 -0
  28. rucio/client/diracclient.py +56 -0
  29. rucio/client/downloadclient.py +1783 -0
  30. rucio/client/exportclient.py +44 -0
  31. rucio/client/fileclient.py +50 -0
  32. rucio/client/importclient.py +42 -0
  33. rucio/client/lifetimeclient.py +90 -0
  34. rucio/client/lockclient.py +109 -0
  35. rucio/client/metaconventionsclient.py +140 -0
  36. rucio/client/pingclient.py +44 -0
  37. rucio/client/replicaclient.py +452 -0
  38. rucio/client/requestclient.py +125 -0
  39. rucio/client/richclient.py +317 -0
  40. rucio/client/rseclient.py +746 -0
  41. rucio/client/ruleclient.py +294 -0
  42. rucio/client/scopeclient.py +90 -0
  43. rucio/client/subscriptionclient.py +173 -0
  44. rucio/client/touchclient.py +82 -0
  45. rucio/client/uploadclient.py +969 -0
  46. rucio/common/__init__.py +13 -0
  47. rucio/common/bittorrent.py +234 -0
  48. rucio/common/cache.py +111 -0
  49. rucio/common/checksum.py +168 -0
  50. rucio/common/client.py +122 -0
  51. rucio/common/config.py +788 -0
  52. rucio/common/constants.py +217 -0
  53. rucio/common/constraints.py +17 -0
  54. rucio/common/didtype.py +237 -0
  55. rucio/common/exception.py +1208 -0
  56. rucio/common/extra.py +31 -0
  57. rucio/common/logging.py +420 -0
  58. rucio/common/pcache.py +1409 -0
  59. rucio/common/plugins.py +185 -0
  60. rucio/common/policy.py +93 -0
  61. rucio/common/schema/__init__.py +200 -0
  62. rucio/common/schema/generic.py +416 -0
  63. rucio/common/schema/generic_multi_vo.py +395 -0
  64. rucio/common/stomp_utils.py +423 -0
  65. rucio/common/stopwatch.py +55 -0
  66. rucio/common/test_rucio_server.py +154 -0
  67. rucio/common/types.py +483 -0
  68. rucio/common/utils.py +1688 -0
  69. rucio/rse/__init__.py +96 -0
  70. rucio/rse/protocols/__init__.py +13 -0
  71. rucio/rse/protocols/bittorrent.py +194 -0
  72. rucio/rse/protocols/cache.py +111 -0
  73. rucio/rse/protocols/dummy.py +100 -0
  74. rucio/rse/protocols/gfal.py +708 -0
  75. rucio/rse/protocols/globus.py +243 -0
  76. rucio/rse/protocols/http_cache.py +82 -0
  77. rucio/rse/protocols/mock.py +123 -0
  78. rucio/rse/protocols/ngarc.py +209 -0
  79. rucio/rse/protocols/posix.py +250 -0
  80. rucio/rse/protocols/protocol.py +361 -0
  81. rucio/rse/protocols/rclone.py +365 -0
  82. rucio/rse/protocols/rfio.py +145 -0
  83. rucio/rse/protocols/srm.py +338 -0
  84. rucio/rse/protocols/ssh.py +414 -0
  85. rucio/rse/protocols/storm.py +195 -0
  86. rucio/rse/protocols/webdav.py +594 -0
  87. rucio/rse/protocols/xrootd.py +302 -0
  88. rucio/rse/rsemanager.py +881 -0
  89. rucio/rse/translation.py +260 -0
  90. rucio/vcsversion.py +11 -0
  91. rucio/version.py +45 -0
  92. rucio_clients-37.0.0rc1.data/data/etc/rse-accounts.cfg.template +25 -0
  93. rucio_clients-37.0.0rc1.data/data/etc/rucio.cfg.atlas.client.template +43 -0
  94. rucio_clients-37.0.0rc1.data/data/etc/rucio.cfg.template +241 -0
  95. rucio_clients-37.0.0rc1.data/data/requirements.client.txt +19 -0
  96. rucio_clients-37.0.0rc1.data/data/rucio_client/merge_rucio_configs.py +144 -0
  97. rucio_clients-37.0.0rc1.data/scripts/rucio +133 -0
  98. rucio_clients-37.0.0rc1.data/scripts/rucio-admin +97 -0
  99. rucio_clients-37.0.0rc1.dist-info/METADATA +54 -0
  100. rucio_clients-37.0.0rc1.dist-info/RECORD +104 -0
  101. rucio_clients-37.0.0rc1.dist-info/WHEEL +5 -0
  102. rucio_clients-37.0.0rc1.dist-info/licenses/AUTHORS.rst +100 -0
  103. rucio_clients-37.0.0rc1.dist-info/licenses/LICENSE +201 -0
  104. rucio_clients-37.0.0rc1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,423 @@
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
+ """
16
+ Common utility functions for stomp connections
17
+ """
18
+ import json
19
+ import logging
20
+ import random
21
+ import socket
22
+ from collections import namedtuple
23
+ from copy import deepcopy
24
+ from functools import partial
25
+ from time import monotonic
26
+ from typing import TYPE_CHECKING, Any
27
+
28
+ from stomp import Connection12
29
+ from stomp.exception import ConnectFailedException, NotConnectedException
30
+ from stomp.listener import HeartbeatListener
31
+
32
+ from rucio.common.config import config_get, config_get_bool, config_get_float, config_get_int, config_get_list
33
+ from rucio.common.logging import formatted_logger
34
+ from rucio.core.monitor import MetricManager
35
+
36
+ if TYPE_CHECKING:
37
+ from collections.abc import Iterable, Iterator
38
+
39
+ from stomp.connect import Frame
40
+
41
+ from rucio.common.types import LoggerFunction
42
+
43
+
44
+ METRICS = MetricManager(module=__name__)
45
+
46
+
47
+ class Connection(Connection12):
48
+ """
49
+ Connection class.
50
+
51
+ Wraps Stomp Connection but knows the brokers without accessing
52
+ hidden variables from the Transport.
53
+ """
54
+ def __init__(self, host_and_ports: list[tuple[str, int]], **kwargs):
55
+ """
56
+ Initialise.
57
+
58
+ Args:
59
+ host_and_ports: brokers list
60
+
61
+ Kwargs:
62
+ Arguments to pass to the Constructor12 base class.
63
+ """
64
+ super().__init__(host_and_ports=host_and_ports, **kwargs)
65
+ self._brokers = host_and_ports
66
+
67
+ @property
68
+ def brokers(self) -> list[tuple[str, int]]:
69
+ """
70
+ List brokers.
71
+
72
+ Returns:
73
+ All assigned brokers in (host, port) format.
74
+ """
75
+ return self._brokers
76
+
77
+
78
+ class ListenerBase(HeartbeatListener):
79
+ """Listener Base."""
80
+
81
+ _logger = formatted_logger(logging.log, 'ListenerBase %s')
82
+
83
+ def __init__(self,
84
+ conn: Connection,
85
+ logger: "None | LoggerFunction" = None,
86
+ **kwargs):
87
+ """
88
+ Initialise.
89
+
90
+ Args:
91
+ conn: The connection object that is using this listener
92
+ logger: Logger to use. Defaults to logging.getLogger(__name__).getChild(__qualname__).
93
+
94
+ Kwargs:
95
+ Arguments to pass to the stomp.ConnectionListener base class.
96
+ """
97
+ super().__init__(transport=conn.transport, **kwargs)
98
+ self._conn = conn
99
+ if logger is not None:
100
+ self._logger = logger
101
+
102
+ @METRICS.count_it
103
+ def on_heartbeat_timeout(self):
104
+ self._conn.disconnect()
105
+
106
+ @METRICS.count_it
107
+ def on_error(self, frame: "Frame"):
108
+ """
109
+ on_error
110
+ """
111
+ self._logger(logging.ERROR, 'Message receive error: [%s] %s', self._conn.brokers[0][0], frame.body)
112
+
113
+
114
+ StompConfig = namedtuple("StompConfig", ('brokers', 'use_ssl', 'port', 'vhost',
115
+ 'destination', 'key_file', 'cert_file',
116
+ 'username', 'password', 'nonssl_port',
117
+ 'reconnect_attempts_max', 'timeout', 'heartbeats'))
118
+
119
+
120
+ class StompConnectionManager:
121
+ """Stomp Connection Manager."""
122
+
123
+ _logger = formatted_logger(logging.log, 'StompConnectionManager %s')
124
+
125
+ def __init__(self,
126
+ config_section: str,
127
+ logger: "None | LoggerFunction" = None):
128
+ """
129
+ Initialise.
130
+
131
+ Args:
132
+ config_section: The name of the config section for this manager to parse for configuration.
133
+ logger: logger to use. Defaults to logging.getLogger(__name__).getChild(__qualname__).
134
+ """
135
+ if logger is not None:
136
+ self._logger = logger
137
+ self._config = self._parse_config(config_section)
138
+ self._listener_factory = None
139
+ self._conns = []
140
+ for broker in self._config.brokers:
141
+ conn = Connection(host_and_ports=[broker],
142
+ vhost=self._config.vhost,
143
+ reconnect_attempts_max=self._config.reconnect_attempts_max,
144
+ timeout=self._config.timeout,
145
+ heartbeats=self._config.heartbeats)
146
+ if self._config.use_ssl:
147
+ conn.set_ssl(cert_file=self._config.cert_file, key_file=self._config.key_file)
148
+ self._conns.append(conn)
149
+
150
+ @property
151
+ def config(self) -> StompConfig:
152
+ """
153
+ Get the config.
154
+
155
+ Returns:
156
+ config object.
157
+ """
158
+ return deepcopy(self._config)
159
+
160
+ def set_listener_factory(self, name: str, listener_cls: type, **kwargs) -> None:
161
+ """
162
+ Setup listener factory
163
+
164
+ This method will setup a factory to create a name and listener for the arguments to
165
+ connection.set_listener based on pre-defined argument values.
166
+
167
+ Args:
168
+ name: Listener name
169
+ listener_cls: Listener class.
170
+ """
171
+ def create_listener(name, listener_factory, conn):
172
+ return name, listener_factory(conn=conn)
173
+ self._listener_factory = partial(create_listener,
174
+ name=name,
175
+ listener_factory=partial(listener_cls, logger=self._logger, **kwargs))
176
+
177
+ def _parse_config(self, config_section: str) -> StompConfig:
178
+ """
179
+ Parse config section.
180
+
181
+ Args:
182
+ config_section: The name of the config section for this manager to parse for configuration.
183
+
184
+ Raises:
185
+ RuntimeError: If cannot parse config sections 'brokers' or 'use_ssl' or if misconfigured.
186
+
187
+ Returns:
188
+ Stomp manager configuration object.
189
+ """
190
+ try:
191
+ brokers = config_get(config_section, 'brokers')
192
+ except Exception as exc:
193
+ self._logger(logging.ERROR, "Could not load brokers from configuration")
194
+ raise RuntimeError('Could not load brokers from configuration') from exc
195
+
196
+ try:
197
+ use_ssl = config_get_bool(config_section, 'use_ssl')
198
+ except Exception as exc:
199
+ self._logger(logging.ERROR, "could not find use_ssl in configuration -- please update your rucio.cfg")
200
+ raise RuntimeError('could not find use_ssl in configuration -- please update your rucio.cfg') from exc
201
+
202
+ port = config_get_int(config_section, 'port')
203
+ vhost = config_get(config_section, 'broker_virtual_host', raise_exception=False)
204
+ destination = config_get(config_section, "destination")
205
+ key_file = config_get(config_section, 'ssl_key_file', default=None, raise_exception=False)
206
+ cert_file = config_get(config_section, 'ssl_cert_file', default=None, raise_exception=False)
207
+ username = config_get(config_section, 'username', default=None, raise_exception=False)
208
+ password = config_get(config_section, 'password', default=None, raise_exception=False)
209
+ nonssl_port = config_get_int(config_section, 'nonssl_port', default=0, raise_exception=False)
210
+ timeout = config_get_float(config_section, 'timeout', default=None, raise_exception=False)
211
+ heartbeats = tuple(config_get_list(config_section, 'heartbeats', default=[0., 0.], raise_exception=False))
212
+ reconnect_attempts = config_get_int(config_section, 'reconnect_attempts', default=100)
213
+ if use_ssl and (key_file is None or cert_file is None):
214
+ self._logger(logging.ERROR, "If use_ssl is True in config you must provide both 'ssl_cert_file' "
215
+ "and 'ssl_key_file'")
216
+ raise RuntimeError("If use_ssl is True in config you must provide both 'ssl_cert_file' and 'ssl_key_file'")
217
+ if not use_ssl and (username is None or password is None or nonssl_port == 0):
218
+ self._logger(logging.ERROR, "If use_ssl is False in config you must provide "
219
+ "'username', 'password' and 'nonssl_port'")
220
+ raise RuntimeError("If use_ssl is False in config you must provide "
221
+ "'username', 'password' and 'nonssl_port'")
222
+ return StompConfig(brokers=self._resolve_host_and_port(brokers, port if use_ssl else nonssl_port),
223
+ use_ssl=use_ssl,
224
+ port=port, vhost=vhost,
225
+ destination=destination, key_file=key_file, cert_file=cert_file,
226
+ username=username, password=password, nonssl_port=nonssl_port,
227
+ reconnect_attempts_max=reconnect_attempts, timeout=timeout, heartbeats=heartbeats)
228
+
229
+ def _resolve_host_and_port(self, fqdns: "str | Iterable[str]", port: int) -> list[tuple[str, int]]:
230
+ """
231
+ Resolve host and port.
232
+
233
+ Args:
234
+ fqdns: fully qualified domain name(s)
235
+ port: port
236
+
237
+ Returns:
238
+ list of (host, port) tuples.
239
+ """
240
+ if isinstance(fqdns, str):
241
+ fqdns = fqdns.split(',')
242
+
243
+ hosts_and_ports = []
244
+ for fqdn in fqdns:
245
+ try:
246
+ addrinfos = socket.getaddrinfo(fqdn.strip(), port, socket.AF_INET, 0, socket.IPPROTO_TCP)
247
+ except socket.gaierror as exc:
248
+ self._logger(logging.ERROR, "[broker] Cannot resolve domain name %s (%s)", fqdn.strip(), str(exc))
249
+ continue
250
+
251
+ hosts_and_ports.extend(addrinfo[4] for addrinfo in addrinfos)
252
+ if not hosts_and_ports:
253
+ self._logger(logging.WARNING, "[broker] No resolved brokers")
254
+ return hosts_and_ports
255
+
256
+ def _is_stalled(self, conn: Connection) -> bool:
257
+ """
258
+ Determine if a connection is stalled.
259
+
260
+ Args:
261
+ conn: The Connection object
262
+
263
+ Returns:
264
+ Whether the connection has stalled.
265
+ """
266
+ received_heartbeat = getattr(conn, 'received_heartbeat', None)
267
+ if received_heartbeat is None or not any(self._config.heartbeats):
268
+ return False
269
+
270
+ heartbeat_period_seconds = max(0, self._config.heartbeats[0], self._config.heartbeats[1]) / 1000
271
+ if heartbeat_period_seconds == 0.:
272
+ return False
273
+
274
+ now = monotonic()
275
+ if received_heartbeat + 10 * heartbeat_period_seconds >= now:
276
+ return False
277
+
278
+ return True
279
+
280
+ def connect(self) -> "Iterator[Connection]":
281
+ """
282
+ Connect.
283
+
284
+ Yields:
285
+ Each connection object after ensuring it's connected.
286
+ """
287
+ config = self._config
288
+ params = {'wait': True, "heartbeats": self._config.heartbeats}
289
+ self._logger(logging.WARNING, 'heartbeats: %s', self._config.heartbeats)
290
+ if config.use_ssl:
291
+ params.update(username=config.username, password=config.password)
292
+
293
+ for conn in self._conns:
294
+ if self._is_stalled(conn):
295
+ try:
296
+ conn.disconnect()
297
+ except Exception:
298
+ self._logger(logging.ERROR, "[broker] Stalled connection could not be disconnected")
299
+ if not conn.is_connected():
300
+ self._logger(logging.INFO, 'connecting to %s:%s', *conn.brokers[0])
301
+ METRICS.counter('reconnect.{host}').labels(host=conn.brokers[0][0]).inc()
302
+ if self._listener_factory is not None:
303
+ conn.set_listener(*self._listener_factory(conn=conn))
304
+
305
+ try:
306
+ conn.connect(**params)
307
+ except ConnectFailedException as error:
308
+ self._logger(logging.WARNING, "[broker] Could not deliver message due to "
309
+ "ConnectFailedException: %s", str(error))
310
+ continue
311
+ except Exception as error:
312
+ self._logger(logging.ERROR, "[broker] Could not connect: %s", str(error))
313
+ continue
314
+ try:
315
+ yield conn
316
+ except Exception:
317
+ self._logger(logging.ERROR, "[broker] Error in yielded code, skipping to next connection.")
318
+
319
+ def deliver_messages(self, messages: "Iterable[dict[str, Any]]") -> list[int]:
320
+ """
321
+ Deliver messages.
322
+
323
+ Args:
324
+ messages: Messages to deliver.
325
+
326
+ Returns:
327
+ delivered message ids, ready for deletion.
328
+ """
329
+ config = self._config
330
+ conn = random.sample(list(self.connect()), 1)[0]
331
+ to_delete = []
332
+ for message in messages:
333
+ try:
334
+ body = json.dumps({"event_type": str(message["event_type"]).lower(),
335
+ "payload": message["payload"],
336
+ "created_at": str(message["created_at"])})
337
+ except ValueError:
338
+ self._logger(logging.ERROR, "[broker] Cannot serialize payload to JSON: %s", str(message["payload"]))
339
+ to_delete.append(message["id"])
340
+ continue
341
+
342
+ try:
343
+ conn.send(
344
+ body=body,
345
+ destination=config.destination,
346
+ headers={"persistent": "true",
347
+ "event_type": str(message["event_type"]).lower()}
348
+ )
349
+ to_delete.append(message["id"])
350
+ except NotConnectedException as error:
351
+ self._logger(logging.WARNING, "[broker] Could not deliver message due to NotConnectedException: %s",
352
+ str(error))
353
+ continue
354
+ except Exception as error:
355
+ self._logger(logging.ERROR, "[broker] Could not deliver message: %s", str(error))
356
+ continue
357
+
358
+ msg_event_type = str(message["event_type"]).lower()
359
+ msg_payload = message.get("payload", {})
360
+ if msg_event_type.startswith("transfer") or msg_event_type.startswith("stagein"):
361
+ self._logger(logging.DEBUG,
362
+ "[broker] - event_type: %s, scope: %s, name: %s, rse: %s, request-id: %s, "
363
+ "transfer-id: %s, created_at: %s",
364
+ msg_event_type,
365
+ msg_payload.get("scope", None),
366
+ msg_payload.get("name", None),
367
+ msg_payload.get("dst-rse", None),
368
+ msg_payload.get("request-id", None),
369
+ msg_payload.get("transfer-id", None),
370
+ str(message["created_at"]))
371
+
372
+ elif msg_event_type.startswith("dataset"):
373
+ self._logger(logging.DEBUG,
374
+ "[broker] - event_type: %s, scope: %s, name: %s, rse: %s, rule-id: %s, created_at: %s)",
375
+ msg_event_type,
376
+ msg_payload.get("scope", None),
377
+ msg_payload.get("name", None),
378
+ msg_payload.get("rse", None),
379
+ msg_payload.get("rule_id", None),
380
+ str(message["created_at"]))
381
+
382
+ elif msg_event_type.startswith("deletion"):
383
+ if "url" not in msg_payload:
384
+ msg_payload["url"] = "unknown"
385
+ self._logger(logging.DEBUG,
386
+ "[broker] - event_type: %s, scope: %s, name: %s, rse: %s, url: %s, created_at: %s)",
387
+ msg_event_type,
388
+ msg_payload.get("scope", None),
389
+ msg_payload.get("name", None),
390
+ msg_payload.get("rse", None),
391
+ msg_payload.get("url", None),
392
+ str(message["created_at"]))
393
+ else:
394
+ self._logger(logging.DEBUG, "[broker] Other message: %s", message)
395
+
396
+ return to_delete
397
+
398
+ def subscribe(self, id_: str, ack: str, destination: "None | str" = None, **kwargs) -> None:
399
+ """
400
+ Subscribe
401
+
402
+ Args:
403
+ id_: The identifier to uniquely identify the subscription
404
+ ack: Either auto, client or client-individual
405
+ destination: The topic or queue to subscribe to. If None then
406
+ destination is taken from the rucio config Defaults to None.
407
+
408
+ Kwargs:
409
+ Arguments to pass to the Construction objects subscribe method.
410
+ """
411
+ if destination is None:
412
+ destination = self._config.destination
413
+ for conn in self.connect():
414
+ conn.subscribe(destination=destination,
415
+ id=id_, ack=ack, **kwargs)
416
+
417
+ def disconnect(self) -> None:
418
+ """Disconnect."""
419
+ for conn in self._conns:
420
+ try:
421
+ conn.disconnect()
422
+ except Exception:
423
+ self._logger(logging.ERROR, "[broker] Could not disconnect")
@@ -0,0 +1,55 @@
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
+
16
+ import time
17
+ from typing import Optional
18
+
19
+
20
+ class Stopwatch:
21
+ """Stopwatch to measure time durations.
22
+
23
+ Note: The stopwatch is started on initialization.
24
+ """
25
+
26
+ _t_start: float
27
+ _t_end: Optional[float]
28
+
29
+ def __init__(self) -> None:
30
+ self.restart()
31
+
32
+ def _now(self) -> float:
33
+ # TODO: change to time.monotonic_ns() if python 3.6 support is dropped.
34
+ return time.monotonic()
35
+
36
+ def restart(self) -> None:
37
+ """Resets and starts the stopwatch."""
38
+ self._t_start = self._now()
39
+ self._t_end = None
40
+
41
+ def stop(self) -> None:
42
+ """Stops the stopwatch."""
43
+ self._t_end = self._now()
44
+
45
+ @property
46
+ def elapsed(self) -> float:
47
+ """Returns the total number of elapsed seconds."""
48
+ if self._t_end is None:
49
+ return self._now() - self._t_start
50
+ else:
51
+ return self._t_end - self._t_start
52
+
53
+ def __float__(self) -> float:
54
+ """Returns the total number of elapsed seconds."""
55
+ return self.elapsed
@@ -0,0 +1,154 @@
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 unittest
16
+ from os import remove
17
+ from os.path import basename
18
+
19
+ from rucio.common.utils import execute
20
+ from rucio.common.utils import generate_uuid as uuid
21
+
22
+
23
+ def file_generator(size=2048, namelen=10):
24
+ """ Create a bogus file and returns it's name.
25
+ :param size: size in bytes
26
+ :returns: The name of the generated file.
27
+ """
28
+ fn = '/tmp/rucio_testfile_' + uuid()
29
+ execute('dd if=/dev/urandom of={0} count={1} bs=1'.format(fn, size))
30
+ return fn
31
+
32
+
33
+ def get_scope_and_rses():
34
+ """
35
+ Check if xrd containers rses for xrootd are available in the testing environment.
36
+
37
+ :return: A tuple (scope, rses) for the rucio client where scope is mock/test and rses is a list or (None, [None]) if no suitable rse exists.
38
+ """
39
+ cmd = "rucio rse list 'test_container_xrd=True'"
40
+ print(cmd)
41
+ exitcode, out, err = execute(cmd)
42
+ print(out, err)
43
+ rses = out.split()
44
+ if len(rses) == 0:
45
+ return None, [None]
46
+
47
+ scope = 'test'
48
+ account = 'root'
49
+ cmd = f"rucio scope add {scope} --account {account}"
50
+ _, out, err = execute(cmd)
51
+ print(out, err)
52
+ return scope, rses
53
+
54
+
55
+ def delete_rules(did):
56
+ # get the rules for the file
57
+ print('Deleting rules')
58
+ cmd = "rucio rule list {0} | grep {0} | cut -f1 -d\\ ".format(did)
59
+ print(cmd)
60
+ exitcode, out, err = execute(cmd)
61
+ print(out, err)
62
+ rules = out.split()
63
+ # delete the rules for the file
64
+ for rule in rules:
65
+ cmd = "rucio rule remove {0}".format(rule)
66
+ print(cmd)
67
+ exitcode, out, err = execute(cmd)
68
+
69
+
70
+ class TestRucioServer(unittest.TestCase):
71
+
72
+ def setUp(self):
73
+ self.marker = '$ > '
74
+ self.scope, self.rses = get_scope_and_rses()
75
+ self.rse = self.rses[0]
76
+ self.generated_dids = []
77
+
78
+ def tearDown(self):
79
+ for did in self.generated_dids:
80
+ delete_rules(did)
81
+ self.generated_dids = []
82
+
83
+ def test_ping(self):
84
+ """CLIENT (USER): rucio ping"""
85
+ cmd = 'rucio ping'
86
+ print(self.marker + cmd)
87
+ exitcode, out, err = execute(cmd)
88
+ print(out, err)
89
+ self.assertEqual(exitcode, 0)
90
+
91
+ def test_whoami(self):
92
+ """CLIENT (USER): rucio whoami"""
93
+ cmd = 'rucio whoami'
94
+ print(self.marker + cmd)
95
+ exitcode, out, err = execute(cmd)
96
+ print(out, err)
97
+ self.assertEqual(exitcode, 0)
98
+
99
+ def test_upload_download(self):
100
+ """CLIENT(USER): rucio upload files to dataset/download dataset"""
101
+ if self.rse is None:
102
+ return
103
+
104
+ tmp_file1 = file_generator()
105
+ tmp_file2 = file_generator()
106
+ tmp_file3 = file_generator()
107
+ tmp_dsn = 'tests.rucio_client_test_server_' + uuid()
108
+
109
+ # Adding files to a new dataset
110
+ cmd = 'rucio upload --rse {0} --scope {1} {2} {3} {4} {1}:{5}'.format(self.rse, self.scope, tmp_file1, tmp_file2, tmp_file3, tmp_dsn)
111
+ print(self.marker + cmd)
112
+ exitcode, out, err = execute(cmd)
113
+ print(out)
114
+ print(err)
115
+ remove(tmp_file1)
116
+ remove(tmp_file2)
117
+ remove(tmp_file3)
118
+ self.assertEqual(exitcode, 0)
119
+
120
+ # List the files
121
+ cmd = 'rucio did content list --did {0}:{1}'.format(self.scope, tmp_dsn)
122
+ print(self.marker + cmd)
123
+ exitcode, out, err = execute(cmd)
124
+ print(out)
125
+ print(err)
126
+ self.assertEqual(exitcode, 0)
127
+
128
+ # List the replicas
129
+ cmd = 'rucio replica list file {0}:{1}'.format(self.scope, tmp_dsn)
130
+ print(self.marker + cmd)
131
+ exitcode, out, err = execute(cmd)
132
+ print(out)
133
+ print(err)
134
+ self.assertEqual(exitcode, 0)
135
+
136
+ # Downloading dataset
137
+ cmd = 'rucio download --dir /tmp/ {0}:{1}'.format(self.scope, tmp_dsn)
138
+ print(self.marker + cmd)
139
+ exitcode, out, err = execute(cmd)
140
+ print(out)
141
+ print(err)
142
+ # The files should be there
143
+ cmd = 'ls /tmp/{0}/rucio_testfile_*'.format(tmp_dsn)
144
+ print(self.marker + cmd)
145
+ exitcode, out, err = execute(cmd)
146
+ print(err, out)
147
+ self.assertEqual(exitcode, 0)
148
+
149
+ # cleaning
150
+ remove('/tmp/{0}/'.format(tmp_dsn) + basename(tmp_file1))
151
+ remove('/tmp/{0}/'.format(tmp_dsn) + basename(tmp_file2))
152
+ remove('/tmp/{0}/'.format(tmp_dsn) + basename(tmp_file3))
153
+ added_dids = ['{0}:{1}'.format(self.scope, did) for did in (basename(tmp_file1), basename(tmp_file2), basename(tmp_file3), tmp_dsn)]
154
+ self.generated_dids += added_dids