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