rucio-clients 35.8.2__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.
Files changed (88) hide show
  1. rucio/__init__.py +17 -0
  2. rucio/alembicrevision.py +15 -0
  3. rucio/client/__init__.py +15 -0
  4. rucio/client/accountclient.py +433 -0
  5. rucio/client/accountlimitclient.py +183 -0
  6. rucio/client/baseclient.py +974 -0
  7. rucio/client/client.py +76 -0
  8. rucio/client/configclient.py +126 -0
  9. rucio/client/credentialclient.py +59 -0
  10. rucio/client/didclient.py +866 -0
  11. rucio/client/diracclient.py +56 -0
  12. rucio/client/downloadclient.py +1785 -0
  13. rucio/client/exportclient.py +44 -0
  14. rucio/client/fileclient.py +50 -0
  15. rucio/client/importclient.py +42 -0
  16. rucio/client/lifetimeclient.py +90 -0
  17. rucio/client/lockclient.py +109 -0
  18. rucio/client/metaconventionsclient.py +140 -0
  19. rucio/client/pingclient.py +44 -0
  20. rucio/client/replicaclient.py +454 -0
  21. rucio/client/requestclient.py +125 -0
  22. rucio/client/rseclient.py +746 -0
  23. rucio/client/ruleclient.py +294 -0
  24. rucio/client/scopeclient.py +90 -0
  25. rucio/client/subscriptionclient.py +173 -0
  26. rucio/client/touchclient.py +82 -0
  27. rucio/client/uploadclient.py +955 -0
  28. rucio/common/__init__.py +13 -0
  29. rucio/common/cache.py +74 -0
  30. rucio/common/config.py +801 -0
  31. rucio/common/constants.py +159 -0
  32. rucio/common/constraints.py +17 -0
  33. rucio/common/didtype.py +189 -0
  34. rucio/common/exception.py +1151 -0
  35. rucio/common/extra.py +36 -0
  36. rucio/common/logging.py +420 -0
  37. rucio/common/pcache.py +1408 -0
  38. rucio/common/plugins.py +153 -0
  39. rucio/common/policy.py +84 -0
  40. rucio/common/schema/__init__.py +150 -0
  41. rucio/common/schema/atlas.py +413 -0
  42. rucio/common/schema/belleii.py +408 -0
  43. rucio/common/schema/domatpc.py +401 -0
  44. rucio/common/schema/escape.py +426 -0
  45. rucio/common/schema/generic.py +433 -0
  46. rucio/common/schema/generic_multi_vo.py +412 -0
  47. rucio/common/schema/icecube.py +406 -0
  48. rucio/common/stomp_utils.py +159 -0
  49. rucio/common/stopwatch.py +55 -0
  50. rucio/common/test_rucio_server.py +148 -0
  51. rucio/common/types.py +403 -0
  52. rucio/common/utils.py +2238 -0
  53. rucio/rse/__init__.py +96 -0
  54. rucio/rse/protocols/__init__.py +13 -0
  55. rucio/rse/protocols/bittorrent.py +184 -0
  56. rucio/rse/protocols/cache.py +122 -0
  57. rucio/rse/protocols/dummy.py +111 -0
  58. rucio/rse/protocols/gfal.py +703 -0
  59. rucio/rse/protocols/globus.py +243 -0
  60. rucio/rse/protocols/gsiftp.py +92 -0
  61. rucio/rse/protocols/http_cache.py +82 -0
  62. rucio/rse/protocols/mock.py +123 -0
  63. rucio/rse/protocols/ngarc.py +209 -0
  64. rucio/rse/protocols/posix.py +250 -0
  65. rucio/rse/protocols/protocol.py +594 -0
  66. rucio/rse/protocols/rclone.py +364 -0
  67. rucio/rse/protocols/rfio.py +136 -0
  68. rucio/rse/protocols/srm.py +338 -0
  69. rucio/rse/protocols/ssh.py +413 -0
  70. rucio/rse/protocols/storm.py +206 -0
  71. rucio/rse/protocols/webdav.py +550 -0
  72. rucio/rse/protocols/xrootd.py +301 -0
  73. rucio/rse/rsemanager.py +764 -0
  74. rucio/vcsversion.py +11 -0
  75. rucio/version.py +38 -0
  76. rucio_clients-35.8.2.data/data/etc/rse-accounts.cfg.template +25 -0
  77. rucio_clients-35.8.2.data/data/etc/rucio.cfg.atlas.client.template +42 -0
  78. rucio_clients-35.8.2.data/data/etc/rucio.cfg.template +257 -0
  79. rucio_clients-35.8.2.data/data/requirements.client.txt +15 -0
  80. rucio_clients-35.8.2.data/data/rucio_client/merge_rucio_configs.py +144 -0
  81. rucio_clients-35.8.2.data/scripts/rucio +2542 -0
  82. rucio_clients-35.8.2.data/scripts/rucio-admin +2447 -0
  83. rucio_clients-35.8.2.dist-info/METADATA +50 -0
  84. rucio_clients-35.8.2.dist-info/RECORD +88 -0
  85. rucio_clients-35.8.2.dist-info/WHEEL +5 -0
  86. rucio_clients-35.8.2.dist-info/licenses/AUTHORS.rst +97 -0
  87. rucio_clients-35.8.2.dist-info/licenses/LICENSE +201 -0
  88. rucio_clients-35.8.2.dist-info/top_level.txt +1 -0
rucio/common/pcache.py ADDED
@@ -0,0 +1,1408 @@
1
+ #!/usr/bin/env python
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 errno
17
+ import fcntl
18
+ import getopt
19
+ import os
20
+ import re
21
+ import signal
22
+ import subprocess
23
+ import sys
24
+ import time
25
+ from socket import gethostname
26
+ from typing import TYPE_CHECKING, Any, Optional
27
+ from urllib.parse import urlencode
28
+ from urllib.request import urlopen
29
+
30
+ if TYPE_CHECKING:
31
+ from collections.abc import Iterable, Iterator
32
+ from types import FrameType
33
+ from urllib.parse import _QueryType
34
+
35
+ from _typeshed import StrOrBytesPath
36
+
37
+
38
+ # The pCache Version
39
+ pcacheversion = "4.2.3"
40
+
41
+ # Log message levels
42
+ DEBUG, INFO, WARN, ERROR = "DEBUG", "INFO ", "WARN ", "ERROR"
43
+
44
+ # filename for locking
45
+ LOCK_NAME = ".LOCK"
46
+
47
+ # Session ID
48
+ sessid = "%s.%s" % (int(time.time()), os.getpid())
49
+
50
+
51
+ # Run a command with a timeout
52
+ def run_cmd(args: "subprocess._CMD", timeout: int = 0) -> tuple[int, Optional[bytes]]:
53
+
54
+ class Alarm(Exception):
55
+ pass
56
+
57
+ def alarm_handler(signum: int, frame: Optional["FrameType"]) -> None:
58
+ raise Alarm
59
+
60
+ # Execute the command as a subprocess
61
+ try:
62
+ p = subprocess.Popen(args=args,
63
+ shell=False,
64
+ stdout=subprocess.PIPE,
65
+ stderr=subprocess.STDOUT)
66
+
67
+ except:
68
+ return (-2, None)
69
+
70
+ # Set the timer if a timeout value was given
71
+ if (timeout > 0):
72
+ signal.signal(signal.SIGALRM, alarm_handler)
73
+ signal.alarm(timeout)
74
+
75
+ # Wait for the command to complete
76
+ try:
77
+
78
+ # Collect the output when the command completes
79
+ stdout = p.communicate()[0][:-1]
80
+
81
+ # Command completed in time, cancel the alarm
82
+ if (timeout > 0):
83
+ signal.alarm(0)
84
+
85
+ # Command timed out
86
+ except Alarm:
87
+
88
+ # The pid of our spawn
89
+ pids = [p.pid]
90
+
91
+ # The pids of the spawn of our spawn
92
+ pids.extend(get_process_children(p.pid))
93
+
94
+ # Terminate all of the evil spawn
95
+ for pid in pids:
96
+ try:
97
+ os.kill(pid, signal.SIGKILL)
98
+ except OSError:
99
+ pass
100
+
101
+ # Return a timeout error
102
+ return (-1, None)
103
+
104
+ return (p.returncode, stdout)
105
+
106
+
107
+ def get_process_children(pid: int) -> list[int]:
108
+
109
+ # Get a list of all pids associated with a given pid
110
+ p = subprocess.Popen(args='ps --no-headers -o pid --ppid %d' % pid,
111
+ shell=True,
112
+ stdout=subprocess.PIPE,
113
+ stderr=subprocess.PIPE)
114
+
115
+ # Wait and fetch the stdout
116
+ stdout, stderr = p.communicate()
117
+
118
+ # Return a list of pids as tuples
119
+ return [int(pr) for pr in stdout.split()]
120
+
121
+
122
+ def unitize(x: int) -> str:
123
+
124
+ suff = 'BKMGTPEZY'
125
+
126
+ while ((x >= 1024) and suff):
127
+ y = x / 1024.0
128
+ suff = suff[1:]
129
+ return "%.4g%s" % (y, suff[0])
130
+
131
+
132
+ class Pcache:
133
+
134
+ def Usage(self) -> None:
135
+ msg = """Usage: %s [flags] copy_prog [copy_flags] input output""" % self.progname
136
+ sys.stderr.write("%s\n" % msg) # py3, py2
137
+ # print>>sys.stderr, " flags are: "
138
+ # "s:r:m:Cy:A:R:t:r:g:fFl:VvqpH:S:",
139
+ # "scratch-dir=",
140
+ # "storage-root=",
141
+ # "max-space=",
142
+ # "clean",
143
+ # "hysterisis=",
144
+ # "accept=",
145
+ # "reject=",
146
+ # "timeout=",
147
+ # "retry=",
148
+ # "force",
149
+ # "flush-cache",
150
+ # "guid=",
151
+ # "log=",
152
+ # "version",
153
+ # "verbose",
154
+ # "debug",
155
+ # "quiet",
156
+ # "panda",
157
+ # "hostname",
158
+ # "sitename"
159
+
160
+ def __init__(self):
161
+ os.umask(0)
162
+ self.storage_root = "/pnfs"
163
+ self.scratch_dir = "/scratch/"
164
+ self.pcache_dir = self.scratch_dir + "pcache/"
165
+ self.log_file = self.pcache_dir + "pcache.log"
166
+ self.max_space = "80%"
167
+ self.percent_max = None
168
+ self.bytes_max = None
169
+ # self.max_space = "10T"
170
+ self.hysterisis = 0.75
171
+ # self.hysterisis = 0.9
172
+ self.clean = False
173
+ self.transfer_timeout_as_str: str = "600"
174
+ self.max_retries = 3
175
+ self.guid = None
176
+ self.accept_patterns: list[re.Pattern] = []
177
+ self.reject_patterns: list[re.Pattern] = []
178
+ self.force = False
179
+ self.flush = False
180
+ self.verbose = False
181
+ self.quiet = False
182
+ self.debug = False
183
+ self.hostname = None
184
+ self.sitename = None # XXX can we get this from somewhere?
185
+ self.update_panda = False
186
+ self.panda_url = "https://pandaserver.cern.ch:25443/server/panda/"
187
+ self.local_src = None
188
+ self.skip_download = False
189
+
190
+ # internal variables
191
+ self.sleep_interval = 15
192
+ self.force = False
193
+ self.locks = {}
194
+ self.deleted_guids = []
195
+ self.version = pcacheversion
196
+
197
+ def parse_args(self, args: list[str]) -> None:
198
+ # handle pcache flags and leave the rest in self.args
199
+
200
+ try:
201
+ opts, args = getopt.getopt(args,
202
+ "s:x:m:Cy:A:R:t:r:g:fFl:VvdqpPH:S:L:X:",
203
+ ["scratch-dir=",
204
+ "storage-root=",
205
+ "max-space=",
206
+ "clean",
207
+ "hysterisis=",
208
+ "accept=",
209
+ "reject=",
210
+ "timeout=",
211
+ "retry=",
212
+ "force",
213
+ "flush-cache",
214
+ "guid=",
215
+ "log=",
216
+ "version",
217
+ "verbose",
218
+ "debug",
219
+ "quiet",
220
+ "print-stats",
221
+ "panda",
222
+ "hostname",
223
+ "sitename",
224
+ "local-src"])
225
+
226
+ # XXXX cache, stats, reset, clean, delete, inventory
227
+ # TODO: move checksum/size validation from lsm to pcache
228
+ except getopt.GetoptError as err:
229
+ sys.stderr.write("%s\n" % str(err))
230
+ self.Usage()
231
+ self.fail(100)
232
+
233
+ for opt, arg in opts:
234
+ if opt in ("-s", "--scratch-dir"):
235
+ self.scratch_dir = arg
236
+ # Make sure scratch_dir endswith /
237
+ if not self.scratch_dir.endswith("/"):
238
+ self.scratch_dir += "/"
239
+ self.pcache_dir = self.scratch_dir + "pcache/"
240
+ self.log_file = self.pcache_dir + "pcache.log"
241
+ elif opt in ("-x", "--storage-root"):
242
+ self.storage_root = arg
243
+ elif opt in ("-m", "--max-space"):
244
+ self.max_space = arg
245
+ elif opt in ("-y", "--hysterisis"):
246
+ if arg.endswith('%'):
247
+ self.hysterisis = float(arg[:-1]) / 100
248
+ else:
249
+ self.hysterisis = float(arg)
250
+ elif opt in ("-A", "--accept"):
251
+ self.accept_patterns.append(re.compile(arg))
252
+ elif opt in ("-R", "--reject"):
253
+ self.reject_patterns.append(re.compile(arg))
254
+ elif opt in ("-t", "--timeout"):
255
+ self.transfer_timeout_as_str = arg
256
+ elif opt in ("-f", "--force"):
257
+ self.force = True
258
+ elif opt in ("-F", "--flush-cache"):
259
+ self.flush = True
260
+ elif opt in ("-C", "--clean"):
261
+ self.clean = True
262
+ elif opt in ("-g", "--guid"):
263
+ if arg == 'None':
264
+ self.guid = None
265
+ else:
266
+ self.guid = arg
267
+ elif opt in ("-r", "--retry"):
268
+ self.max_retries = int(arg)
269
+ elif opt in ("-V", "--version"):
270
+ print(str(self.version))
271
+ sys.exit(0)
272
+ elif opt in ("-l", "--log"):
273
+ self.log_file = arg
274
+ elif opt in ("-v", "--verbose"):
275
+ self.verbose = True
276
+ elif opt in ("-d", "--debug"):
277
+ self.debug = True
278
+ elif opt in ("-q", "--quiet"):
279
+ self.quiet = True
280
+ elif opt in ("-p", "--print-stats"):
281
+ self.print_stats()
282
+ sys.exit(0)
283
+ elif opt in ("-P", "--panda"):
284
+ self.update_panda = True
285
+ elif opt in ("-H", "--hostname"):
286
+ self.hostname = arg
287
+ elif opt in ("-S", "--sitename"):
288
+ self.sitename = arg
289
+ elif opt in ("-L", "--local-src"):
290
+ self.local_src = str(arg)
291
+ elif opt in ("-X", "--skip-download"):
292
+ if str(arg) in ('True', 'true') or arg:
293
+ self.skip_download = True
294
+
295
+ # Treatment of limits on pcache size
296
+ self._convert_max_space()
297
+
298
+ # Convert timeout to seconds
299
+ mult = 1
300
+ t = self.transfer_timeout_as_str
301
+ suff = t[-1]
302
+ if suff in ('H', 'h'):
303
+ mult = 3600
304
+ t = t[:-1]
305
+ elif suff in ('M', 'm'):
306
+ mult = 60
307
+ t = t[:1]
308
+ elif suff in ('S', 's'):
309
+ mult = 1
310
+ t = t[:-1]
311
+ self.transfer_timeout: int = mult * int(t)
312
+
313
+ # Set host and name
314
+ if self.hostname is None:
315
+ self.hostname = gethostname()
316
+ if self.sitename is None:
317
+ self.sitename = os.environ.get("SITE", "") # XXXX
318
+
319
+ # All done
320
+ self.args = args
321
+
322
+ def _convert_max_space(self) -> None:
323
+ '''
324
+ Added by Rucio team. Converts max allowed space usage of pcache into units used by this tool.
325
+ :input self.max_space: limit set by user
326
+ :output self.percent_max: max percentage of pcache space used
327
+ :output self.bytes_max: max size in bytes of pcache space used
328
+ '''
329
+
330
+ # Convert max_space arg to percent_max or bytes_max
331
+ if self.max_space.endswith('%'):
332
+ self.percent_max = float(self.max_space[:-1])
333
+ self.bytes_max = None
334
+ else: # handle suffix
335
+ self.percent_max = None
336
+ m = self.max_space.upper()
337
+ index = "BKMGTPEZY".find(m[-1])
338
+ if index >= 0:
339
+ self.bytes_max = float(m[:-1]) * (1024**index)
340
+ else: # Numeric value w/o units (exception if invalid)
341
+ self.bytes_max = float(self.max_space)
342
+
343
+ def clean_pcache(self, max_space: Optional[str] = None) -> None:
344
+ '''
345
+ Added by Rucio team. Cleans pcache in case it is over limit.
346
+ Used for tests of the pcache functionality. Can be called without other init.
347
+ '''
348
+
349
+ self.t0 = time.time()
350
+ self.progname = "pcache"
351
+
352
+ # set max. occupancy of pcache:
353
+ if max_space:
354
+ self.max_space = max_space
355
+ self._convert_max_space()
356
+
357
+ # Fail on extra args
358
+ if not self.scratch_dir:
359
+ self.Usage()
360
+ self.fail(100)
361
+
362
+ # hardcoded pcache dir
363
+ self.pcache_dir = self.scratch_dir + '/pcache/'
364
+
365
+ # clean pcache
366
+ self.maybe_start_cleaner_thread()
367
+
368
+ def check_and_link(
369
+ self,
370
+ src: str = '',
371
+ dst: str = '',
372
+ dst_prefix: str = '',
373
+ scratch_dir: str = '/scratch/',
374
+ storage_root: Optional[str] = None,
375
+ force: bool = False,
376
+ guid: Optional[str] = None,
377
+ log_file: Optional[str] = None,
378
+ version: str = '',
379
+ hostname: Optional[str] = None,
380
+ sitename: Optional[str] = None,
381
+ local_src: Optional[str] = None
382
+ ):
383
+ '''
384
+ Added by Rucio team. Replacement for the main method.
385
+ Checks whether a file is in pcache:
386
+ - if yes: creates a hardlink to the file in pcahce
387
+ - if not:
388
+ - returns 500 and leaves it to Rucio
389
+ - Rucio downloads a file
390
+ Makes hardlink in pcache to downloaded file:
391
+ - needs :param local_src: path to downloaded file
392
+ '''
393
+ self.t0 = time.time()
394
+ self.progname = "pcache"
395
+ self.pcache_dir = scratch_dir + '/pcache/'
396
+ self.src = src
397
+ self.dst = dst
398
+ self.dst_prefix = dst_prefix
399
+ self.sitename = sitename
400
+ self.hostname = hostname
401
+ self.guid = guid
402
+ if log_file:
403
+ self.log_file = log_file
404
+ self.local_src = local_src
405
+ self.version = version
406
+ self.storage_root = storage_root
407
+
408
+ # Cache dir may have been wiped
409
+ if ((not os.path.exists(self.pcache_dir)) and self.update_panda):
410
+ self.panda_flush_cache()
411
+
412
+ # Create the pCache directory
413
+ if (self.mkdir_p(self.pcache_dir)):
414
+ self.fail(101)
415
+
416
+ self.log(INFO, "%s %s invoked as: API", self.progname, self.version)
417
+
418
+ # Fail on extra args
419
+ if not scratch_dir:
420
+ self.Usage()
421
+ self.fail(100)
422
+
423
+ # If the source is lfn:, execute original command, no further action
424
+ if (self.src.startswith('lfn:')):
425
+ # status = os.execvp(self.copy_util, self.args)
426
+ os._exit(1)
427
+
428
+ # If the destination is a local file, do some rewrites
429
+ if (self.dst.startswith('file:')):
430
+ self.dst_prefix = 'file:'
431
+ self.dst = self.dst[5:]
432
+ # Leave one '/' on dst
433
+ while ((len(self.dst) > 1) and (self.dst[1] == '/')):
434
+ self.dst_prefix += '/'
435
+ self.dst = self.dst[1:]
436
+
437
+ # load file into pcache
438
+ self.create_pcache_dst_dir()
439
+ # XXXX TODO _ dst_dir can get deleted before lock!
440
+ waited = False
441
+
442
+ # If another transfer is active, lock_dir will block
443
+ if (self.lock_dir(self.pcache_dst_dir, blocking=False)):
444
+ waited = True
445
+ self.log(INFO, "%s locked, waiting", self.pcache_dst_dir)
446
+ self.lock_dir(self.pcache_dst_dir, blocking=True)
447
+
448
+ if (waited):
449
+ self.log(INFO, "waited %.2f secs", time.time() - self.t0)
450
+
451
+ if force:
452
+ self.empty_dir(self.pcache_dst_dir)
453
+
454
+ # The name of the cached version of this file
455
+ cache_file = self.pcache_dst_dir + "data"
456
+
457
+ # Check if the file is in cache or we need to transfer it down
458
+ if (os.path.exists(cache_file)):
459
+ exit_status = 0
460
+ copy_status = None
461
+ self.log(INFO, "check_and_link: file found in cache")
462
+ self.log(INFO, "cache hit %s", self.src)
463
+ self.update_stats("cache_hits")
464
+ self.finish()
465
+ if (os.path.exists(self.dst)):
466
+ copy_status = 1
467
+ elif self.local_src:
468
+ exit_status = 1
469
+ copy_status = None
470
+ self.log(INFO, "check_and_link: local replica found, linking to pcache")
471
+ self.finish()
472
+ else:
473
+ self.log(INFO, "check_and_link: %s file not found in pcache and was not downloaded yet", self.src)
474
+ return (500, None)
475
+
476
+ self.unlock_dir(self.pcache_dst_dir)
477
+ self.log(INFO, "total time %.2f secs", time.time() - self.t0)
478
+
479
+ # in case that the pcache is over limit
480
+ self.maybe_start_cleaner_thread()
481
+
482
+ # Return if the file was cached, copied or an error (and its code)
483
+ return (exit_status, copy_status)
484
+
485
+ def main(self, args: list[str]) -> tuple[int, Optional[int]]:
486
+
487
+ # args
488
+ self.cmdline = ' '.join(args)
489
+ self.t0 = time.time()
490
+ self.progname = args[0] or "pcache"
491
+
492
+ # Must have a list of arguments
493
+ if (self.parse_args(args[1:])):
494
+ self.Usage()
495
+ self.fail(100)
496
+
497
+ # Cache dir may have been wiped
498
+ if ((not os.path.exists(self.pcache_dir)) and self.update_panda):
499
+ self.panda_flush_cache()
500
+
501
+ # Create the pCache directory
502
+ if (self.mkdir_p(self.pcache_dir)):
503
+ self.fail(101)
504
+
505
+ self.log(INFO, "%s %s invoked as: %s", self.progname, self.version, self.cmdline)
506
+
507
+ # Are we flushing the cache
508
+ if (self.flush):
509
+ if (self.args):
510
+ sys.stderr.write("--flush not compatible with other options")
511
+ self.fail(100)
512
+ else:
513
+ self.flush_cache()
514
+ sys.exit(0)
515
+
516
+ # Are we cleaning the cache
517
+ if (self.clean):
518
+ # size = self.do_cache_inventory()
519
+ self.maybe_start_cleaner_thread()
520
+ if (len(self.args) < 1):
521
+ sys.exit(0)
522
+
523
+ # Fail on extra args
524
+ if (len(self.args) < 3):
525
+ self.Usage()
526
+ self.fail(100)
527
+
528
+ self.copy_util = self.args[0]
529
+ self.copy_args = self.args[1:-2]
530
+ self.src = self.args[-2]
531
+ self.dst = self.args[-1]
532
+ self.dst_prefix = ''
533
+
534
+ # If the source is lfn:, execute original command, no further action
535
+ if (self.src.startswith('lfn:')):
536
+ # status = os.execvp(self.copy_util, self.args)
537
+ os._exit(1)
538
+
539
+ # If the destination is a local file, do some rewrites
540
+ if (self.dst.startswith('file:')):
541
+ self.dst_prefix = 'file:'
542
+ self.dst = self.dst[5:]
543
+ # Leave one '/' on dst
544
+ while ((len(self.dst) > 1) and (self.dst[1] == '/')):
545
+ self.dst_prefix += '/'
546
+ self.dst = self.dst[1:]
547
+
548
+ # Execute original command, no further action
549
+ if (not (self.dst.startswith(self.scratch_dir) and self.accept(self.src) and (not self.reject(self.src)))):
550
+ os.execvp(self.copy_util, self.args) # noqa: S606
551
+ os._exit(1)
552
+
553
+ # XXXX todo: fast-path - try to acquire lock
554
+ # first, if that succeeds, don't call
555
+ # create_pcache_dst_dir
556
+
557
+ # load file into pcache
558
+ self.create_pcache_dst_dir()
559
+ # XXXX TODO _ dst_dir can get deleted before lock!
560
+ waited = False
561
+
562
+ # If another transfer is active, lock_dir will block
563
+ if (self.lock_dir(self.pcache_dst_dir, blocking=False)):
564
+ waited = True
565
+ self.log(INFO, "%s locked, waiting", self.pcache_dst_dir)
566
+ self.lock_dir(self.pcache_dst_dir, blocking=True)
567
+
568
+ if (waited):
569
+ self.log(INFO, "waited %.2f secs", time.time() - self.t0)
570
+
571
+ if (self.force):
572
+ self.empty_dir(self.pcache_dst_dir)
573
+
574
+ # The name of the cached version of this file
575
+ cache_file = self.pcache_dst_dir + "data"
576
+
577
+ # Check if the file is in cache or we need to transfer it down
578
+ if (os.path.exists(cache_file)):
579
+ exit_status = 1
580
+ copy_status = None
581
+ self.log(INFO, "cache hit %s", self.src)
582
+ self.update_stats("cache_hits")
583
+ self.finish()
584
+ else:
585
+ if self.skip_download:
586
+ return (500, None)
587
+ self.update_stats("cache_misses")
588
+ exit_status, copy_status = self.pcache_copy_in()
589
+ if ((exit_status == 0) or (exit_status == 2)):
590
+ self.finish()
591
+
592
+ self.unlock_dir(self.pcache_dst_dir)
593
+ self.log(INFO, "total time %.2f secs", time.time() - self.t0)
594
+
595
+ self.maybe_start_cleaner_thread()
596
+
597
+ # Return if the file was cached, copied or an error (and its code)
598
+ return (exit_status, copy_status)
599
+
600
+ def finish(self, local_src: Optional[str] = None) -> None:
601
+ cache_file = self.pcache_dst_dir + "data"
602
+ self.update_mru()
603
+ if self.local_src:
604
+ if (self.make_hard_link(self.local_src, cache_file)):
605
+ self.fail(102)
606
+ else:
607
+ if (self.make_hard_link(cache_file, self.dst)):
608
+ self.fail(102)
609
+
610
+ def pcache_copy_in(self) -> tuple[int, Optional[int]]:
611
+
612
+ cache_file = self.pcache_dst_dir + "data"
613
+
614
+ # Record source URL
615
+ try:
616
+ fname = self.pcache_dst_dir + "src"
617
+ f = open(fname, 'w')
618
+ f.write(self.src + '\n')
619
+ f.close()
620
+ self.chmod(fname, 0o666)
621
+ except:
622
+ pass
623
+
624
+ # Record GUID if given
625
+ if (self.guid):
626
+ try:
627
+ fname = self.pcache_dst_dir + "guid"
628
+ f = open(fname, 'w')
629
+ f.write(self.guid + '\n')
630
+ f.close()
631
+ self.chmod(fname, 0o666)
632
+ except:
633
+ pass
634
+
635
+ # Try to transfer the file up the the number of retries allowed
636
+ retry = 0
637
+ while (retry <= self.max_retries):
638
+
639
+ # Is this is a retry attempt, log it as such
640
+ if (retry > 0):
641
+ self.log(INFO, "do_transfer: retry %s", retry)
642
+
643
+ # Do the transfer. exit_status will be either
644
+ # 0 - success
645
+ # 3 - Transfer command failed. copy_status has the return code
646
+ # 4 - Transfer command timed out
647
+ # 5 - Transfer command was not found
648
+ exit_status, copy_status = self.do_transfer()
649
+
650
+ # If success, stop trying, otherwise increment the retry count
651
+ if (exit_status == 0):
652
+ break
653
+ retry += 1
654
+
655
+ # Did the transfer succeed
656
+ if (exit_status == 0):
657
+
658
+ # If we succeeded on a retry, return status 2 and the retries
659
+ if (retry == 0):
660
+ copy_status = None
661
+ else:
662
+ exit_status = 2
663
+ copy_status = retry
664
+
665
+ # Update the cache information
666
+ if self.local_src:
667
+ self.update_cache_size(os.stat(self.local_src).st_size)
668
+ else:
669
+ self.update_cache_size(os.stat(cache_file).st_size)
670
+
671
+ # Update the panda cache
672
+ if (self.guid and self.update_panda):
673
+ self.panda_add_cache_files((self.guid,))
674
+
675
+ return (exit_status, copy_status)
676
+
677
+ def create_pcache_dst_dir(self) -> None:
678
+
679
+ d = self.src
680
+ if self.storage_root is not None:
681
+ index = d.find(self.storage_root)
682
+
683
+ if (index >= 0):
684
+ d = d[index:]
685
+ else:
686
+ index = d.find("SFN=")
687
+ if (index >= 0):
688
+ d = d[index + 4:]
689
+
690
+ # self.log(INFO, '%s', self.storage_root)
691
+ # self.log(INFO, '%s', d)
692
+ # XXXX any more patterns to look for?
693
+ d = os.path.normpath(self.pcache_dir + "CACHE/" + d)
694
+ if (not d.endswith('/')):
695
+ d += '/'
696
+
697
+ self.pcache_dst_dir = d
698
+ status = self.mkdir_p(d)
699
+ if (status):
700
+ self.log(ERROR, "mkdir %s %s", d, status)
701
+ self.fail(103)
702
+
703
+ def get_disk_usage(self) -> int:
704
+ p = os.popen("df -P %s | tail -1" % self.pcache_dir, 'r') # noqa: S605
705
+ data = p.read()
706
+ status = p.close()
707
+ if status:
708
+ self.log(ERROR, "get_disk_usage: df command failed, status=%s", status)
709
+ sys.exit(1)
710
+ tok = data.split()
711
+ percent = tok[-2]
712
+ if not percent.endswith('%'):
713
+ self.log(ERROR, "get_disk_usage: cannot parse df output: %s", data)
714
+ sys.exit(1)
715
+ percent = int(percent[:-1])
716
+ return percent
717
+
718
+ def over_limit(self, factor: float = 1.0) -> bool:
719
+ if self.percent_max:
720
+ return self.get_disk_usage() > factor * self.percent_max
721
+ if self.bytes_max:
722
+ cache_size = self.get_cache_size()
723
+ if cache_size is not None:
724
+ return cache_size > factor * self.bytes_max
725
+ return False
726
+
727
+ def clean_cache(self) -> None:
728
+ t0 = time.time()
729
+ cache_size = self.get_cache_size()
730
+
731
+ if cache_size is not None:
732
+ self.log(INFO, "starting cleanup, cache size=%s, usage=%s%%",
733
+ unitize(cache_size),
734
+ self.get_disk_usage())
735
+
736
+ for link in self.list_by_mru():
737
+ try:
738
+ d = os.readlink(link)
739
+
740
+ except OSError as e:
741
+ self.log(ERROR, "readlink: %s", e)
742
+ continue
743
+
744
+ self.log(DEBUG, "deleting %s", d)
745
+
746
+ if os.path.exists(d):
747
+ self.empty_dir(d)
748
+ else:
749
+ self.log(WARN, "Attempt to delete missing file %s", d)
750
+ self.flush_cache()
751
+ break
752
+
753
+ # empty_dir should also delete MRU symlink, but
754
+ # mop up here in there is some problem with the
755
+ # backlink
756
+
757
+ try:
758
+ os.unlink(link)
759
+
760
+ except OSError as e:
761
+ if e.errno != errno.ENOENT:
762
+ self.log(ERROR, "unlink: %s", e)
763
+
764
+ if not self.over_limit(self.hysterisis):
765
+ break
766
+
767
+ self.log(INFO, "cleanup complete, cache size=%s, usage=%s%%, time=%.2f secs",
768
+ self.get_cache_size(),
769
+ self.get_disk_usage(),
770
+ time.time() - t0)
771
+
772
+ def list_by_mru(self) -> "Iterator[str]":
773
+ mru_dir = self.pcache_dir + "MRU/"
774
+ for root, dirs, files in os.walk(mru_dir):
775
+ dirs.sort()
776
+ for d in dirs:
777
+ path = os.path.join(root, d)
778
+ if os.path.islink(path):
779
+ dirs.remove(d)
780
+ yield path
781
+ if files:
782
+ files.sort()
783
+ for file in files:
784
+ path = os.path.join(root, file)
785
+ yield path
786
+
787
+ def flush_cache(self) -> None:
788
+ # Delete everything in CACHE, MRU, and reset stats
789
+ self.log(INFO, "flushing cache")
790
+ if self.update_panda:
791
+ self.panda_flush_cache()
792
+ self.reset_stats()
793
+ ts = '.' + str(time.time())
794
+ for d in "CACHE", "MRU":
795
+ d = self.pcache_dir + d
796
+ try:
797
+ os.rename(d, d + ts)
798
+ os.system("rm -rf %s &" % (d + ts)) # noqa: S605
799
+ except OSError as e:
800
+ if e.errno != errno.ENOENT:
801
+ self.log(ERROR, "%s: %s", d, e)
802
+
803
+ def do_transfer(self) -> tuple[int, Optional[int]]:
804
+
805
+ # Cache file and transfer file locations
806
+ cache_file = self.pcache_dst_dir + "data"
807
+ xfer_file = self.pcache_dst_dir + "xfer"
808
+
809
+ # Remove any transfer file with the same name
810
+ try:
811
+ os.unlink(xfer_file)
812
+ except OSError as e:
813
+ if e.errno != errno.ENOENT:
814
+ self.log(ERROR, "unlink: %s", e)
815
+
816
+ # Build the copy command with the destination into the xfer location
817
+ args = self.args[:]
818
+ args[-1] = self.dst_prefix + xfer_file
819
+
820
+ # Save the current time for timing output
821
+ t0 = time.time()
822
+
823
+ # Do the copy with a timeout
824
+ if self.local_src:
825
+ return (0, None)
826
+ else:
827
+ copy_status, copy_output = run_cmd(args, self.transfer_timeout)
828
+
829
+ # Did the command timeout
830
+ if (copy_status == -1):
831
+ self.log(ERROR, "copy command timed out, elapsed time = %.2f sec", time.time() - t0)
832
+ self.cleanup_failed_transfer()
833
+ return (4, None)
834
+ elif (copy_status == -2):
835
+ self.log(ERROR, "copy command was not found")
836
+ self.cleanup_failed_transfer()
837
+ return (5, None)
838
+
839
+ # Display any output from the copy
840
+ if (copy_output):
841
+ print('%s' % copy_output)
842
+
843
+ # Did the copy succeed (good status and an existing file)
844
+ if ((copy_status > 0) or (not os.path.exists(xfer_file))):
845
+ self.log(ERROR, "copy command failed, elapsed time = %.2f sec", time.time() - t0)
846
+ self.cleanup_failed_transfer()
847
+ return (3, copy_status)
848
+
849
+ self.log(INFO, "copy command succeeded, elapsed time = %.2f sec", time.time() - t0)
850
+
851
+ try:
852
+ os.rename(xfer_file, cache_file)
853
+ # self.log(INFO, "rename %s %s", xfer_file, cache_file)
854
+ except OSError: # Fatal error if we can't do this
855
+ self.log(ERROR, "rename %s %s", xfer_file, cache_file)
856
+ try:
857
+ os.unlink(xfer_file)
858
+ except:
859
+ pass
860
+ self.fail(104)
861
+
862
+ # Make the file readable to all
863
+ self.chmod(cache_file, 0o666)
864
+
865
+ # Transfer completed, return the transfer command status
866
+ return (0, None)
867
+
868
+ def maybe_start_cleaner_thread(self) -> None:
869
+ if not self.over_limit():
870
+ return
871
+ # exit immediately if another cleaner is active
872
+ cleaner_lock = os.path.join(self.pcache_dir, ".clean")
873
+ if self.lock_file(cleaner_lock, blocking=False):
874
+ self.log(INFO, "cleanup not starting: %s locked", cleaner_lock)
875
+ return
876
+ # see http://www.faqs.org/faqs/unix-faq/faq/part3/section-13.html
877
+ # for explanation of double-fork
878
+ pid = os.fork()
879
+ if pid: # parent
880
+ os.waitpid(pid, 0)
881
+ return
882
+ else: # child
883
+ self.daemonize()
884
+ pid = os.fork()
885
+ if pid:
886
+ os._exit(0)
887
+ # grandchild
888
+ self.clean_cache()
889
+ self.unlock_file(cleaner_lock)
890
+ os._exit(0)
891
+
892
+ def make_hard_link(self, src: "StrOrBytesPath", dst: "StrOrBytesPath") -> Optional[int]:
893
+ self.log(INFO, "linking %s to %s", src, dst)
894
+ try:
895
+ if os.path.exists(dst):
896
+ os.unlink(dst)
897
+ os.link(src, dst)
898
+ except OSError as e:
899
+ self.log(ERROR, "make_hard_link: %s", e)
900
+ ret = e.errno
901
+ if ret == errno.ENOENT:
902
+ try:
903
+ stat_info = os.stat(src)
904
+ self.log(INFO, "stat(%s) = %s", src, stat_info)
905
+ except:
906
+ self.log(INFO, "cannot stat %s", src)
907
+ try:
908
+ stat_info = os.stat(dst)
909
+ self.log(INFO, "stat(%s) = %s", dst, stat_info)
910
+ except:
911
+ self.log(INFO, "cannot stat %s", dst)
912
+ return ret
913
+
914
+ def reject(self, name: str) -> bool:
915
+ for pat in self.reject_patterns:
916
+ if pat.search(name):
917
+ return True
918
+ return False
919
+
920
+ def accept(self, name: str) -> bool:
921
+ if not self.accept_patterns:
922
+ return True
923
+ for pat in self.accept_patterns:
924
+ if pat.search(name):
925
+ return True
926
+ return False
927
+
928
+ def get_stat(self, stats_dir: str, stat_name: str) -> int:
929
+ filename = os.path.join(self.pcache_dir, stats_dir, stat_name)
930
+ try:
931
+ f = open(filename, 'r')
932
+ data = int(f.read().strip())
933
+ f.close()
934
+ except:
935
+ data = 0
936
+ return data
937
+
938
+ def print_stats(self) -> None:
939
+ print(("Cache size: %s", unitize(self.get_stat("CACHE", "size"))))
940
+ print(("Cache hits: %s", self.get_stat("stats", "cache_hits")))
941
+ print(("Cache misses: %s", self.get_stat("stats", "cache_misses")))
942
+
943
+ def reset_stats(self) -> None:
944
+ stats_dir = os.path.join(self.pcache_dir, "stats")
945
+ try:
946
+ for f in os.listdir(stats_dir):
947
+ try:
948
+ os.unlink(os.path.join(stats_dir, f))
949
+ except:
950
+ pass
951
+ except:
952
+ pass
953
+ # XXXX error handling
954
+ pass
955
+
956
+ def update_stat_file(self, stats_dir: str, name: str, delta: int) -> None: # internal
957
+ stats_dir = os.path.join(self.pcache_dir, stats_dir)
958
+ self.mkdir_p(stats_dir)
959
+ self.lock_dir(stats_dir)
960
+ stats_file = os.path.join(stats_dir, name)
961
+ try:
962
+ f = open(stats_file, 'r')
963
+ data = f.read()
964
+ f.close()
965
+ value = int(data)
966
+ except:
967
+ # XXXX
968
+ value = 0
969
+ value += delta
970
+ try:
971
+ f = open(stats_file, 'w')
972
+ f.write("%s\n" % value)
973
+ f.close()
974
+ self.chmod(stats_file, 0o666)
975
+ except:
976
+ pass
977
+ # XXX
978
+ self.unlock_dir(stats_dir)
979
+
980
+ def update_stats(self, name: str, delta: int = 1) -> None:
981
+ return self.update_stat_file("stats", name, delta)
982
+
983
+ def update_cache_size(self, bytes_: int) -> None:
984
+ return self.update_stat_file("CACHE", "size", bytes_)
985
+
986
+ def get_cache_size(self) -> Optional[int]:
987
+ filename = os.path.join(self.pcache_dir, "CACHE", "size")
988
+ size = 0
989
+
990
+ try:
991
+ f = open(filename)
992
+ data = f.read()
993
+ size = int(data)
994
+ except:
995
+ pass
996
+
997
+ # If we could not fetch the size, do a reinventory
998
+ if size == 0:
999
+ size = self.do_cache_inventory()
1000
+
1001
+ # The size should never be negative, so lets cleanup and start over
1002
+ if size is not None and size < 0:
1003
+ self.log(WARN, "CACHE corruption found. Negative CACHE size: %d", size)
1004
+ self.flush_cache()
1005
+ size = 0
1006
+
1007
+ return size
1008
+
1009
+ def do_cache_inventory(self) -> Optional[int]:
1010
+
1011
+ inventory_lock = os.path.join(self.pcache_dir, ".inventory")
1012
+ if self.lock_file(inventory_lock, blocking=False):
1013
+ return
1014
+
1015
+ size = 0
1016
+
1017
+ self.log(INFO, "starting inventory")
1018
+
1019
+ for root, dirs, files in os.walk(self.pcache_dir):
1020
+ for f in files:
1021
+ if f == "data":
1022
+ fullname = os.path.join(root, f)
1023
+ try:
1024
+ size += os.stat(fullname).st_size
1025
+ except OSError as e:
1026
+ self.log(ERROR, "stat(%s): %s", fullname, e)
1027
+
1028
+ filename = os.path.join(self.pcache_dir, "CACHE", "size")
1029
+
1030
+ try:
1031
+ f = open(filename, 'w')
1032
+ f.write("%s\n" % size)
1033
+ f.close()
1034
+ self.chmod(filename, 0o666)
1035
+ except:
1036
+ pass # XXXX
1037
+
1038
+ self.unlock_file(inventory_lock)
1039
+ self.log(INFO, "inventory complete, cache size %s", size)
1040
+ return size
1041
+
1042
+ def daemonize(self) -> None:
1043
+ if self.debug:
1044
+ return
1045
+ try:
1046
+ os.setsid()
1047
+ except OSError:
1048
+ pass
1049
+ try:
1050
+ os.chdir("/")
1051
+ except OSError:
1052
+ pass
1053
+ try:
1054
+ os.umask(0)
1055
+ except OSError:
1056
+ pass
1057
+ n = os.open("/dev/null", os.O_RDWR)
1058
+ i, o, e = sys.stdin.fileno(), sys.stdout.fileno(), sys.stderr.fileno()
1059
+ os.dup2(n, i)
1060
+ os.dup2(n, o)
1061
+ os.dup2(n, e)
1062
+ MAXFD = 1024
1063
+ try:
1064
+ import resource # Resource usage information.
1065
+ maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1066
+ if (maxfd == resource.RLIM_INFINITY):
1067
+ maxfd = MAXFD
1068
+ except:
1069
+ try:
1070
+ maxfd = os.sysconf("SC_OPEN_MAX")
1071
+ except:
1072
+ maxfd = MAXFD # use default value
1073
+
1074
+ for fd in range(0, maxfd + 1):
1075
+ try:
1076
+ os.close(fd)
1077
+ except:
1078
+ pass
1079
+
1080
+ # Panda server callback functions
1081
+ def do_http_post(self, url: str, data: "_QueryType") -> None:
1082
+ # see http://www.faqs.org/faqs/unix-faq/faq/part3/section-13.html
1083
+ # for explanation of double-fork (is it overkill here?)
1084
+ pid = os.fork()
1085
+ if pid: # parent
1086
+ os.waitpid(pid, 0)
1087
+ return
1088
+ else: # child
1089
+ self.daemonize()
1090
+ pid = os.fork()
1091
+ if pid:
1092
+ os._exit(0)
1093
+ # grandchild
1094
+ retry = 0
1095
+ # This will retry for up to 1 hour, at 2 minute intervals
1096
+ while retry < 30:
1097
+ try:
1098
+ u = urlopen(url, data=urlencode(data)) # type: ignore
1099
+ ret = u.read()
1100
+ u.close()
1101
+ self.log(INFO, "http post to %s, retry %s, data='%s', return='%s'",
1102
+ url, retry, data, ret)
1103
+ if ret == "True":
1104
+ break
1105
+ except:
1106
+ exc, msg, tb = sys.exc_info()
1107
+ self.log(ERROR, "post to %s, data=%s, error=%s", url, data, msg)
1108
+ retry += 1
1109
+ time.sleep(120)
1110
+ # finished, don't keep the child thread around!
1111
+ os._exit(0)
1112
+
1113
+ def panda_flush_cache(self) -> None:
1114
+ self.do_http_post(self.panda_url + "flushCacheDB",
1115
+ data={"site": self.sitename,
1116
+ "node": self.hostname})
1117
+
1118
+ def panda_add_cache_files(self, guids: "Iterable[str]") -> None:
1119
+ self.do_http_post(self.panda_url + "addFilesToCacheDB",
1120
+ data={"site": self.sitename,
1121
+ "node": self.hostname,
1122
+ "guids": ','.join(guids)})
1123
+
1124
+ def panda_del_cache_files(self, guids: "Iterable[str]") -> None:
1125
+ self.do_http_post(self.panda_url + "deleteFilesFromCacheDB",
1126
+ data={"site": self.sitename,
1127
+ "node": self.hostname,
1128
+ "guids": ','.join(guids)})
1129
+
1130
+ # Locking functions
1131
+ def lock_dir(self, d: str, create: bool = True, blocking: bool = True) -> Optional[int]:
1132
+ lock_name = os.path.join(d, LOCK_NAME)
1133
+ lock_status = self.lock_file(lock_name, blocking)
1134
+ if (not lock_status): # succeeded
1135
+ return
1136
+ if ((lock_status == errno.ENOENT) and create):
1137
+ mkdir_status = self.mkdir_p(d)
1138
+ if (mkdir_status):
1139
+ self.log(ERROR, "mkdir %s %s", d, mkdir_status)
1140
+ self.fail(105)
1141
+ lock_status = self.lock_file(lock_name, blocking)
1142
+ return lock_status
1143
+
1144
+ def unlock_dir(self, d: str) -> Optional[Any]:
1145
+ return self.unlock_file(os.path.join(d, LOCK_NAME))
1146
+
1147
+ def lock_file(self, name: str, blocking: bool = True) -> Optional[int]:
1148
+ if name in self.locks:
1149
+ self.log(DEBUG, "lock_file: %s already locked", name)
1150
+ return
1151
+ try:
1152
+ f = open(name, 'w')
1153
+ except OSError as e:
1154
+ self.log(ERROR, "open: %s", e)
1155
+ return e.errno
1156
+
1157
+ self.locks[name] = f
1158
+ flag = fcntl.LOCK_EX
1159
+ if not blocking:
1160
+ flag |= fcntl.LOCK_NB
1161
+ while True:
1162
+ try:
1163
+ status = fcntl.lockf(f, flag)
1164
+ break
1165
+ except OSError as e:
1166
+ if e.errno in (errno.EAGAIN, errno.EACCES) and not blocking:
1167
+ f.close()
1168
+ del self.locks[name]
1169
+ return e.errno
1170
+ if e.errno != errno.EINTR:
1171
+ status = e.errno
1172
+ self.log(ERROR, "lockf: %s", e)
1173
+ self.fail(106)
1174
+ return status
1175
+
1176
+ def unlock_file(self, name: str) -> Optional[Any]:
1177
+ f = self.locks.get(name)
1178
+ if not f:
1179
+ self.log(DEBUG, "unlock_file: %s not locked", name)
1180
+ return
1181
+
1182
+ # XXXX does this create a possible race condition?
1183
+ if 0:
1184
+ try:
1185
+ os.unlink(name)
1186
+ except:
1187
+ pass
1188
+ status = fcntl.lockf(f, fcntl.LOCK_UN)
1189
+ f.close()
1190
+ del self.locks[name]
1191
+ return status
1192
+
1193
+ def unlock_all(self) -> None:
1194
+ for filename, f in list(self.locks.items()):
1195
+ try:
1196
+ f.close()
1197
+ os.unlink(filename)
1198
+ except:
1199
+ pass
1200
+
1201
+ # Cleanup functions
1202
+ def delete_file_and_parents(self, name: str) -> None:
1203
+ try:
1204
+ os.unlink(name)
1205
+ except OSError as e:
1206
+ if e.errno != errno.ENOENT:
1207
+ self.log(ERROR, "unlink: %s", e)
1208
+ self.fail(107)
1209
+ self.delete_parents_recursive(name)
1210
+
1211
+ def delete_parents_recursive(self, name: str) -> None: # internal
1212
+ try:
1213
+ dirname = os.path.dirname(name)
1214
+ if not os.listdir(dirname):
1215
+ os.rmdir(dirname)
1216
+ self.delete_parents_recursive(dirname)
1217
+ except OSError as e:
1218
+ self.log(DEBUG, "delete_parents_recursive: %s", e)
1219
+
1220
+ def update_mru(self) -> None:
1221
+ now = time.time()
1222
+ link_to_mru = self.pcache_dst_dir + "mru"
1223
+ if os.path.exists(link_to_mru):
1224
+ link = os.readlink(link_to_mru)
1225
+ self.delete_file_and_parents(link)
1226
+
1227
+ try:
1228
+ os.unlink(link_to_mru)
1229
+ except OSError as e:
1230
+ if e.errno != errno.ENOENT:
1231
+ self.log(ERROR, "unlink: %s", e)
1232
+ self.fail(108)
1233
+
1234
+ mru_dir = self.pcache_dir + "MRU/" + time.strftime("%Y/%m/%d/%H/%M/",
1235
+ time.localtime(now))
1236
+
1237
+ self.mkdir_p(mru_dir)
1238
+
1239
+ # getting symlink
1240
+ name = "%.3f" % (now % 60)
1241
+ ext = 0
1242
+ while True:
1243
+ if ext:
1244
+ link_from_mru = "%s%s-%s" % (mru_dir, name, ext)
1245
+ else:
1246
+ link_from_mru = "%s%s" % (mru_dir, name)
1247
+
1248
+ try:
1249
+ os.symlink(self.pcache_dst_dir, link_from_mru)
1250
+ break
1251
+ except OSError as e:
1252
+ if e.errno == errno.EEXIST:
1253
+ ext += 1 # add an extension & retry if file exists
1254
+ continue
1255
+ else:
1256
+ self.log(ERROR, "symlink: %s %s", e, link_from_mru)
1257
+ self.fail(109)
1258
+
1259
+ while True:
1260
+ try:
1261
+ os.symlink(link_from_mru, link_to_mru)
1262
+ break
1263
+ except OSError as e:
1264
+ if e.errno == errno.EEXIST:
1265
+ try:
1266
+ os.unlink(link_to_mru)
1267
+ except OSError as e:
1268
+ if e.errno != errno.ENOENT:
1269
+ self.log(ERROR, "unlink: %s %s", e, link_to_mru)
1270
+ self.fail(109)
1271
+ else:
1272
+ self.log(ERROR, "symlink: %s %s", e, link_from_mru)
1273
+ self.fail(109)
1274
+
1275
+ def cleanup_failed_transfer(self) -> None:
1276
+ try:
1277
+ os.unlink(self.pcache_dir + 'xfer')
1278
+ except:
1279
+ pass
1280
+
1281
+ def empty_dir(self, d: str) -> None:
1282
+ status = None
1283
+ bytes_deleted = 0
1284
+ for name in os.listdir(d):
1285
+ size = 0
1286
+ fullname = os.path.join(d, name)
1287
+ if name == "data":
1288
+ try:
1289
+ size = os.stat(fullname).st_size
1290
+ except OSError as e:
1291
+ if e.errno != errno.ENOENT:
1292
+ self.log(WARN, "stat: %s", e)
1293
+ elif name == "guid":
1294
+ try:
1295
+ guid = open(fullname).read().strip()
1296
+ self.deleted_guids.append(guid)
1297
+ except:
1298
+ pass # XXXX
1299
+ elif name == "mru" and os.path.islink(fullname):
1300
+ try:
1301
+ mru_file = os.readlink(fullname)
1302
+ os.unlink(fullname)
1303
+ self.delete_file_and_parents(mru_file)
1304
+ except OSError as e:
1305
+ if e.errno != errno.ENOENT:
1306
+ self.log(WARN, "empty_dir: %s", e)
1307
+ try:
1308
+ if self.debug:
1309
+ print(("UNLINK %s", fullname))
1310
+ os.unlink(fullname)
1311
+ bytes_deleted += size
1312
+ except OSError as e:
1313
+ if e.errno != errno.ENOENT:
1314
+ self.log(WARN, "empty_dir2: %s", e)
1315
+ # self.fail()
1316
+ self.update_cache_size(-bytes_deleted)
1317
+ self.delete_parents_recursive(d)
1318
+ return status
1319
+
1320
+ def chmod(self, path: str, mode: int) -> None:
1321
+ try:
1322
+ os.chmod(path, mode)
1323
+ except OSError as e:
1324
+ if e.errno != errno.EPERM: # Cannot chmod files we don't own!
1325
+ self.log(ERROR, "chmod %s %s", path, e)
1326
+
1327
+ def mkdir_p(self, d: str, mode: int = 0o777) -> Optional[int]:
1328
+ # Thread-safe
1329
+ try:
1330
+ os.makedirs(d, mode)
1331
+ return 0
1332
+ except OSError as e:
1333
+ if e.errno == errno.EEXIST:
1334
+ pass
1335
+ else:
1336
+ # Don't use log here, log dir may not exist
1337
+ sys.stderr.write("%s\n" % str(e))
1338
+ return e.errno
1339
+
1340
+ def log(self, level: str, msg: str, *args) -> None:
1341
+
1342
+ # Disable all logging
1343
+ if (self.quiet):
1344
+ return
1345
+
1346
+ if ((level == DEBUG) and (not self.debug)):
1347
+ return
1348
+
1349
+ msg = "%s %s %s %s %s\n" % (time.strftime("%F %H:%M:%S"),
1350
+ sessid,
1351
+ self.hostname,
1352
+ level,
1353
+ str(msg) % args)
1354
+
1355
+ try:
1356
+ f = open(self.log_file, "a+", 0o666)
1357
+ f.write(msg)
1358
+ f.close()
1359
+
1360
+ except Exception as e:
1361
+ sys.stderr.write("%s\n" % str(e))
1362
+ sys.stderr.write(msg)
1363
+ sys.stderr.flush()
1364
+
1365
+ if (self.debug or self.verbose or (level == ERROR)):
1366
+ sys.stderr.write(msg)
1367
+ sys.stderr.flush()
1368
+
1369
+ def fail(self, errcode: int = 1) -> None:
1370
+ self.unlock_all()
1371
+ sys.exit(errcode)
1372
+
1373
+ ##################################################################################
1374
+
1375
+ # pCache exit_status will be
1376
+ #
1377
+ # 0 - File was transferred into cache and is ready
1378
+ # 1 - File is cached and ready to use
1379
+ # 2 - File was transferred but with a retry (copy_status has the retry count)
1380
+ # 3 - Transfer command failed (copy_status has the transfer return code)
1381
+ # 4 - Transfer command timed out
1382
+ #
1383
+ # 100 - Usage error
1384
+ # 101 - Cache directory does not exist
1385
+ # 102 - Cache hard link error
1386
+ # 103 - Cache destination mkdir error
1387
+ # 104 - Cache rename error
1388
+ # 105 - Cache locking error
1389
+ # 106 - Cache file locking error
1390
+ # 107 - Cache cleanup error
1391
+ # 108 - Cache MRU update error
1392
+ # 109 - Cache MRU link error
1393
+ # 500 - Is file in pcache? No other action
1394
+
1395
+
1396
+ if (__name__ == "__main__"):
1397
+
1398
+ # Load pCache
1399
+ p = Pcache()
1400
+
1401
+ # Save the passed arguments
1402
+ args = sys.argv
1403
+
1404
+ # Find the file
1405
+ exit_status, copy_status = p.main(args)
1406
+
1407
+ # Take us home percy...
1408
+ sys.exit(exit_status)