vortex-nwp 2.0.0b1__py3-none-any.whl → 2.0.0b2__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.
- vortex/__init__.py +59 -45
- vortex/algo/__init__.py +3 -2
- vortex/algo/components.py +940 -614
- vortex/algo/mpitools.py +802 -497
- vortex/algo/serversynctools.py +34 -33
- vortex/config.py +19 -22
- vortex/data/__init__.py +9 -3
- vortex/data/abstractstores.py +593 -655
- vortex/data/containers.py +217 -162
- vortex/data/contents.py +65 -39
- vortex/data/executables.py +93 -102
- vortex/data/flow.py +40 -34
- vortex/data/geometries.py +228 -132
- vortex/data/handlers.py +428 -225
- vortex/data/outflow.py +15 -15
- vortex/data/providers.py +185 -163
- vortex/data/resources.py +48 -42
- vortex/data/stores.py +544 -413
- vortex/gloves.py +114 -87
- vortex/layout/__init__.py +1 -8
- vortex/layout/contexts.py +150 -84
- vortex/layout/dataflow.py +353 -202
- vortex/layout/monitor.py +264 -128
- vortex/nwp/__init__.py +5 -2
- vortex/nwp/algo/__init__.py +14 -5
- vortex/nwp/algo/assim.py +205 -151
- vortex/nwp/algo/clim.py +683 -517
- vortex/nwp/algo/coupling.py +447 -225
- vortex/nwp/algo/eda.py +437 -229
- vortex/nwp/algo/eps.py +403 -231
- vortex/nwp/algo/forecasts.py +420 -271
- vortex/nwp/algo/fpserver.py +683 -307
- vortex/nwp/algo/ifsnaming.py +205 -145
- vortex/nwp/algo/ifsroot.py +210 -122
- vortex/nwp/algo/monitoring.py +132 -76
- vortex/nwp/algo/mpitools.py +321 -191
- vortex/nwp/algo/odbtools.py +617 -353
- vortex/nwp/algo/oopsroot.py +449 -273
- vortex/nwp/algo/oopstests.py +90 -56
- vortex/nwp/algo/request.py +287 -206
- vortex/nwp/algo/stdpost.py +878 -522
- vortex/nwp/data/__init__.py +22 -4
- vortex/nwp/data/assim.py +125 -137
- vortex/nwp/data/boundaries.py +121 -68
- vortex/nwp/data/climfiles.py +193 -211
- vortex/nwp/data/configfiles.py +73 -69
- vortex/nwp/data/consts.py +426 -401
- vortex/nwp/data/ctpini.py +59 -43
- vortex/nwp/data/diagnostics.py +94 -66
- vortex/nwp/data/eda.py +50 -51
- vortex/nwp/data/eps.py +195 -146
- vortex/nwp/data/executables.py +440 -434
- vortex/nwp/data/fields.py +63 -48
- vortex/nwp/data/gridfiles.py +183 -111
- vortex/nwp/data/logs.py +250 -217
- vortex/nwp/data/modelstates.py +180 -151
- vortex/nwp/data/monitoring.py +72 -99
- vortex/nwp/data/namelists.py +254 -202
- vortex/nwp/data/obs.py +400 -308
- vortex/nwp/data/oopsexec.py +22 -20
- vortex/nwp/data/providers.py +90 -65
- vortex/nwp/data/query.py +71 -82
- vortex/nwp/data/stores.py +49 -36
- vortex/nwp/data/surfex.py +136 -137
- vortex/nwp/syntax/__init__.py +1 -1
- vortex/nwp/syntax/stdattrs.py +173 -111
- vortex/nwp/tools/__init__.py +2 -2
- vortex/nwp/tools/addons.py +22 -17
- vortex/nwp/tools/agt.py +24 -12
- vortex/nwp/tools/bdap.py +16 -5
- vortex/nwp/tools/bdcp.py +4 -1
- vortex/nwp/tools/bdm.py +3 -0
- vortex/nwp/tools/bdmp.py +14 -9
- vortex/nwp/tools/conftools.py +728 -378
- vortex/nwp/tools/drhook.py +12 -8
- vortex/nwp/tools/grib.py +65 -39
- vortex/nwp/tools/gribdiff.py +22 -17
- vortex/nwp/tools/ifstools.py +82 -42
- vortex/nwp/tools/igastuff.py +167 -143
- vortex/nwp/tools/mars.py +14 -2
- vortex/nwp/tools/odb.py +234 -125
- vortex/nwp/tools/partitioning.py +61 -37
- vortex/nwp/tools/satrad.py +27 -12
- vortex/nwp/util/async.py +83 -55
- vortex/nwp/util/beacon.py +10 -10
- vortex/nwp/util/diffpygram.py +174 -86
- vortex/nwp/util/ens.py +144 -63
- vortex/nwp/util/hooks.py +30 -19
- vortex/nwp/util/taskdeco.py +28 -24
- vortex/nwp/util/usepygram.py +278 -172
- vortex/nwp/util/usetnt.py +31 -17
- vortex/sessions.py +72 -39
- vortex/syntax/__init__.py +1 -1
- vortex/syntax/stdattrs.py +410 -171
- vortex/syntax/stddeco.py +31 -22
- vortex/toolbox.py +327 -192
- vortex/tools/__init__.py +11 -2
- vortex/tools/actions.py +125 -59
- vortex/tools/addons.py +111 -92
- vortex/tools/arm.py +42 -22
- vortex/tools/compression.py +72 -69
- vortex/tools/date.py +11 -4
- vortex/tools/delayedactions.py +242 -132
- vortex/tools/env.py +75 -47
- vortex/tools/folder.py +342 -171
- vortex/tools/grib.py +311 -149
- vortex/tools/lfi.py +423 -216
- vortex/tools/listings.py +109 -40
- vortex/tools/names.py +218 -156
- vortex/tools/net.py +632 -298
- vortex/tools/parallelism.py +93 -61
- vortex/tools/prestaging.py +55 -31
- vortex/tools/schedulers.py +172 -105
- vortex/tools/services.py +402 -333
- vortex/tools/storage.py +293 -358
- vortex/tools/surfex.py +24 -24
- vortex/tools/systems.py +1211 -631
- vortex/tools/targets.py +156 -100
- vortex/util/__init__.py +1 -1
- vortex/util/config.py +377 -327
- vortex/util/empty.py +2 -2
- vortex/util/helpers.py +56 -24
- vortex/util/introspection.py +18 -12
- vortex/util/iosponge.py +8 -4
- vortex/util/roles.py +4 -6
- vortex/util/storefunctions.py +39 -13
- vortex/util/structs.py +3 -3
- vortex/util/worker.py +29 -17
- vortex_nwp-2.0.0b2.dist-info/METADATA +66 -0
- vortex_nwp-2.0.0b2.dist-info/RECORD +142 -0
- {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.0.0b2.dist-info}/WHEEL +1 -1
- vortex/layout/appconf.py +0 -109
- vortex/layout/jobs.py +0 -1276
- vortex/layout/nodes.py +0 -1424
- vortex/layout/subjobs.py +0 -464
- vortex_nwp-2.0.0b1.dist-info/METADATA +0 -50
- vortex_nwp-2.0.0b1.dist-info/RECORD +0 -146
- {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.0.0b2.dist-info}/LICENSE +0 -0
- {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.0.0b2.dist-info}/top_level.txt +0 -0
vortex/tools/net.py
CHANGED
|
@@ -44,28 +44,28 @@ def uriparse(uristring):
|
|
|
44
44
|
* username
|
|
45
45
|
* password
|
|
46
46
|
"""
|
|
47
|
-
(realscheme, other) = uristring.split(
|
|
48
|
-
rp = urlparse.urlparse(
|
|
47
|
+
(realscheme, other) = uristring.split(":", 1)
|
|
48
|
+
rp = urlparse.urlparse("http:" + other)
|
|
49
49
|
uridict = rp._asdict()
|
|
50
|
-
netloc = uridict[
|
|
51
|
-
hostport = netloc.pop().split(
|
|
52
|
-
uridict[
|
|
50
|
+
netloc = uridict["netloc"].split("@", 1)
|
|
51
|
+
hostport = netloc.pop().split(":")
|
|
52
|
+
uridict["netloc"] = hostport.pop(0)
|
|
53
53
|
if hostport:
|
|
54
|
-
uridict[
|
|
54
|
+
uridict["port"] = hostport.pop()
|
|
55
55
|
else:
|
|
56
|
-
uridict[
|
|
56
|
+
uridict["port"] = None
|
|
57
57
|
if netloc:
|
|
58
|
-
userpass = netloc.pop().split(
|
|
59
|
-
uridict[
|
|
58
|
+
userpass = netloc.pop().split(":")
|
|
59
|
+
uridict["username"] = userpass.pop(0)
|
|
60
60
|
if userpass:
|
|
61
|
-
uridict[
|
|
61
|
+
uridict["password"] = userpass.pop()
|
|
62
62
|
else:
|
|
63
|
-
uridict[
|
|
63
|
+
uridict["password"] = None
|
|
64
64
|
else:
|
|
65
|
-
uridict[
|
|
66
|
-
uridict[
|
|
67
|
-
uridict[
|
|
68
|
-
uridict[
|
|
65
|
+
uridict["username"] = None
|
|
66
|
+
uridict["password"] = None
|
|
67
|
+
uridict["scheme"] = realscheme
|
|
68
|
+
uridict["query"] = urlparse.parse_qs(uridict["query"])
|
|
69
69
|
return uridict
|
|
70
70
|
|
|
71
71
|
|
|
@@ -74,20 +74,24 @@ def uriunparse(uridesc):
|
|
|
74
74
|
return urlparse.urlunparse(uridesc)
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
def http_post_data(
|
|
77
|
+
def http_post_data(
|
|
78
|
+
url, data, ok_statuses=(), proxies=None, headers=None, verify=None
|
|
79
|
+
):
|
|
78
80
|
"""Make a http/https POST request, encoding **data**."""
|
|
79
81
|
if isinstance(proxies, (list, tuple)):
|
|
80
|
-
proxies = {scheme: proxies for scheme in (
|
|
82
|
+
proxies = {scheme: proxies for scheme in ("http", "https")}
|
|
81
83
|
# Try to use the requests package
|
|
82
84
|
try:
|
|
83
85
|
import requests
|
|
86
|
+
|
|
84
87
|
use_requests = True
|
|
85
88
|
except ImportError:
|
|
86
89
|
use_requests = False
|
|
87
90
|
# The modern way
|
|
88
91
|
if use_requests:
|
|
89
|
-
resp = requests.post(
|
|
90
|
-
|
|
92
|
+
resp = requests.post(
|
|
93
|
+
url=url, data=data, headers=headers, proxies=proxies, verify=verify
|
|
94
|
+
)
|
|
91
95
|
if ok_statuses:
|
|
92
96
|
is_ok = resp.status_code in ok_statuses
|
|
93
97
|
else:
|
|
@@ -95,16 +99,20 @@ def http_post_data(url, data, ok_statuses=(), proxies=None, headers=None, verify
|
|
|
95
99
|
return is_ok, resp.status_code, resp.headers, resp.text
|
|
96
100
|
else:
|
|
97
101
|
if not isinstance(data, bytes):
|
|
98
|
-
data = urlparse.urlencode(data).encode(
|
|
99
|
-
if uriparse(url)[
|
|
100
|
-
raise RuntimeError(
|
|
101
|
-
|
|
102
|
+
data = urlparse.urlencode(data).encode("utf-8")
|
|
103
|
+
if uriparse(url)["scheme"] == "https":
|
|
104
|
+
raise RuntimeError(
|
|
105
|
+
"HTTPS is not properly supported by urllib.request ({}).".format(
|
|
106
|
+
url
|
|
107
|
+
)
|
|
108
|
+
)
|
|
102
109
|
handlers = []
|
|
103
110
|
if isinstance(proxies, dict):
|
|
104
111
|
handlers.append(urlrequest.ProxyHandler(proxies))
|
|
105
|
-
opener = urlrequest.build_opener(*
|
|
106
|
-
req = urlrequest.Request(
|
|
107
|
-
|
|
112
|
+
opener = urlrequest.build_opener(*handlers)
|
|
113
|
+
req = urlrequest.Request(
|
|
114
|
+
url=url, data=data, headers={} if headers is None else headers
|
|
115
|
+
)
|
|
108
116
|
try:
|
|
109
117
|
req_f = opener.open(req)
|
|
110
118
|
except Exception as e:
|
|
@@ -116,7 +124,7 @@ def http_post_data(url, data, ok_statuses=(), proxies=None, headers=None, verify
|
|
|
116
124
|
try:
|
|
117
125
|
req_rc = req_f.getcode()
|
|
118
126
|
req_info = req_f.info()
|
|
119
|
-
req_data = req_f.read().decode(
|
|
127
|
+
req_data = req_f.read().decode("utf-8")
|
|
120
128
|
if ok_statuses:
|
|
121
129
|
return req_rc in ok_statuses, req_rc, req_info, req_data
|
|
122
130
|
else:
|
|
@@ -146,21 +154,21 @@ def netrc_lookup(logname, hostname, nrcfile=None):
|
|
|
146
154
|
auth = nrc.authenticators(hostname, login=logname)
|
|
147
155
|
if not auth:
|
|
148
156
|
# self.host may be a FQDN, try to guess only the hostname
|
|
149
|
-
auth = nrc.authenticators(hostname.split(
|
|
157
|
+
auth = nrc.authenticators(hostname.split(".")[0], login=logname)
|
|
150
158
|
# for backward compatibility: This might be removed one day
|
|
151
159
|
if not auth:
|
|
152
160
|
auth = nrc.authenticators(hostname)
|
|
153
161
|
if not auth:
|
|
154
162
|
# self.host may be a FQDN, try to guess only the hostname
|
|
155
|
-
auth = nrc.authenticators(hostname.split(
|
|
163
|
+
auth = nrc.authenticators(hostname.split(".")[0])
|
|
156
164
|
# End of backward compatibility section
|
|
157
165
|
if auth:
|
|
158
166
|
actual_logname = auth[0]
|
|
159
167
|
actual_pwd = auth[2]
|
|
160
168
|
else:
|
|
161
|
-
logger.warning(
|
|
169
|
+
logger.warning("netrc lookup failed (%s)", str(auth))
|
|
162
170
|
else:
|
|
163
|
-
logger.warning(
|
|
171
|
+
logger.warning("unable to fetch .netrc file")
|
|
164
172
|
return actual_logname, actual_pwd
|
|
165
173
|
|
|
166
174
|
|
|
@@ -170,7 +178,7 @@ class ExtendedFtplib:
|
|
|
170
178
|
It wraps the standard ftplib object to add or overwrite methods.
|
|
171
179
|
"""
|
|
172
180
|
|
|
173
|
-
def __init__(self, system, ftpobj, hostname=
|
|
181
|
+
def __init__(self, system, ftpobj, hostname="", port=DEFAULT_FTP_PORT):
|
|
174
182
|
"""
|
|
175
183
|
:param ~vortex.tools.systems.OSExtended system: The system object to work with
|
|
176
184
|
:param ftplib.FTP ftpobj: The FTP object to work with / to extend
|
|
@@ -178,7 +186,7 @@ class ExtendedFtplib:
|
|
|
178
186
|
self._system = system
|
|
179
187
|
self._ftplib = ftpobj
|
|
180
188
|
self._closed = True
|
|
181
|
-
self._logname =
|
|
189
|
+
self._logname = "not_logged_in"
|
|
182
190
|
self._created = datetime.now()
|
|
183
191
|
self._opened = None
|
|
184
192
|
self._deleted = None
|
|
@@ -200,8 +208,8 @@ class ExtendedFtplib:
|
|
|
200
208
|
Nicely formatted print, built as the concatenation
|
|
201
209
|
of the class full name and `logname` and `length` attributes.
|
|
202
210
|
"""
|
|
203
|
-
return
|
|
204
|
-
repr(self).rstrip(
|
|
211
|
+
return "{:s} | host={:s} logname={:s} since={!s}>".format(
|
|
212
|
+
repr(self).rstrip(">"),
|
|
205
213
|
self.host,
|
|
206
214
|
self.logname,
|
|
207
215
|
self.length,
|
|
@@ -212,10 +220,11 @@ class ExtendedFtplib:
|
|
|
212
220
|
"""Gateway to undefined method or attributes if present in ``_ftplib``."""
|
|
213
221
|
actualattr = getattr(self._ftplib, key)
|
|
214
222
|
if callable(actualattr):
|
|
223
|
+
|
|
215
224
|
def osproxy(*args, **kw):
|
|
216
225
|
cmd = [key]
|
|
217
226
|
cmd.extend(args)
|
|
218
|
-
cmd.extend([
|
|
227
|
+
cmd.extend(["{:s}={!s}".format(x, kw[x]) for x in kw.keys()])
|
|
219
228
|
self.stderr(*cmd)
|
|
220
229
|
return actualattr(*args, **kw)
|
|
221
230
|
|
|
@@ -234,7 +243,7 @@ class ExtendedFtplib:
|
|
|
234
243
|
|
|
235
244
|
def stderr(self, cmd, *args):
|
|
236
245
|
"""Proxy to local system's standard error."""
|
|
237
|
-
self.system.stderr(
|
|
246
|
+
self.system.stderr("ftp:" + cmd, *args)
|
|
238
247
|
|
|
239
248
|
@property
|
|
240
249
|
def closed(self):
|
|
@@ -254,12 +263,14 @@ class ExtendedFtplib:
|
|
|
254
263
|
topnow = datetime.now() if self._deleted is None else self._deleted
|
|
255
264
|
timelength = (topnow - self._opened).total_seconds()
|
|
256
265
|
except TypeError:
|
|
257
|
-
logger.warning(
|
|
266
|
+
logger.warning(
|
|
267
|
+
"Could not evaluate connexion length %s", repr(self)
|
|
268
|
+
)
|
|
258
269
|
return timelength
|
|
259
270
|
|
|
260
271
|
def close(self):
|
|
261
272
|
"""Proxy to ftplib :meth:`ftplib.FTP.close`."""
|
|
262
|
-
self.stderr(
|
|
273
|
+
self.stderr("close")
|
|
263
274
|
rc = True
|
|
264
275
|
if not self.closed:
|
|
265
276
|
rc = self._ftplib.close() or True
|
|
@@ -269,7 +280,7 @@ class ExtendedFtplib:
|
|
|
269
280
|
|
|
270
281
|
def login(self, *args):
|
|
271
282
|
"""Proxy to ftplib :meth:`ftplib.FTP.login`."""
|
|
272
|
-
self.stderr(
|
|
283
|
+
self.stderr("login", args[0])
|
|
273
284
|
self._logname = args[0]
|
|
274
285
|
# kept for debugging, but this exposes the user's password!
|
|
275
286
|
# logger.debug('FTP login <args:%s>', str(args))
|
|
@@ -279,48 +290,48 @@ class ExtendedFtplib:
|
|
|
279
290
|
self._deleted = None
|
|
280
291
|
self._opened = datetime.now()
|
|
281
292
|
else:
|
|
282
|
-
logger.warning(
|
|
293
|
+
logger.warning("FTP could not login <args:%s>", str(args))
|
|
283
294
|
return rc
|
|
284
295
|
|
|
285
296
|
def list(self, *args):
|
|
286
297
|
"""Returns standard directory listing from ftp protocol."""
|
|
287
|
-
self.stderr(
|
|
298
|
+
self.stderr("list", *args)
|
|
288
299
|
contents = []
|
|
289
|
-
self.retrlines(
|
|
300
|
+
self.retrlines("LIST", callback=contents.append)
|
|
290
301
|
return contents
|
|
291
302
|
|
|
292
303
|
def dir(self, *args):
|
|
293
304
|
"""Proxy to ftplib :meth:`ftplib.FTP.dir`."""
|
|
294
|
-
self.stderr(
|
|
305
|
+
self.stderr("dir", *args)
|
|
295
306
|
return self._ftplib.dir(*args)
|
|
296
307
|
|
|
297
308
|
def ls(self, *args):
|
|
298
309
|
"""Returns directory listing."""
|
|
299
|
-
self.stderr(
|
|
310
|
+
self.stderr("ls", *args)
|
|
300
311
|
return self.dir(*args)
|
|
301
312
|
|
|
302
313
|
ll = ls
|
|
303
314
|
|
|
304
315
|
def get(self, source, destination):
|
|
305
316
|
"""Retrieve a remote `destination` file to a local `source` file object."""
|
|
306
|
-
self.stderr(
|
|
317
|
+
self.stderr("get", source, destination)
|
|
307
318
|
if isinstance(destination, str):
|
|
308
319
|
self.system.filecocoon(destination)
|
|
309
|
-
target = open(destination,
|
|
320
|
+
target = open(destination, "wb")
|
|
310
321
|
xdestination = True
|
|
311
322
|
else:
|
|
312
323
|
target = destination
|
|
313
324
|
xdestination = False
|
|
314
|
-
logger.info(
|
|
325
|
+
logger.info("FTP <get:{:s}>".format(source))
|
|
315
326
|
rc = False
|
|
316
327
|
try:
|
|
317
|
-
self.retrbinary(
|
|
328
|
+
self.retrbinary("RETR " + source, target.write)
|
|
318
329
|
if xdestination:
|
|
319
330
|
target.seek(0, io.SEEK_END)
|
|
320
331
|
if self.size(source) == target.tell():
|
|
321
332
|
rc = True
|
|
322
333
|
else:
|
|
323
|
-
logger.error(
|
|
334
|
+
logger.error("FTP incomplete get %s", repr(source))
|
|
324
335
|
else:
|
|
325
336
|
rc = True
|
|
326
337
|
finally:
|
|
@@ -341,9 +352,9 @@ class ExtendedFtplib:
|
|
|
341
352
|
When `exact` is True, the size is checked against the size of the
|
|
342
353
|
destination, and a mismatch is considered a failure.
|
|
343
354
|
"""
|
|
344
|
-
self.stderr(
|
|
355
|
+
self.stderr("put", source, destination)
|
|
345
356
|
if isinstance(source, str):
|
|
346
|
-
inputsrc = open(source,
|
|
357
|
+
inputsrc = open(source, "rb")
|
|
347
358
|
xsource = True
|
|
348
359
|
else:
|
|
349
360
|
inputsrc = source
|
|
@@ -354,43 +365,60 @@ class ExtendedFtplib:
|
|
|
354
365
|
exact = True
|
|
355
366
|
inputsrc.seek(0)
|
|
356
367
|
except AttributeError:
|
|
357
|
-
logger.warning(
|
|
368
|
+
logger.warning("Could not rewind <source:%s>", str(source))
|
|
358
369
|
except OSError:
|
|
359
|
-
logger.debug(
|
|
370
|
+
logger.debug("Seek trouble <source:%s>", str(source))
|
|
360
371
|
|
|
361
372
|
self.rmkdir(destination)
|
|
362
373
|
try:
|
|
363
374
|
self.delete(destination)
|
|
364
|
-
logger.info(
|
|
375
|
+
logger.info("Replacing <file:%s>", str(destination))
|
|
365
376
|
except ftplib.error_perm:
|
|
366
|
-
logger.info(
|
|
367
|
-
except (
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
377
|
+
logger.info("Creating <file:%s>", str(destination))
|
|
378
|
+
except (
|
|
379
|
+
ValueError,
|
|
380
|
+
TypeError,
|
|
381
|
+
OSError,
|
|
382
|
+
ftplib.error_proto,
|
|
383
|
+
ftplib.error_reply,
|
|
384
|
+
ftplib.error_temp,
|
|
385
|
+
) as e:
|
|
386
|
+
logger.error(
|
|
387
|
+
"Serious delete trouble <file:%s> <error:%s>",
|
|
388
|
+
str(destination),
|
|
389
|
+
str(e),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
logger.info("FTP <put:%s>", str(destination))
|
|
373
393
|
rc = False
|
|
374
394
|
|
|
375
395
|
if size is not None:
|
|
376
396
|
try:
|
|
377
|
-
self.voidcmd(
|
|
397
|
+
self.voidcmd("ALLO {:d}".format(size))
|
|
378
398
|
except ftplib.error_perm:
|
|
379
399
|
pass
|
|
380
400
|
|
|
381
401
|
try:
|
|
382
|
-
self.storbinary(
|
|
402
|
+
self.storbinary("STOR " + destination, inputsrc)
|
|
383
403
|
if exact:
|
|
384
404
|
if self.size(destination) == size:
|
|
385
405
|
rc = True
|
|
386
406
|
else:
|
|
387
|
-
logger.error(
|
|
388
|
-
|
|
407
|
+
logger.error(
|
|
408
|
+
"FTP incomplete put %s (%d / %d bytes)",
|
|
409
|
+
repr(source),
|
|
410
|
+
self.size(destination),
|
|
411
|
+
size,
|
|
412
|
+
)
|
|
389
413
|
else:
|
|
390
414
|
rc = True
|
|
391
415
|
if self.size(destination) != size:
|
|
392
|
-
logger.info(
|
|
393
|
-
|
|
416
|
+
logger.info(
|
|
417
|
+
"FTP put %s: estimated %s bytes, real %s bytes",
|
|
418
|
+
repr(source),
|
|
419
|
+
str(size),
|
|
420
|
+
self.size(destination),
|
|
421
|
+
)
|
|
394
422
|
finally:
|
|
395
423
|
if xsource:
|
|
396
424
|
inputsrc.close()
|
|
@@ -398,29 +426,29 @@ class ExtendedFtplib:
|
|
|
398
426
|
|
|
399
427
|
def rmkdir(self, destination):
|
|
400
428
|
"""Recursive directory creation (mimics `mkdir -p`)."""
|
|
401
|
-
self.stderr(
|
|
429
|
+
self.stderr("rmkdir", destination)
|
|
402
430
|
origin = self.pwd()
|
|
403
|
-
if destination.startswith(
|
|
404
|
-
path_pre =
|
|
405
|
-
elif destination.startswith(
|
|
406
|
-
path_pre =
|
|
431
|
+
if destination.startswith("/"):
|
|
432
|
+
path_pre = "/"
|
|
433
|
+
elif destination.startswith("~"):
|
|
434
|
+
path_pre = ""
|
|
407
435
|
else:
|
|
408
|
-
path_pre = origin +
|
|
436
|
+
path_pre = origin + "/"
|
|
409
437
|
|
|
410
|
-
for subdir in self.system.path.dirname(destination).split(
|
|
438
|
+
for subdir in self.system.path.dirname(destination).split("/"):
|
|
411
439
|
current = path_pre + subdir
|
|
412
440
|
try:
|
|
413
441
|
self.cwd(current)
|
|
414
|
-
path_pre = current +
|
|
442
|
+
path_pre = current + "/"
|
|
415
443
|
except ftplib.error_perm:
|
|
416
|
-
self.stderr(
|
|
444
|
+
self.stderr("mkdir", current)
|
|
417
445
|
try:
|
|
418
446
|
self.mkd(current)
|
|
419
447
|
except ftplib.error_perm as errmkd:
|
|
420
|
-
if
|
|
448
|
+
if "File exists" not in str(errmkd):
|
|
421
449
|
raise
|
|
422
450
|
self.cwd(current)
|
|
423
|
-
path_pre = current +
|
|
451
|
+
path_pre = current + "/"
|
|
424
452
|
self.cwd(origin)
|
|
425
453
|
|
|
426
454
|
def cd(self, destination):
|
|
@@ -433,16 +461,16 @@ class ExtendedFtplib:
|
|
|
433
461
|
|
|
434
462
|
def mtime(self, filename):
|
|
435
463
|
"""Retrieve the modification time of a file."""
|
|
436
|
-
resp = self.sendcmd(
|
|
437
|
-
if resp[:3] ==
|
|
464
|
+
resp = self.sendcmd("MDTM " + filename)
|
|
465
|
+
if resp[:3] == "213":
|
|
438
466
|
s = resp[3:].strip().split()[-1]
|
|
439
467
|
return int(s)
|
|
440
468
|
|
|
441
469
|
def size(self, filename):
|
|
442
470
|
"""Retrieve the size of a file."""
|
|
443
471
|
# The SIZE command is defined in RFC-3659
|
|
444
|
-
resp = self.sendcmd(
|
|
445
|
-
if resp[:3] ==
|
|
472
|
+
resp = self.sendcmd("SIZE " + filename)
|
|
473
|
+
if resp[:3] == "213":
|
|
446
474
|
s = resp[3:].strip().split()[-1]
|
|
447
475
|
return int(s)
|
|
448
476
|
|
|
@@ -465,11 +493,23 @@ class StdFtp:
|
|
|
465
493
|
:class:`ExtendedFtplib` and :class:`ftplib.FTP` class).
|
|
466
494
|
"""
|
|
467
495
|
|
|
468
|
-
_PROXY_TYPES = (
|
|
469
|
-
|
|
470
|
-
_NO_AUTOLOGIN = (
|
|
471
|
-
|
|
472
|
-
|
|
496
|
+
_PROXY_TYPES = ("no-auth-logname-based",)
|
|
497
|
+
|
|
498
|
+
_NO_AUTOLOGIN = (
|
|
499
|
+
"set_debuglevel",
|
|
500
|
+
"connect",
|
|
501
|
+
"login",
|
|
502
|
+
"stderr",
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
def __init__(
|
|
506
|
+
self,
|
|
507
|
+
system,
|
|
508
|
+
hostname,
|
|
509
|
+
port=DEFAULT_FTP_PORT,
|
|
510
|
+
nrcfile=None,
|
|
511
|
+
ignoreproxy=False,
|
|
512
|
+
):
|
|
473
513
|
"""
|
|
474
514
|
:param ~vortex.tools.systems.OSExtended system: The system object to work with
|
|
475
515
|
:param str hostname: The remote host's network name
|
|
@@ -477,12 +517,18 @@ class StdFtp:
|
|
|
477
517
|
:param str nrcfile: The path to the .netrc file (if `None` the ~/.netrc default is used)
|
|
478
518
|
:param bool ignoreproxy: Forcibly ignore any proxy related environment variables
|
|
479
519
|
"""
|
|
480
|
-
logger.debug(
|
|
520
|
+
logger.debug("FTP init <host:%s>", hostname)
|
|
481
521
|
self._system = system
|
|
482
522
|
if ignoreproxy:
|
|
483
|
-
self._proxy_host, self._proxy_port, self._proxy_type = (
|
|
523
|
+
self._proxy_host, self._proxy_port, self._proxy_type = (
|
|
524
|
+
None,
|
|
525
|
+
None,
|
|
526
|
+
None,
|
|
527
|
+
)
|
|
484
528
|
else:
|
|
485
|
-
self._proxy_host, self._proxy_port, self._proxy_type =
|
|
529
|
+
self._proxy_host, self._proxy_port, self._proxy_type = (
|
|
530
|
+
self._proxy_init()
|
|
531
|
+
)
|
|
486
532
|
self._hostname = hostname
|
|
487
533
|
self._port = port
|
|
488
534
|
self._nrcfile = nrcfile
|
|
@@ -495,18 +541,22 @@ class StdFtp:
|
|
|
495
541
|
"""Return the proxy type, address and port."""
|
|
496
542
|
p_netloc = (None, None)
|
|
497
543
|
p_url = self.system.env.get(
|
|
498
|
-
|
|
544
|
+
"VORTEX_FTP_PROXY", self.system.env.get("FTP_PROXY", None)
|
|
499
545
|
)
|
|
500
546
|
if p_url:
|
|
501
|
-
p_netloc = p_url.split(
|
|
547
|
+
p_netloc = p_url.split(":", 1)
|
|
502
548
|
if len(p_netloc) == 1:
|
|
503
549
|
p_netloc.append(DEFAULT_FTP_PORT)
|
|
504
550
|
else:
|
|
505
551
|
p_netloc[1] = int(p_netloc[1])
|
|
506
|
-
p_type = self.system.env.get(
|
|
552
|
+
p_type = self.system.env.get(
|
|
553
|
+
"VORTEX_FTP_PROXY_TYPE", self._PROXY_TYPES[0]
|
|
554
|
+
)
|
|
507
555
|
if p_type not in self._PROXY_TYPES:
|
|
508
|
-
raise ValueError(
|
|
509
|
-
|
|
556
|
+
raise ValueError(
|
|
557
|
+
"Incorrect value for the VORTEX_FTP_PROXY_TYPE "
|
|
558
|
+
+ "environment variable (got: {:s})".format(p_type)
|
|
559
|
+
)
|
|
510
560
|
return p_netloc[0], p_netloc[1], p_type
|
|
511
561
|
|
|
512
562
|
def _extended_ftp_host_and_port(self):
|
|
@@ -523,9 +573,9 @@ class StdFtp:
|
|
|
523
573
|
It is created on-demand.
|
|
524
574
|
"""
|
|
525
575
|
if self._internal_ftp is None:
|
|
526
|
-
self._internal_ftp = ExtendedFtplib(
|
|
527
|
-
|
|
528
|
-
|
|
576
|
+
self._internal_ftp = ExtendedFtplib(
|
|
577
|
+
self._system, ftplib.FTP(), *self._extended_ftp_host_and_port()
|
|
578
|
+
)
|
|
529
579
|
return self._internal_ftp
|
|
530
580
|
|
|
531
581
|
_loginlike_extended_ftp = _extended_ftp
|
|
@@ -559,7 +609,7 @@ class StdFtp:
|
|
|
559
609
|
@property
|
|
560
610
|
def proxy(self):
|
|
561
611
|
if self._proxy_host:
|
|
562
|
-
return
|
|
612
|
+
return "{0._proxy_host}:{0._proxy_port}".format(self)
|
|
563
613
|
else:
|
|
564
614
|
return None
|
|
565
615
|
|
|
@@ -570,15 +620,20 @@ class StdFtp:
|
|
|
570
620
|
|
|
571
621
|
def netpath(self, remote):
|
|
572
622
|
"""The complete qualified net path of the remote resource."""
|
|
573
|
-
return
|
|
574
|
-
|
|
623
|
+
return "{:s}@{:s}:{:s}".format(
|
|
624
|
+
self.logname if self.logname is not None else "unknown",
|
|
625
|
+
self.host,
|
|
626
|
+
remote,
|
|
627
|
+
)
|
|
575
628
|
|
|
576
629
|
def delayedlogin(self):
|
|
577
630
|
"""Login to the FTP server (if it was not already done)."""
|
|
578
631
|
if self._loginlike_extended_ftp.closed:
|
|
579
632
|
if self._logname is None or self.cached_pwd is None:
|
|
580
|
-
logger.warning(
|
|
581
|
-
|
|
633
|
+
logger.warning(
|
|
634
|
+
"FTP logname/password must be set first. Use the fastlogin method."
|
|
635
|
+
)
|
|
636
|
+
raise RuntimeError("logname/password were not provided")
|
|
582
637
|
return self.login(self._logname, self.cached_pwd)
|
|
583
638
|
else:
|
|
584
639
|
return True
|
|
@@ -588,11 +643,15 @@ class StdFtp:
|
|
|
588
643
|
if logname and password:
|
|
589
644
|
bare_logname = logname
|
|
590
645
|
else:
|
|
591
|
-
bare_logname, password = netrc_lookup(
|
|
646
|
+
bare_logname, password = netrc_lookup(
|
|
647
|
+
logname, self.host, nrcfile=self._nrcfile
|
|
648
|
+
)
|
|
592
649
|
logname = bare_logname
|
|
593
650
|
if logname and self._proxy_host:
|
|
594
651
|
if self._proxy_type == self._PROXY_TYPES[0]:
|
|
595
|
-
logname =
|
|
652
|
+
logname = "{0:s}@{1.host:s}:{1.port:d}".format(
|
|
653
|
+
bare_logname, self
|
|
654
|
+
)
|
|
596
655
|
if logname:
|
|
597
656
|
return logname, password, bare_logname
|
|
598
657
|
else:
|
|
@@ -614,7 +673,9 @@ class StdFtp:
|
|
|
614
673
|
necessary).
|
|
615
674
|
"""
|
|
616
675
|
rc = False
|
|
617
|
-
p_logname, p_password, p_barelogname = self._process_logname_password(
|
|
676
|
+
p_logname, p_password, p_barelogname = self._process_logname_password(
|
|
677
|
+
logname, password
|
|
678
|
+
)
|
|
618
679
|
if p_logname and p_password:
|
|
619
680
|
self._logname = p_logname
|
|
620
681
|
self._cached_pwd = p_password
|
|
@@ -627,7 +688,7 @@ class StdFtp:
|
|
|
627
688
|
|
|
628
689
|
def _extended_ftp_lookup_check(self, key):
|
|
629
690
|
"""Are we allowed to look for *key* in the `self._extended_ftp` object ?"""
|
|
630
|
-
return not key.startswith(
|
|
691
|
+
return not key.startswith("_")
|
|
631
692
|
|
|
632
693
|
def _extended_ftp_lookup(self, key):
|
|
633
694
|
"""Look if the `self._extended_ftp` object can provide a given method.
|
|
@@ -637,6 +698,7 @@ class StdFtp:
|
|
|
637
698
|
"""
|
|
638
699
|
actualattr = getattr(self._extended_ftp, key)
|
|
639
700
|
if callable(actualattr):
|
|
701
|
+
|
|
640
702
|
def osproxy(*args, **kw):
|
|
641
703
|
# For most of the native commands, we want autologin to be performed
|
|
642
704
|
if key not in self._NO_AUTOLOGIN:
|
|
@@ -679,9 +741,20 @@ class AutoRetriesFtp(StdFtp):
|
|
|
679
741
|
the retry-on-failure capability.
|
|
680
742
|
"""
|
|
681
743
|
|
|
682
|
-
def __init__(
|
|
683
|
-
|
|
684
|
-
|
|
744
|
+
def __init__(
|
|
745
|
+
self,
|
|
746
|
+
system,
|
|
747
|
+
hostname,
|
|
748
|
+
port=DEFAULT_FTP_PORT,
|
|
749
|
+
nrcfile=None,
|
|
750
|
+
ignoreproxy=False,
|
|
751
|
+
retrycount_default=6,
|
|
752
|
+
retrycount_connect=8,
|
|
753
|
+
retrycount_login=3,
|
|
754
|
+
retrydelay_default=15,
|
|
755
|
+
retrydelay_connect=15,
|
|
756
|
+
retrydelay_login=10,
|
|
757
|
+
):
|
|
685
758
|
"""
|
|
686
759
|
:param ~vortex.tools.systems.OSExtended system: The system object to work with.
|
|
687
760
|
:param str hostname: The remote host's network name.
|
|
@@ -695,7 +768,7 @@ class AutoRetriesFtp(StdFtp):
|
|
|
695
768
|
:param int retrycount_login: The maximum number of retries when login in to the FTP server.
|
|
696
769
|
:param int retrydelay_login: The delay (in seconds) between two retries when login in to the FTP server.
|
|
697
770
|
"""
|
|
698
|
-
logger.debug(
|
|
771
|
+
logger.debug("AutoRetries FTP init <host:%s>", hostname)
|
|
699
772
|
# Retry stuff
|
|
700
773
|
self.retrycount_default = retrycount_default
|
|
701
774
|
self.retrycount_connect = retrycount_connect
|
|
@@ -706,11 +779,17 @@ class AutoRetriesFtp(StdFtp):
|
|
|
706
779
|
# Reset everything
|
|
707
780
|
self._initialise()
|
|
708
781
|
# Finalise
|
|
709
|
-
super().__init__(
|
|
782
|
+
super().__init__(
|
|
783
|
+
system,
|
|
784
|
+
hostname,
|
|
785
|
+
port=port,
|
|
786
|
+
nrcfile=nrcfile,
|
|
787
|
+
ignoreproxy=ignoreproxy,
|
|
788
|
+
)
|
|
710
789
|
|
|
711
790
|
def _initialise(self):
|
|
712
791
|
self._internal_retries_max = None
|
|
713
|
-
self._cwd =
|
|
792
|
+
self._cwd = ""
|
|
714
793
|
self._autodestroy()
|
|
715
794
|
|
|
716
795
|
def _autodestroy(self):
|
|
@@ -720,25 +799,39 @@ class AutoRetriesFtp(StdFtp):
|
|
|
720
799
|
def _get_extended_ftp(self, retrycount, retrydelay, exceptions_extras):
|
|
721
800
|
"""Delay the call to 'connect' as much as possible."""
|
|
722
801
|
if self._internal_ftp is None:
|
|
723
|
-
eftplib = self._retry_wrapped_callable(
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
802
|
+
eftplib = self._retry_wrapped_callable(
|
|
803
|
+
ExtendedFtplib,
|
|
804
|
+
retrycount=retrycount,
|
|
805
|
+
retrydelay=retrydelay,
|
|
806
|
+
exceptions_extras=exceptions_extras,
|
|
807
|
+
)
|
|
808
|
+
self._internal_ftp = eftplib(
|
|
809
|
+
self._system, ftplib.FTP(), *self._extended_ftp_host_and_port()
|
|
810
|
+
)
|
|
729
811
|
return self._internal_ftp
|
|
730
812
|
|
|
731
813
|
@property
|
|
732
814
|
def _extended_ftp(self):
|
|
733
815
|
"""Delay the call to 'connect' as much as possible."""
|
|
734
|
-
return self._get_extended_ftp(
|
|
735
|
-
|
|
816
|
+
return self._get_extended_ftp(
|
|
817
|
+
self.retrycount_connect,
|
|
818
|
+
self.retrydelay_connect,
|
|
819
|
+
[
|
|
820
|
+
socket.timeout,
|
|
821
|
+
],
|
|
822
|
+
)
|
|
736
823
|
|
|
737
824
|
@property
|
|
738
825
|
def _loginlike_extended_ftp(self):
|
|
739
826
|
"""Delay the call to 'connect' as much as possible."""
|
|
740
|
-
return self._get_extended_ftp(
|
|
741
|
-
|
|
827
|
+
return self._get_extended_ftp(
|
|
828
|
+
self.retrycount_login,
|
|
829
|
+
self.retrydelay_login,
|
|
830
|
+
[
|
|
831
|
+
ftplib.error_perm,
|
|
832
|
+
socket.error,
|
|
833
|
+
],
|
|
834
|
+
)
|
|
742
835
|
|
|
743
836
|
def _actual_login(self, *args):
|
|
744
837
|
"""Actually log in + save logname/password + correct the cwd if needed."""
|
|
@@ -750,22 +843,23 @@ class AutoRetriesFtp(StdFtp):
|
|
|
750
843
|
self._cached_pwd = args[1]
|
|
751
844
|
if rc and self._cwd:
|
|
752
845
|
cocoondir = self._cwd
|
|
753
|
-
self._cwd =
|
|
846
|
+
self._cwd = ""
|
|
754
847
|
rc = rc and self.cwd(cocoondir)
|
|
755
848
|
return rc
|
|
756
849
|
|
|
757
850
|
def login(self, *args):
|
|
758
851
|
"""Proxy to ftplib :meth:`ftplib.FTP.login`."""
|
|
759
|
-
wftplogin = self._retry_wrapped_callable(
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
852
|
+
wftplogin = self._retry_wrapped_callable(
|
|
853
|
+
self._actual_login,
|
|
854
|
+
retrycount=self.retrycount_login,
|
|
855
|
+
retrydelay=self.retrydelay_login,
|
|
856
|
+
exceptions_extras=[ftplib.error_perm, socket.error, EOFError],
|
|
857
|
+
)
|
|
765
858
|
return wftplogin(*args)
|
|
766
859
|
|
|
767
|
-
def _retry_wrapped_callable(
|
|
768
|
-
|
|
860
|
+
def _retry_wrapped_callable(
|
|
861
|
+
self, func, retrycount=None, retrydelay=None, exceptions_extras=None
|
|
862
|
+
):
|
|
769
863
|
"""
|
|
770
864
|
Wraps the *func* function in order to implement a retry on failure
|
|
771
865
|
mechanism.
|
|
@@ -782,7 +876,11 @@ class AutoRetriesFtp(StdFtp):
|
|
|
782
876
|
"""
|
|
783
877
|
actual_rcount = retrycount or self.retrycount_default
|
|
784
878
|
actual_rdelay = retrydelay or self.retrydelay_default
|
|
785
|
-
actual_exc = [
|
|
879
|
+
actual_exc = [
|
|
880
|
+
ftplib.error_temp,
|
|
881
|
+
ftplib.error_proto,
|
|
882
|
+
ftplib.error_reply,
|
|
883
|
+
]
|
|
786
884
|
if exceptions_extras:
|
|
787
885
|
actual_exc.extend(exceptions_extras)
|
|
788
886
|
actual_exc = tuple(actual_exc)
|
|
@@ -791,22 +889,29 @@ class AutoRetriesFtp(StdFtp):
|
|
|
791
889
|
globalcounter_driver = self._internal_retries_max is None
|
|
792
890
|
if globalcounter_driver:
|
|
793
891
|
self._internal_retries_max = actual_rcount
|
|
794
|
-
retriesleft = max(
|
|
892
|
+
retriesleft = max(
|
|
893
|
+
min(self._internal_retries_max, actual_rcount), 1
|
|
894
|
+
)
|
|
795
895
|
try:
|
|
796
896
|
while retriesleft:
|
|
797
897
|
try:
|
|
798
898
|
return func(*args, **kw)
|
|
799
899
|
except actual_exc as e:
|
|
800
|
-
logger.warning(
|
|
801
|
-
|
|
900
|
+
logger.warning(
|
|
901
|
+
'An error occurred (in "%s"): %s', func.__name__, e
|
|
902
|
+
)
|
|
802
903
|
retriesleft -= 1
|
|
803
904
|
self._internal_retries_max -= 1
|
|
804
905
|
if not retriesleft:
|
|
805
|
-
logger.warning(
|
|
806
|
-
|
|
906
|
+
logger.warning(
|
|
907
|
+
"The maximum number of retries (%d) was reached.",
|
|
908
|
+
actual_rcount,
|
|
909
|
+
)
|
|
807
910
|
raise
|
|
808
|
-
logger.warning(
|
|
809
|
-
|
|
911
|
+
logger.warning(
|
|
912
|
+
"Sleeping %d sec. before the next attempt.",
|
|
913
|
+
actual_rdelay,
|
|
914
|
+
)
|
|
810
915
|
self._autodestroy()
|
|
811
916
|
self.system.sleep(actual_rdelay)
|
|
812
917
|
finally:
|
|
@@ -825,15 +930,19 @@ class AutoRetriesFtp(StdFtp):
|
|
|
825
930
|
attr = self._extended_ftp_lookup(key)
|
|
826
931
|
if callable(attr):
|
|
827
932
|
if key not in self._NO_AUTOLOGIN:
|
|
828
|
-
attr = self._retry_wrapped_callable(
|
|
829
|
-
|
|
933
|
+
attr = self._retry_wrapped_callable(
|
|
934
|
+
attr,
|
|
935
|
+
exceptions_extras=[
|
|
936
|
+
socket.error,
|
|
937
|
+
],
|
|
938
|
+
)
|
|
830
939
|
setattr(self, key, attr)
|
|
831
940
|
return attr
|
|
832
941
|
raise AttributeError(key)
|
|
833
942
|
|
|
834
943
|
def cwd(self, pathname):
|
|
835
944
|
"""Change the current directory to the *pathname* directory."""
|
|
836
|
-
todo = self._retry_wrapped_callable(self._extended_ftp_lookup(
|
|
945
|
+
todo = self._retry_wrapped_callable(self._extended_ftp_lookup("cwd"))
|
|
837
946
|
rc = todo(pathname)
|
|
838
947
|
if rc:
|
|
839
948
|
if self.system.path.isabs(pathname):
|
|
@@ -850,7 +959,9 @@ class AutoRetriesFtp(StdFtp):
|
|
|
850
959
|
def quit(self):
|
|
851
960
|
"""Quit the current ftp session politely."""
|
|
852
961
|
try:
|
|
853
|
-
rc = self._retry_wrapped_callable(
|
|
962
|
+
rc = self._retry_wrapped_callable(
|
|
963
|
+
self._extended_ftp_lookup("quit")
|
|
964
|
+
)()
|
|
854
965
|
finally:
|
|
855
966
|
self._initialise()
|
|
856
967
|
return rc
|
|
@@ -885,7 +996,7 @@ class ResetableAutoRetriesFtp(AutoRetriesFtp):
|
|
|
885
996
|
def reset(self):
|
|
886
997
|
"""Reset the current working directory to its initial value."""
|
|
887
998
|
if self._initialpath is not None and self._cwd:
|
|
888
|
-
self._cwd =
|
|
999
|
+
self._cwd = ""
|
|
889
1000
|
return self.cwd(self._initialpath)
|
|
890
1001
|
|
|
891
1002
|
|
|
@@ -904,7 +1015,9 @@ class PooledResetableAutoRetriesFtp(ResetableAutoRetriesFtp):
|
|
|
904
1015
|
"""
|
|
905
1016
|
self._pool = pool
|
|
906
1017
|
super().__init__(*kargs, **kwargs)
|
|
907
|
-
logger.debug(
|
|
1018
|
+
logger.debug(
|
|
1019
|
+
"Pooled FTP init <host:%s> <pool:%s>", self.host, repr(pool)
|
|
1020
|
+
)
|
|
908
1021
|
|
|
909
1022
|
def forceclose(self):
|
|
910
1023
|
"""Really quit the ftp session."""
|
|
@@ -965,40 +1078,60 @@ class FtpConnectionPool:
|
|
|
965
1078
|
|
|
966
1079
|
def __str__(self):
|
|
967
1080
|
"""Print a summary of the connection pool activity."""
|
|
968
|
-
out =
|
|
969
|
-
out +=
|
|
970
|
-
out +=
|
|
971
|
-
out +=
|
|
1081
|
+
out = "Current connection pool size: {:d}\n".format(self.poolsize)
|
|
1082
|
+
out += " # of created objects: {:d}\n".format(self._created)
|
|
1083
|
+
out += " # of re-used objects: {:d}\n".format(self._reused)
|
|
1084
|
+
out += " # of given back objects: {:d}\n".format(self._givenback)
|
|
972
1085
|
if self.poolsize:
|
|
973
|
-
out +=
|
|
1086
|
+
out += "\nDetailed list of current spare clients:\n"
|
|
974
1087
|
for ident, hpool in self._reusable.items():
|
|
975
1088
|
for client in hpool:
|
|
976
|
-
out +=
|
|
1089
|
+
out += " - {id[1]:s}@{id[0]:s}: {cl!r}\n".format(
|
|
1090
|
+
id=ident, cl=client
|
|
1091
|
+
)
|
|
977
1092
|
return out
|
|
978
1093
|
|
|
979
|
-
def deal(
|
|
1094
|
+
def deal(
|
|
1095
|
+
self,
|
|
1096
|
+
hostname,
|
|
1097
|
+
logname,
|
|
1098
|
+
port=DEFAULT_FTP_PORT,
|
|
1099
|
+
delayed=True,
|
|
1100
|
+
ignoreproxy=False,
|
|
1101
|
+
):
|
|
980
1102
|
"""Retrieve an FTP client for the *hostname*/*logname* pair."""
|
|
981
1103
|
p_logname, _ = netrc_lookup(logname, hostname, nrcfile=self._nrcfile)
|
|
982
1104
|
if self._reusable[(hostname, port, p_logname)]:
|
|
983
1105
|
ftpc = self._reusable[(hostname, port, p_logname)].pop()
|
|
984
1106
|
ftpc.reset()
|
|
985
|
-
logger.debug(
|
|
1107
|
+
logger.debug("Re-using a client: %s", repr(ftpc))
|
|
986
1108
|
if not delayed:
|
|
987
1109
|
# If requested, ensure that we are logged in
|
|
988
1110
|
ftpc.delayedlogin()
|
|
989
1111
|
self._reused += 1
|
|
990
1112
|
return ftpc
|
|
991
1113
|
else:
|
|
992
|
-
ftpc = self._FTPCLIENT_CLASS(
|
|
993
|
-
|
|
994
|
-
|
|
1114
|
+
ftpc = self._FTPCLIENT_CLASS(
|
|
1115
|
+
self,
|
|
1116
|
+
self._system,
|
|
1117
|
+
hostname,
|
|
1118
|
+
port=port,
|
|
1119
|
+
nrcfile=self._nrcfile,
|
|
1120
|
+
ignoreproxy=self._ignoreproxy,
|
|
1121
|
+
)
|
|
995
1122
|
rc = ftpc.fastlogin(p_logname, delayed=delayed)
|
|
996
1123
|
if rc:
|
|
997
|
-
logger.debug(
|
|
1124
|
+
logger.debug("Creating a new client: %s", repr(ftpc))
|
|
998
1125
|
self._created += 1
|
|
999
1126
|
return ftpc
|
|
1000
1127
|
else:
|
|
1001
|
-
logger.warning(
|
|
1128
|
+
logger.warning(
|
|
1129
|
+
"Could not login on %s:%d as %s [%s]",
|
|
1130
|
+
hostname,
|
|
1131
|
+
port,
|
|
1132
|
+
p_logname,
|
|
1133
|
+
str(rc),
|
|
1134
|
+
)
|
|
1002
1135
|
return None
|
|
1003
1136
|
|
|
1004
1137
|
def relinquishing(self, client):
|
|
@@ -1010,19 +1143,32 @@ class FtpConnectionPool:
|
|
|
1010
1143
|
its `close` method is called.
|
|
1011
1144
|
"""
|
|
1012
1145
|
assert isinstance(client, self._FTPCLIENT_CLASS)
|
|
1013
|
-
self._reusable[(client.host, client.port, client.logname)].append(
|
|
1146
|
+
self._reusable[(client.host, client.port, client.logname)].append(
|
|
1147
|
+
client
|
|
1148
|
+
)
|
|
1014
1149
|
self._givenback += 1
|
|
1015
|
-
logger.debug(
|
|
1016
|
-
|
|
1150
|
+
logger.debug(
|
|
1151
|
+
"Spare client for %s@%s:%d has been stored (poolsize=%d).",
|
|
1152
|
+
client.logname,
|
|
1153
|
+
client.host,
|
|
1154
|
+
client.port,
|
|
1155
|
+
self.poolsize,
|
|
1156
|
+
)
|
|
1017
1157
|
if self.poolsize >= self._REUSABLE_THRESHOLD:
|
|
1018
|
-
logger.warning(
|
|
1019
|
-
|
|
1158
|
+
logger.warning(
|
|
1159
|
+
"The FTP pool is too big ! (%d >= %d). Here are the details:\n%s",
|
|
1160
|
+
self.poolsize,
|
|
1161
|
+
self._REUSABLE_THRESHOLD,
|
|
1162
|
+
str(self),
|
|
1163
|
+
)
|
|
1020
1164
|
|
|
1021
1165
|
def clear(self):
|
|
1022
1166
|
"""Destroy all the spare FTP clients."""
|
|
1023
1167
|
for hpool in self._reusable.values():
|
|
1024
1168
|
for client in hpool:
|
|
1025
|
-
logger.debug(
|
|
1169
|
+
logger.debug(
|
|
1170
|
+
"Destroying client for %s@%s", client.logname, client.host
|
|
1171
|
+
)
|
|
1026
1172
|
client.forceclose()
|
|
1027
1173
|
hpool.clear()
|
|
1028
1174
|
|
|
@@ -1047,14 +1193,16 @@ class Ssh:
|
|
|
1047
1193
|
self._remote = hostname
|
|
1048
1194
|
|
|
1049
1195
|
target = sh.default_target
|
|
1050
|
-
self._sshcmd = target.get(key=
|
|
1051
|
-
self._scpcmd = target.get(key=
|
|
1196
|
+
self._sshcmd = target.get(key="services:sshcmd", default="ssh")
|
|
1197
|
+
self._scpcmd = target.get(key="services:scpcmd", default="scp")
|
|
1052
1198
|
self._sshopts = (
|
|
1053
|
-
target.get(key=
|
|
1054
|
-
(sshopts or
|
|
1199
|
+
target.get(key="services:sshopts", default="-x").split()
|
|
1200
|
+
+ (sshopts or "").split()
|
|
1201
|
+
)
|
|
1055
1202
|
self._scpopts = (
|
|
1056
|
-
target.get(key=
|
|
1057
|
-
(scpopts or
|
|
1203
|
+
target.get(key="services:scpopts", default="-Bp").split()
|
|
1204
|
+
+ (scpopts or "").split()
|
|
1205
|
+
)
|
|
1058
1206
|
|
|
1059
1207
|
@property
|
|
1060
1208
|
def sh(self):
|
|
@@ -1062,13 +1210,15 @@ class Ssh:
|
|
|
1062
1210
|
|
|
1063
1211
|
@property
|
|
1064
1212
|
def remote(self):
|
|
1065
|
-
return (
|
|
1213
|
+
return (
|
|
1214
|
+
"" if self._logname is None else self._logname + "@"
|
|
1215
|
+
) + self._remote
|
|
1066
1216
|
|
|
1067
1217
|
def check_ok(self):
|
|
1068
1218
|
"""Is the connexion ok ?"""
|
|
1069
|
-
return self.execute(
|
|
1219
|
+
return self.execute("true") is not False
|
|
1070
1220
|
|
|
1071
|
-
def execute(self, remote_command, sshopts=
|
|
1221
|
+
def execute(self, remote_command, sshopts=""):
|
|
1072
1222
|
"""Execute the command remotely.
|
|
1073
1223
|
|
|
1074
1224
|
Return the output of the command (list of lines), or False on error.
|
|
@@ -1083,12 +1233,24 @@ class Ssh:
|
|
|
1083
1233
|
myremote = self.remote
|
|
1084
1234
|
if myremote is None:
|
|
1085
1235
|
return False
|
|
1086
|
-
cmd = (
|
|
1087
|
-
|
|
1088
|
-
|
|
1236
|
+
cmd = (
|
|
1237
|
+
[
|
|
1238
|
+
self._sshcmd,
|
|
1239
|
+
]
|
|
1240
|
+
+ self._sshopts
|
|
1241
|
+
+ sshopts.split()
|
|
1242
|
+
+ [
|
|
1243
|
+
myremote,
|
|
1244
|
+
]
|
|
1245
|
+
+ [
|
|
1246
|
+
remote_command,
|
|
1247
|
+
]
|
|
1248
|
+
)
|
|
1089
1249
|
return self.sh.spawn(cmd, output=True, fatal=False)
|
|
1090
1250
|
|
|
1091
|
-
def background_execute(
|
|
1251
|
+
def background_execute(
|
|
1252
|
+
self, remote_command, sshopts="", stdout=None, stderr=None
|
|
1253
|
+
):
|
|
1092
1254
|
"""Execute the command remotely and return the object representing the ssh process.
|
|
1093
1255
|
|
|
1094
1256
|
Return a Popen object representing the ssh process. The user is reponsible
|
|
@@ -1097,9 +1259,19 @@ class Ssh:
|
|
|
1097
1259
|
myremote = self.remote
|
|
1098
1260
|
if myremote is None:
|
|
1099
1261
|
return False
|
|
1100
|
-
cmd = (
|
|
1101
|
-
|
|
1102
|
-
|
|
1262
|
+
cmd = (
|
|
1263
|
+
[
|
|
1264
|
+
self._sshcmd,
|
|
1265
|
+
]
|
|
1266
|
+
+ self._sshopts
|
|
1267
|
+
+ sshopts.split()
|
|
1268
|
+
+ [
|
|
1269
|
+
myremote,
|
|
1270
|
+
]
|
|
1271
|
+
+ [
|
|
1272
|
+
remote_command,
|
|
1273
|
+
]
|
|
1274
|
+
)
|
|
1103
1275
|
return self.sh.popen(cmd, stdout=stdout, stderr=stderr)
|
|
1104
1276
|
|
|
1105
1277
|
def cocoon(self, destination):
|
|
@@ -1108,14 +1280,18 @@ class Ssh:
|
|
|
1108
1280
|
Return ``False`` on failure.
|
|
1109
1281
|
"""
|
|
1110
1282
|
remote_dir = self.sh.path.dirname(destination)
|
|
1111
|
-
if remote_dir ==
|
|
1283
|
+
if remote_dir == "":
|
|
1112
1284
|
return True
|
|
1113
1285
|
logger.debug('Cocooning remote directory "%s"', remote_dir)
|
|
1114
1286
|
cmd = 'mkdir -p "{}"'.format(remote_dir)
|
|
1115
1287
|
rc = self.execute(cmd)
|
|
1116
1288
|
if not rc:
|
|
1117
|
-
logger.error(
|
|
1118
|
-
|
|
1289
|
+
logger.error(
|
|
1290
|
+
"Cannot cocoon on %s (user: %s) for %s",
|
|
1291
|
+
str(self._remote),
|
|
1292
|
+
str(self._logname),
|
|
1293
|
+
destination,
|
|
1294
|
+
)
|
|
1119
1295
|
return rc
|
|
1120
1296
|
|
|
1121
1297
|
def remove(self, target):
|
|
@@ -1128,33 +1304,43 @@ class Ssh:
|
|
|
1128
1304
|
cmd = 'rm -fr "{}"'.format(target)
|
|
1129
1305
|
rc = self.execute(cmd)
|
|
1130
1306
|
if not rc:
|
|
1131
|
-
logger.error(
|
|
1132
|
-
|
|
1307
|
+
logger.error(
|
|
1308
|
+
'Cannot remove from %s (user: %s) item "%s"',
|
|
1309
|
+
str(self._remote),
|
|
1310
|
+
str(self._logname),
|
|
1311
|
+
target,
|
|
1312
|
+
)
|
|
1133
1313
|
return rc
|
|
1134
1314
|
|
|
1135
1315
|
def _scp_putget_commons(self, source, destination):
|
|
1136
1316
|
"""Common checks on source and destination."""
|
|
1137
1317
|
if not isinstance(source, str):
|
|
1138
|
-
msg =
|
|
1318
|
+
msg = "Source is not a plain file path: {!r}".format(source)
|
|
1139
1319
|
raise TypeError(msg)
|
|
1140
1320
|
if not isinstance(destination, str):
|
|
1141
|
-
msg =
|
|
1321
|
+
msg = "Destination is not a plain file path: {!r}".format(
|
|
1322
|
+
destination
|
|
1323
|
+
)
|
|
1142
1324
|
raise TypeError(msg)
|
|
1143
1325
|
|
|
1144
1326
|
# avoid special cases
|
|
1145
|
-
if destination ==
|
|
1146
|
-
destination =
|
|
1327
|
+
if destination == "" or destination == ".":
|
|
1328
|
+
destination = "./"
|
|
1147
1329
|
else:
|
|
1148
|
-
if destination.endswith(
|
|
1149
|
-
destination +=
|
|
1150
|
-
if
|
|
1151
|
-
raise ValueError(
|
|
1152
|
-
|
|
1153
|
-
|
|
1330
|
+
if destination.endswith(".."):
|
|
1331
|
+
destination += "/"
|
|
1332
|
+
if "../" in destination:
|
|
1333
|
+
raise ValueError(
|
|
1334
|
+
'"../" is not allowed in the destination path'
|
|
1335
|
+
)
|
|
1336
|
+
if destination.endswith("/"):
|
|
1337
|
+
destination = self.sh.path.join(
|
|
1338
|
+
destination, self.sh.path.basename(source)
|
|
1339
|
+
)
|
|
1154
1340
|
|
|
1155
1341
|
return source, destination
|
|
1156
1342
|
|
|
1157
|
-
def scpput(self, source, destination, scpopts=
|
|
1343
|
+
def scpput(self, source, destination, scpopts=""):
|
|
1158
1344
|
r"""Send ``source`` to ``destination``.
|
|
1159
1345
|
|
|
1160
1346
|
- ``source`` is a single file or a directory, not a pattern (no '\*.grib').
|
|
@@ -1170,7 +1356,7 @@ class Ssh:
|
|
|
1170
1356
|
source, destination = self._scp_putget_commons(source, destination)
|
|
1171
1357
|
|
|
1172
1358
|
if not self.sh.path.exists(source):
|
|
1173
|
-
logger.error(
|
|
1359
|
+
logger.error("No such file or directory: %s", source)
|
|
1174
1360
|
return False
|
|
1175
1361
|
|
|
1176
1362
|
source = self.sh.path.realpath(source)
|
|
@@ -1186,25 +1372,29 @@ class Ssh:
|
|
|
1186
1372
|
return False
|
|
1187
1373
|
|
|
1188
1374
|
if self.sh.path.isdir(source):
|
|
1189
|
-
scpopts +=
|
|
1375
|
+
scpopts += " -r"
|
|
1190
1376
|
|
|
1191
|
-
if not self.remove(destination +
|
|
1377
|
+
if not self.remove(destination + ".tmp"):
|
|
1192
1378
|
return False
|
|
1193
1379
|
|
|
1194
1380
|
# transfer to a temporary place.
|
|
1195
1381
|
# when ``destination`` contains spaces, 1 round of quoting
|
|
1196
1382
|
# is necessary, to avoid an 'scp: ambiguous target' error.
|
|
1197
|
-
cmd = (
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1383
|
+
cmd = (
|
|
1384
|
+
[
|
|
1385
|
+
self._scpcmd,
|
|
1386
|
+
]
|
|
1387
|
+
+ self._scpopts
|
|
1388
|
+
+ scpopts.split()
|
|
1389
|
+
+ [source, myremote + ":" + shlex.quote(destination + ".tmp")]
|
|
1390
|
+
)
|
|
1201
1391
|
rc = self.sh.spawn(cmd, output=False, fatal=False)
|
|
1202
1392
|
if rc:
|
|
1203
1393
|
# success, rename the tmp
|
|
1204
1394
|
rc = self.execute('mv "{0}.tmp" "{0}"'.format(destination))
|
|
1205
1395
|
return rc
|
|
1206
1396
|
|
|
1207
|
-
def scpget(self, source, destination, scpopts=
|
|
1397
|
+
def scpget(self, source, destination, scpopts="", isadir=False):
|
|
1208
1398
|
r"""Send ``source`` to ``destination``.
|
|
1209
1399
|
|
|
1210
1400
|
- ``source`` is the remote name, not a pattern (no '\*.grib').
|
|
@@ -1229,19 +1419,23 @@ class Ssh:
|
|
|
1229
1419
|
if isadir:
|
|
1230
1420
|
if not self.sh.remove(destination):
|
|
1231
1421
|
return False
|
|
1232
|
-
scpopts +=
|
|
1422
|
+
scpopts += " -r"
|
|
1233
1423
|
|
|
1234
1424
|
# transfer to a temporary place.
|
|
1235
1425
|
# when ``source`` contains spaces, 1 round of quoting
|
|
1236
1426
|
# is necessary, to avoid an 'scp: ambiguous target' error.
|
|
1237
|
-
cmd = (
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1427
|
+
cmd = (
|
|
1428
|
+
[
|
|
1429
|
+
self._scpcmd,
|
|
1430
|
+
]
|
|
1431
|
+
+ self._scpopts
|
|
1432
|
+
+ scpopts.split()
|
|
1433
|
+
+ [myremote + ":" + shlex.quote(source), destination + ".tmp"]
|
|
1434
|
+
)
|
|
1241
1435
|
rc = self.sh.spawn(cmd, output=False, fatal=False)
|
|
1242
1436
|
if rc:
|
|
1243
1437
|
# success, rename the tmp
|
|
1244
|
-
rc = self.sh.move(destination +
|
|
1438
|
+
rc = self.sh.move(destination + ".tmp", destination)
|
|
1245
1439
|
return rc
|
|
1246
1440
|
|
|
1247
1441
|
def get_permissions(self, source):
|
|
@@ -1252,7 +1446,7 @@ class Ssh:
|
|
|
1252
1446
|
mode = self.sh.stat(source).st_mode
|
|
1253
1447
|
return stat.S_IMODE(mode)
|
|
1254
1448
|
|
|
1255
|
-
def scpput_stream(self, stream, destination, permissions=None, sshopts=
|
|
1449
|
+
def scpput_stream(self, stream, destination, permissions=None, sshopts=""):
|
|
1256
1450
|
"""Send the ``stream`` to the ``destination``.
|
|
1257
1451
|
|
|
1258
1452
|
- ``stream`` is a ``file`` (typically returned by open(),
|
|
@@ -1262,11 +1456,15 @@ class Ssh:
|
|
|
1262
1456
|
Return True for ok, False on error.
|
|
1263
1457
|
"""
|
|
1264
1458
|
if not isinstance(stream, io.IOBase):
|
|
1265
|
-
msg = "stream is a {}, should be a <type 'file'>".format(
|
|
1459
|
+
msg = "stream is a {}, should be a <type 'file'>".format(
|
|
1460
|
+
type(stream)
|
|
1461
|
+
)
|
|
1266
1462
|
raise TypeError(msg)
|
|
1267
1463
|
|
|
1268
1464
|
if not isinstance(destination, str):
|
|
1269
|
-
msg =
|
|
1465
|
+
msg = "Destination is not a plain file path: {!r}".format(
|
|
1466
|
+
destination
|
|
1467
|
+
)
|
|
1270
1468
|
raise TypeError(msg)
|
|
1271
1469
|
|
|
1272
1470
|
myremote = self.remote
|
|
@@ -1277,16 +1475,25 @@ class Ssh:
|
|
|
1277
1475
|
return False
|
|
1278
1476
|
|
|
1279
1477
|
# transfer to a tmp, rename and set permissions in one go
|
|
1280
|
-
remote_cmd =
|
|
1478
|
+
remote_cmd = "cat > {0}.tmp && mv {0}.tmp {0}".format(
|
|
1479
|
+
shlex.quote(destination)
|
|
1480
|
+
)
|
|
1281
1481
|
if permissions:
|
|
1282
|
-
remote_cmd +=
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1482
|
+
remote_cmd += " && chmod -v {:o} {}".format(
|
|
1483
|
+
permissions, shlex.quote(destination)
|
|
1484
|
+
)
|
|
1485
|
+
|
|
1486
|
+
cmd = (
|
|
1487
|
+
[
|
|
1488
|
+
self._sshcmd,
|
|
1489
|
+
]
|
|
1490
|
+
+ self._sshopts
|
|
1491
|
+
+ sshopts.split()
|
|
1492
|
+
+ [myremote, remote_cmd]
|
|
1493
|
+
)
|
|
1287
1494
|
return self.sh.spawn(cmd, stdin=stream, output=False, fatal=False)
|
|
1288
1495
|
|
|
1289
|
-
def scpget_stream(self, source, stream, sshopts=
|
|
1496
|
+
def scpget_stream(self, source, stream, sshopts=""):
|
|
1290
1497
|
"""Send the ``source`` to the ``stream``.
|
|
1291
1498
|
|
|
1292
1499
|
- ``source`` is the remote file name.
|
|
@@ -1296,11 +1503,13 @@ class Ssh:
|
|
|
1296
1503
|
Return True for ok, False on error.
|
|
1297
1504
|
"""
|
|
1298
1505
|
if not isinstance(stream, io.IOBase):
|
|
1299
|
-
msg = "stream is a {}, should be a <type 'file'>".format(
|
|
1506
|
+
msg = "stream is a {}, should be a <type 'file'>".format(
|
|
1507
|
+
type(stream)
|
|
1508
|
+
)
|
|
1300
1509
|
raise TypeError(msg)
|
|
1301
1510
|
|
|
1302
1511
|
if not isinstance(source, str):
|
|
1303
|
-
msg =
|
|
1512
|
+
msg = "Source is not a plain file path: {!r}".format(source)
|
|
1304
1513
|
raise TypeError(msg)
|
|
1305
1514
|
|
|
1306
1515
|
myremote = self.remote
|
|
@@ -1308,14 +1517,25 @@ class Ssh:
|
|
|
1308
1517
|
return False
|
|
1309
1518
|
|
|
1310
1519
|
# transfer to a tmp, rename and set permissions in one go
|
|
1311
|
-
remote_cmd =
|
|
1312
|
-
cmd = (
|
|
1313
|
-
|
|
1314
|
-
|
|
1520
|
+
remote_cmd = "cat {}".format(shlex.quote(source))
|
|
1521
|
+
cmd = (
|
|
1522
|
+
[
|
|
1523
|
+
self._sshcmd,
|
|
1524
|
+
]
|
|
1525
|
+
+ self._sshopts
|
|
1526
|
+
+ sshopts.split()
|
|
1527
|
+
+ [myremote, remote_cmd]
|
|
1528
|
+
)
|
|
1315
1529
|
return self.sh.spawn(cmd, output=stream, fatal=False)
|
|
1316
1530
|
|
|
1317
|
-
def tunnel(
|
|
1318
|
-
|
|
1531
|
+
def tunnel(
|
|
1532
|
+
self,
|
|
1533
|
+
finaldestination,
|
|
1534
|
+
finalport=0,
|
|
1535
|
+
entranceport=None,
|
|
1536
|
+
maxwait=3.0,
|
|
1537
|
+
checkdelay=0.25,
|
|
1538
|
+
):
|
|
1319
1539
|
"""Create an SSH tunnel and check that it actually starts.
|
|
1320
1540
|
|
|
1321
1541
|
:param str finaldestination: The destination hostname (i.e the machine
|
|
@@ -1343,45 +1563,84 @@ class Ssh:
|
|
|
1343
1563
|
entranceport = self.sh.available_localport()
|
|
1344
1564
|
else:
|
|
1345
1565
|
if self.sh.check_localport(entranceport):
|
|
1346
|
-
logger.error(
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1566
|
+
logger.error(
|
|
1567
|
+
"The SSH tunnel creation failed "
|
|
1568
|
+
+ "(entrance: %d, dest: %s:%d, via %s).",
|
|
1569
|
+
entranceport,
|
|
1570
|
+
finaldestination,
|
|
1571
|
+
finalport,
|
|
1572
|
+
myremote,
|
|
1573
|
+
)
|
|
1574
|
+
logger.error("The entrance port is already in use.")
|
|
1350
1575
|
return False
|
|
1351
|
-
if finaldestination ==
|
|
1352
|
-
p = self.sh.popen(
|
|
1353
|
-
|
|
1354
|
-
|
|
1576
|
+
if finaldestination == "socks":
|
|
1577
|
+
p = self.sh.popen(
|
|
1578
|
+
[
|
|
1579
|
+
self._sshcmd,
|
|
1580
|
+
]
|
|
1581
|
+
+ self._sshopts
|
|
1582
|
+
+ ["-N", "-D", "{:d}".format(entranceport), myremote],
|
|
1583
|
+
stdin=False,
|
|
1584
|
+
output=False,
|
|
1585
|
+
)
|
|
1355
1586
|
else:
|
|
1356
1587
|
if finalport <= 0:
|
|
1357
|
-
raise ValueError(
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1588
|
+
raise ValueError(
|
|
1589
|
+
"Erroneous finalport value: {!s}".format(finalport)
|
|
1590
|
+
)
|
|
1591
|
+
p = self.sh.popen(
|
|
1592
|
+
[
|
|
1593
|
+
self._sshcmd,
|
|
1594
|
+
]
|
|
1595
|
+
+ self._sshopts
|
|
1596
|
+
+ [
|
|
1597
|
+
"-N",
|
|
1598
|
+
"-L",
|
|
1599
|
+
"{:d}:{:s}:{:d}".format(
|
|
1600
|
+
entranceport, finaldestination, finalport
|
|
1601
|
+
),
|
|
1602
|
+
myremote,
|
|
1603
|
+
],
|
|
1604
|
+
stdin=False,
|
|
1605
|
+
output=False,
|
|
1606
|
+
)
|
|
1607
|
+
tunnel = ActiveSshTunnel(
|
|
1608
|
+
self.sh, p, entranceport, finaldestination, finalport
|
|
1609
|
+
)
|
|
1610
|
+
elapsed = 0.0
|
|
1611
|
+
while (
|
|
1612
|
+
not self.sh.check_localport(entranceport)
|
|
1613
|
+
) and elapsed < maxwait:
|
|
1367
1614
|
self.sh.sleep(checkdelay)
|
|
1368
1615
|
elapsed += checkdelay
|
|
1369
1616
|
if not self.sh.check_localport(entranceport):
|
|
1370
|
-
logger.error(
|
|
1371
|
-
|
|
1372
|
-
|
|
1617
|
+
logger.error(
|
|
1618
|
+
"The SSH tunnel creation failed "
|
|
1619
|
+
+ "(entrance: %d, dest: %s:%d, via %s).",
|
|
1620
|
+
entranceport,
|
|
1621
|
+
finaldestination,
|
|
1622
|
+
finalport,
|
|
1623
|
+
myremote,
|
|
1624
|
+
)
|
|
1373
1625
|
tunnel.close()
|
|
1374
1626
|
tunnel = False
|
|
1375
|
-
logger.info(
|
|
1376
|
-
|
|
1377
|
-
|
|
1627
|
+
logger.info(
|
|
1628
|
+
"SSH tunnel opened, enjoy the ride ! "
|
|
1629
|
+
+ "(entrance: %d, dest: %s:%d, via %s).",
|
|
1630
|
+
entranceport,
|
|
1631
|
+
finaldestination,
|
|
1632
|
+
finalport,
|
|
1633
|
+
myremote,
|
|
1634
|
+
)
|
|
1378
1635
|
return tunnel
|
|
1379
1636
|
|
|
1380
1637
|
|
|
1381
1638
|
class ActiveSshTunnel:
|
|
1382
1639
|
"""Hold an opened SSH tunnel."""
|
|
1383
1640
|
|
|
1384
|
-
def __init__(
|
|
1641
|
+
def __init__(
|
|
1642
|
+
self, sh, activeprocess, entranceport, finaldestination, finalport
|
|
1643
|
+
):
|
|
1385
1644
|
"""
|
|
1386
1645
|
:param Popen activeprocess: The active tunnel process.
|
|
1387
1646
|
:param int entranceport: Tunnel's entrance port.
|
|
@@ -1407,12 +1666,18 @@ class ActiveSshTunnel:
|
|
|
1407
1666
|
t0 = time.time()
|
|
1408
1667
|
while self.opened and time.time() - t0 < 5:
|
|
1409
1668
|
self._sh.sleep(0.1)
|
|
1410
|
-
logger.debug(
|
|
1669
|
+
logger.debug(
|
|
1670
|
+
"Tunnel termination took: %f seconds", time.time() - t0
|
|
1671
|
+
)
|
|
1411
1672
|
if self.opened:
|
|
1412
1673
|
logger.debug("Tunnel termination failed: issuing SIGKILL")
|
|
1413
1674
|
self.activeprocess.kill()
|
|
1414
|
-
logger.info(
|
|
1415
|
-
|
|
1675
|
+
logger.info(
|
|
1676
|
+
"SSH tunnel closed (entrance: %d, dest: %s:%d).",
|
|
1677
|
+
self.entranceport,
|
|
1678
|
+
self.finaldestination,
|
|
1679
|
+
self.finalport,
|
|
1680
|
+
)
|
|
1416
1681
|
|
|
1417
1682
|
@property
|
|
1418
1683
|
def opened(self):
|
|
@@ -1444,9 +1709,14 @@ def _check_fatal(func):
|
|
|
1444
1709
|
try:
|
|
1445
1710
|
rc = func(self, *args[1:], **kwargs)
|
|
1446
1711
|
if not rc:
|
|
1447
|
-
logger.error(
|
|
1712
|
+
logger.error(
|
|
1713
|
+
"The maximum number of retries (%s) was reached...",
|
|
1714
|
+
self._maxtries,
|
|
1715
|
+
)
|
|
1448
1716
|
if self._fatal:
|
|
1449
|
-
raise RuntimeError(
|
|
1717
|
+
raise RuntimeError(
|
|
1718
|
+
"Could not execute the SSH command."
|
|
1719
|
+
)
|
|
1450
1720
|
finally:
|
|
1451
1721
|
self._fatal_in_progress = False
|
|
1452
1722
|
return rc
|
|
@@ -1474,7 +1744,11 @@ def _tryagain(func):
|
|
|
1474
1744
|
rc = func(self, *args[1:], **kwargs)
|
|
1475
1745
|
while not rc and trycount < self._maxtries:
|
|
1476
1746
|
trycount += 1
|
|
1477
|
-
logger.info(
|
|
1747
|
+
logger.info(
|
|
1748
|
+
"Trying again (retries=%d/%d)...",
|
|
1749
|
+
trycount,
|
|
1750
|
+
self._maxtries,
|
|
1751
|
+
)
|
|
1478
1752
|
self.sh.sleep(self._triesdelay)
|
|
1479
1753
|
rc = func(self, *args[1:], **kwargs)
|
|
1480
1754
|
finally:
|
|
@@ -1503,13 +1777,17 @@ class _AssistedSshMeta(type):
|
|
|
1503
1777
|
"""
|
|
1504
1778
|
bare_methods = list(d.keys())
|
|
1505
1779
|
# Add the tryagain decorator...
|
|
1506
|
-
for tagain in [x for x in d[
|
|
1780
|
+
for tagain in [x for x in d["_auto_retries"] if x not in bare_methods]:
|
|
1507
1781
|
inherited = [base for base in b if hasattr(base, tagain)]
|
|
1508
1782
|
d[tagain] = _tryagain(getattr(inherited[0], tagain))
|
|
1509
1783
|
# Add the check_fatal decorator...
|
|
1510
|
-
for cfatal in [
|
|
1784
|
+
for cfatal in [
|
|
1785
|
+
x for x in d["_auto_checkfatal"] if x not in bare_methods
|
|
1786
|
+
]:
|
|
1511
1787
|
inherited = [base for base in b if hasattr(base, cfatal)]
|
|
1512
|
-
d[cfatal] = _check_fatal(
|
|
1788
|
+
d[cfatal] = _check_fatal(
|
|
1789
|
+
d.get(cfatal, getattr(inherited[0], cfatal))
|
|
1790
|
+
)
|
|
1513
1791
|
return super().__new__(cls, n, b, d)
|
|
1514
1792
|
|
|
1515
1793
|
|
|
@@ -1577,16 +1855,42 @@ class AssistedSsh(Ssh, metaclass=_AssistedSshMeta):
|
|
|
1577
1855
|
|
|
1578
1856
|
"""
|
|
1579
1857
|
|
|
1580
|
-
_auto_checkfatal = [
|
|
1581
|
-
|
|
1582
|
-
|
|
1858
|
+
_auto_checkfatal = [
|
|
1859
|
+
"check_ok",
|
|
1860
|
+
"execute",
|
|
1861
|
+
"cocoon",
|
|
1862
|
+
"remove",
|
|
1863
|
+
"scpput",
|
|
1864
|
+
"scpget",
|
|
1865
|
+
"scpput_stream",
|
|
1866
|
+
"scpget_stream",
|
|
1867
|
+
"tunnel",
|
|
1868
|
+
]
|
|
1583
1869
|
# No retries on scpput_stream since it's not guaranteed that the stream is seekable.
|
|
1584
|
-
_auto_retries = [
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1870
|
+
_auto_retries = [
|
|
1871
|
+
"check_ok",
|
|
1872
|
+
"execute",
|
|
1873
|
+
"cocoon",
|
|
1874
|
+
"remove",
|
|
1875
|
+
"scpput",
|
|
1876
|
+
"scpget",
|
|
1877
|
+
"tunnel",
|
|
1878
|
+
]
|
|
1879
|
+
|
|
1880
|
+
def __init__(
|
|
1881
|
+
self,
|
|
1882
|
+
sh,
|
|
1883
|
+
hostname,
|
|
1884
|
+
logname=None,
|
|
1885
|
+
sshopts=None,
|
|
1886
|
+
scpopts=None,
|
|
1887
|
+
maxtries=1,
|
|
1888
|
+
triesdelay=1,
|
|
1889
|
+
virtualnode=False,
|
|
1890
|
+
permut=True,
|
|
1891
|
+
fatal=False,
|
|
1892
|
+
mandatory_hostcheck=True,
|
|
1893
|
+
):
|
|
1590
1894
|
"""
|
|
1591
1895
|
:param System sh: The :class:`System` object that is to be used.
|
|
1592
1896
|
:param hostname: The target hostname(s).
|
|
@@ -1616,7 +1920,9 @@ class AssistedSsh(Ssh, metaclass=_AssistedSshMeta):
|
|
|
1616
1920
|
self._fatal = fatal
|
|
1617
1921
|
self._mandatory_hostcheck = mandatory_hostcheck
|
|
1618
1922
|
if self._virtualnode and isinstance(self._remote, (list, tuple)):
|
|
1619
|
-
raise ValueError(
|
|
1923
|
+
raise ValueError(
|
|
1924
|
+
"When virtual nodes are used, the hostname must be a string"
|
|
1925
|
+
)
|
|
1620
1926
|
|
|
1621
1927
|
self._retry_in_progress = False
|
|
1622
1928
|
self._fatal_in_progress = False
|
|
@@ -1640,7 +1946,7 @@ class AssistedSsh(Ssh, metaclass=_AssistedSshMeta):
|
|
|
1640
1946
|
else:
|
|
1641
1947
|
targets = [self._remote]
|
|
1642
1948
|
if self._logname is not None:
|
|
1643
|
-
targets = [self._logname +
|
|
1949
|
+
targets = [self._logname + "@" + x for x in targets]
|
|
1644
1950
|
if self._permut:
|
|
1645
1951
|
random.shuffle(targets)
|
|
1646
1952
|
return targets
|
|
@@ -1667,7 +1973,16 @@ class AssistedSsh(Ssh, metaclass=_AssistedSshMeta):
|
|
|
1667
1973
|
if self._mandatory_hostcheck:
|
|
1668
1974
|
if self._chosen_target is None:
|
|
1669
1975
|
for guess in self.targets:
|
|
1670
|
-
cmd =
|
|
1976
|
+
cmd = (
|
|
1977
|
+
[
|
|
1978
|
+
self._sshcmd,
|
|
1979
|
+
]
|
|
1980
|
+
+ self._sshopts
|
|
1981
|
+
+ [
|
|
1982
|
+
guess,
|
|
1983
|
+
"true",
|
|
1984
|
+
]
|
|
1985
|
+
)
|
|
1671
1986
|
try:
|
|
1672
1987
|
self.sh.spawn(cmd, output=False, silent=True)
|
|
1673
1988
|
except Exception:
|
|
@@ -1680,9 +1995,16 @@ class AssistedSsh(Ssh, metaclass=_AssistedSshMeta):
|
|
|
1680
1995
|
return next(self._targets_iter)
|
|
1681
1996
|
|
|
1682
1997
|
|
|
1683
|
-
_ConnectionStatusAttrs = (
|
|
1684
|
-
|
|
1685
|
-
|
|
1998
|
+
_ConnectionStatusAttrs = (
|
|
1999
|
+
"Family",
|
|
2000
|
+
"LocalAddr",
|
|
2001
|
+
"LocalPort",
|
|
2002
|
+
"DestAddr",
|
|
2003
|
+
"DestPort",
|
|
2004
|
+
"Status",
|
|
2005
|
+
)
|
|
2006
|
+
TcpConnectionStatus = namedtuple("TcpConnectionStatus", _ConnectionStatusAttrs)
|
|
2007
|
+
UdpConnectionStatus = namedtuple("UdpConnectionStatus", _ConnectionStatusAttrs)
|
|
1686
2008
|
|
|
1687
2009
|
|
|
1688
2010
|
class AbstractNetstats(metaclass=abc.ABCMeta):
|
|
@@ -1730,11 +2052,9 @@ class AbstractNetstats(metaclass=abc.ABCMeta):
|
|
|
1730
2052
|
class LinuxNetstats(AbstractNetstats):
|
|
1731
2053
|
"""A Netstats implementation for Linux (based on the /proc/net data)."""
|
|
1732
2054
|
|
|
1733
|
-
_LINUX_LPORT =
|
|
1734
|
-
_LINUX_PORTS_V4 = {
|
|
1735
|
-
|
|
1736
|
-
_LINUX_PORTS_V6 = {'tcp': '/proc/net/tcp6',
|
|
1737
|
-
'udp': '/proc/net/udp6'}
|
|
2055
|
+
_LINUX_LPORT = "/proc/sys/net/ipv4/ip_local_port_range"
|
|
2056
|
+
_LINUX_PORTS_V4 = {"tcp": "/proc/net/tcp", "udp": "/proc/net/udp"}
|
|
2057
|
+
_LINUX_PORTS_V6 = {"tcp": "/proc/net/tcp6", "udp": "/proc/net/udp6"}
|
|
1738
2058
|
_LINUX_AF_INET4 = socket.AF_INET
|
|
1739
2059
|
_LINUX_AF_INET6 = socket.AF_INET6
|
|
1740
2060
|
|
|
@@ -1747,7 +2067,9 @@ class LinuxNetstats(AbstractNetstats):
|
|
|
1747
2067
|
with open(self._LINUX_LPORT) as tmprange:
|
|
1748
2068
|
tmpports = [int(x) for x in tmprange.readline().split()]
|
|
1749
2069
|
unports = set(range(5001, 65536))
|
|
1750
|
-
self.__unprivileged_ports = sorted(
|
|
2070
|
+
self.__unprivileged_ports = sorted(
|
|
2071
|
+
unports - set(range(tmpports[0], tmpports[1] + 1))
|
|
2072
|
+
)
|
|
1751
2073
|
return self.__unprivileged_ports
|
|
1752
2074
|
|
|
1753
2075
|
@classmethod
|
|
@@ -1755,8 +2077,7 @@ class LinuxNetstats(AbstractNetstats):
|
|
|
1755
2077
|
if family == cls._LINUX_AF_INET4:
|
|
1756
2078
|
packed = struct.pack(b"<I", int(hexip, 16))
|
|
1757
2079
|
elif family == cls._LINUX_AF_INET6:
|
|
1758
|
-
packed = struct.unpack(b">IIII",
|
|
1759
|
-
binascii.a2b_hex(hexip))
|
|
2080
|
+
packed = struct.unpack(b">IIII", binascii.a2b_hex(hexip))
|
|
1760
2081
|
packed = struct.pack(b"@IIII", *packed)
|
|
1761
2082
|
else:
|
|
1762
2083
|
raise ValueError("Unknown address family.")
|
|
@@ -1766,25 +2087,38 @@ class LinuxNetstats(AbstractNetstats):
|
|
|
1766
2087
|
tmpports = dict()
|
|
1767
2088
|
with open(self._LINUX_PORTS_V4[proto]) as netstats:
|
|
1768
2089
|
netstats.readline() # Skip the header line
|
|
1769
|
-
tmpports[self._LINUX_AF_INET4] = [
|
|
1770
|
-
|
|
2090
|
+
tmpports[self._LINUX_AF_INET4] = [
|
|
2091
|
+
re.split(r":\b|\s+", x.strip())[1:6]
|
|
2092
|
+
for x in netstats.readlines()
|
|
2093
|
+
]
|
|
1771
2094
|
try:
|
|
1772
2095
|
with open(self._LINUX_PORTS_V6[proto]) as netstats:
|
|
1773
2096
|
netstats.readline() # Skip the header line
|
|
1774
|
-
tmpports[self._LINUX_AF_INET6] = [
|
|
1775
|
-
|
|
2097
|
+
tmpports[self._LINUX_AF_INET6] = [
|
|
2098
|
+
re.split(r":\b|\s+", x.strip())[1:6]
|
|
2099
|
+
for x in netstats.readlines()
|
|
2100
|
+
]
|
|
1776
2101
|
except OSError:
|
|
1777
2102
|
# Apparently, no IPv6 support on this machine
|
|
1778
2103
|
tmpports[self._LINUX_AF_INET6] = []
|
|
1779
|
-
tmpports = [
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
2104
|
+
tmpports = [
|
|
2105
|
+
[
|
|
2106
|
+
rclass(
|
|
2107
|
+
family,
|
|
2108
|
+
self._ip_from_hex(l[0], family),
|
|
2109
|
+
int(l[1], 16),
|
|
2110
|
+
self._ip_from_hex(l[2], family),
|
|
2111
|
+
int(l[3], 16),
|
|
2112
|
+
int(l[4], 16),
|
|
2113
|
+
)
|
|
2114
|
+
for l in tmpports[family]
|
|
2115
|
+
]
|
|
2116
|
+
for family in (self._LINUX_AF_INET4, self._LINUX_AF_INET6)
|
|
2117
|
+
]
|
|
1784
2118
|
return functools.reduce(operator.add, tmpports)
|
|
1785
2119
|
|
|
1786
2120
|
def tcp_netstats(self):
|
|
1787
|
-
return self._generic_netstats(
|
|
2121
|
+
return self._generic_netstats("tcp", TcpConnectionStatus)
|
|
1788
2122
|
|
|
1789
2123
|
def udp_netstats(self):
|
|
1790
|
-
return self._generic_netstats(
|
|
2124
|
+
return self._generic_netstats("udp", UdpConnectionStatus)
|