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