blob: c6b1f706915656785876467880e5d9fd6a19e7c9 [file] [log] [blame]
koder aka kdanilove06762a2015-03-22 23:32:09 +02001import re
koder aka kdanilov3a6633e2015-03-26 18:20:00 +02002import time
koder aka kdanilov652cd802015-04-13 12:21:07 +03003import socket
koder aka kdanilov0c598a12015-04-21 03:01:40 +03004import shutil
koder aka kdanilove06762a2015-03-22 23:32:09 +02005import logging
6import os.path
koder aka kdanilov3a6633e2015-03-26 18:20:00 +02007import getpass
koder aka kdanilov652cd802015-04-13 12:21:07 +03008import threading
koder aka kdanilov0c598a12015-04-21 03:01:40 +03009import subprocess
koder aka kdanilov652cd802015-04-13 12:21:07 +030010
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020011import paramiko
koder aka kdanilove06762a2015-03-22 23:32:09 +020012
koder aka kdanilove06762a2015-03-22 23:32:09 +020013
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030014logger = logging.getLogger("wally")
koder aka kdanilove06762a2015-03-22 23:32:09 +020015
16
koder aka kdanilov0c598a12015-04-21 03:01:40 +030017class Local(object):
18 "placeholder for local node"
19 @classmethod
20 def open_sftp(cls):
koder aka kdanilovafd98742015-04-24 01:27:22 +030021 return cls()
koder aka kdanilov0c598a12015-04-21 03:01:40 +030022
23 @classmethod
24 def mkdir(cls, remotepath, mode=None):
25 os.mkdir(remotepath)
26 if mode is not None:
27 os.chmod(remotepath, mode)
28
29 @classmethod
30 def put(cls, localfile, remfile):
koder aka kdanilov4d4771c2015-04-23 01:32:02 +030031 dirname = os.path.dirname(remfile)
32 if not os.path.exists(dirname):
33 os.makedirs(dirname)
koder aka kdanilov0c598a12015-04-21 03:01:40 +030034 shutil.copyfile(localfile, remfile)
35
36 @classmethod
37 def chmod(cls, path, mode):
38 os.chmod(path, mode)
39
40 @classmethod
41 def copytree(cls, src, dst):
42 shutil.copytree(src, dst)
43
44 @classmethod
45 def remove(cls, path):
46 os.unlink(path)
47
48 @classmethod
49 def close(cls):
50 pass
51
52 @classmethod
53 def open(cls, *args, **kwarhgs):
54 return open(*args, **kwarhgs)
55
koder aka kdanilov783b4542015-04-23 18:57:04 +030056 def __enter__(self):
57 return self
58
59 def __exit__(self, x, y, z):
60 return False
61
koder aka kdanilov0c598a12015-04-21 03:01:40 +030062
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030063def ssh_connect(creds, conn_timeout=60):
koder aka kdanilov0c598a12015-04-21 03:01:40 +030064 if creds == 'local':
65 return Local
66
koder aka kdanilov46d4f392015-04-24 11:35:00 +030067 tcp_timeout = 15
68 banner_timeout = 30
69
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020070 ssh = paramiko.SSHClient()
71 ssh.load_host_keys('/dev/null')
72 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
73 ssh.known_hosts = None
koder aka kdanilov168f6092015-04-19 02:33:38 +030074
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030075 etime = time.time() + conn_timeout
76
77 while True:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020078 try:
koder aka kdanilov46d4f392015-04-24 11:35:00 +030079 tleft = etime - time.time()
80 c_tcp_timeout = min(tcp_timeout, tleft)
81 c_banner_timeout = min(banner_timeout, tleft)
82
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020083 if creds.passwd is not None:
84 ssh.connect(creds.host,
koder aka kdanilov46d4f392015-04-24 11:35:00 +030085 timeout=c_tcp_timeout,
koder aka kdanilova4a570f2015-04-23 22:11:40 +030086 username=creds.user,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020087 password=creds.passwd,
88 port=creds.port,
89 allow_agent=False,
koder aka kdanilov46d4f392015-04-24 11:35:00 +030090 look_for_keys=False,
91 banner_timeout=c_banner_timeout)
92 elif creds.key_file is not None:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020093 ssh.connect(creds.host,
koder aka kdanilova4a570f2015-04-23 22:11:40 +030094 username=creds.user,
koder aka kdanilov46d4f392015-04-24 11:35:00 +030095 timeout=c_tcp_timeout,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020096 key_filename=creds.key_file,
97 look_for_keys=False,
koder aka kdanilov46d4f392015-04-24 11:35:00 +030098 port=creds.port,
99 banner_timeout=c_banner_timeout)
100 else:
101 key_file = os.path.expanduser('~/.ssh/id_rsa')
102 ssh.connect(creds.host,
103 username=creds.user,
104 timeout=c_tcp_timeout,
105 key_filename=key_file,
106 look_for_keys=False,
107 port=creds.port,
108 banner_timeout=c_banner_timeout)
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200109 return ssh
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200110 except paramiko.PasswordRequiredException:
111 raise
koder aka kdanilov46d4f392015-04-24 11:35:00 +0300112 except (socket.error, paramiko.SSHException):
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300113 if time.time() > etime:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200114 raise
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300115 time.sleep(1)
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200116
117
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300118def save_to_remote(sftp, path, content):
119 with sftp.open(path, "wb") as fd:
120 fd.write(content)
121
122
123def read_from_remote(sftp, path):
124 with sftp.open(path, "rb") as fd:
125 return fd.read()
126
127
koder aka kdanilove06762a2015-03-22 23:32:09 +0200128def normalize_dirpath(dirpath):
129 while dirpath.endswith("/"):
130 dirpath = dirpath[:-1]
131 return dirpath
132
133
koder aka kdanilov2c473092015-03-29 17:12:13 +0300134ALL_RWX_MODE = ((1 << 9) - 1)
135
136
137def ssh_mkdir(sftp, remotepath, mode=ALL_RWX_MODE, intermediate=False):
koder aka kdanilove06762a2015-03-22 23:32:09 +0200138 remotepath = normalize_dirpath(remotepath)
139 if intermediate:
140 try:
141 sftp.mkdir(remotepath, mode=mode)
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300142 except (IOError, OSError):
koder aka kdanilov168f6092015-04-19 02:33:38 +0300143 upper_dir = remotepath.rsplit("/", 1)[0]
144
145 if upper_dir == '' or upper_dir == '/':
146 raise
147
148 ssh_mkdir(sftp, upper_dir, mode=mode, intermediate=True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200149 return sftp.mkdir(remotepath, mode=mode)
150 else:
151 sftp.mkdir(remotepath, mode=mode)
152
153
154def ssh_copy_file(sftp, localfile, remfile, preserve_perm=True):
155 sftp.put(localfile, remfile)
156 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300157 sftp.chmod(remfile, os.stat(localfile).st_mode & ALL_RWX_MODE)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200158
159
160def put_dir_recursively(sftp, localpath, remotepath, preserve_perm=True):
161 "upload local directory to remote recursively"
162
163 # hack for localhost connection
164 if hasattr(sftp, "copytree"):
165 sftp.copytree(localpath, remotepath)
166 return
167
168 assert remotepath.startswith("/"), "%s must be absolute path" % remotepath
169
170 # normalize
171 localpath = normalize_dirpath(localpath)
172 remotepath = normalize_dirpath(remotepath)
173
174 try:
175 sftp.chdir(remotepath)
176 localsuffix = localpath.rsplit("/", 1)[1]
177 remotesuffix = remotepath.rsplit("/", 1)[1]
178 if localsuffix != remotesuffix:
179 remotepath = os.path.join(remotepath, localsuffix)
180 except IOError:
181 pass
182
183 for root, dirs, fls in os.walk(localpath):
184 prefix = os.path.commonprefix([localpath, root])
185 suffix = root.split(prefix, 1)[1]
186 if suffix.startswith("/"):
187 suffix = suffix[1:]
188
189 remroot = os.path.join(remotepath, suffix)
190
191 try:
192 sftp.chdir(remroot)
193 except IOError:
194 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300195 mode = os.stat(root).st_mode & ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200196 else:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300197 mode = ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200198 ssh_mkdir(sftp, remroot, mode=mode, intermediate=True)
199 sftp.chdir(remroot)
200
201 for f in fls:
202 remfile = os.path.join(remroot, f)
203 localfile = os.path.join(root, f)
204 ssh_copy_file(sftp, localfile, remfile, preserve_perm)
205
206
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300207def delete_file(conn, path):
208 sftp = conn.open_sftp()
209 sftp.remove(path)
210 sftp.close()
211
212
koder aka kdanilove06762a2015-03-22 23:32:09 +0200213def copy_paths(conn, paths):
214 sftp = conn.open_sftp()
215 try:
216 for src, dst in paths.items():
217 try:
218 if os.path.isfile(src):
219 ssh_copy_file(sftp, src, dst)
220 elif os.path.isdir(src):
221 put_dir_recursively(sftp, src, dst)
222 else:
223 templ = "Can't copy {0!r} - " + \
224 "it neither a file not a directory"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300225 raise OSError(templ.format(src))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200226 except Exception as exc:
227 tmpl = "Scp {0!r} => {1!r} failed - {2!r}"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300228 raise OSError(tmpl.format(src, dst, exc))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200229 finally:
230 sftp.close()
231
232
233class ConnCreds(object):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300234 conn_uri_attrs = ("user", "passwd", "host", "port", "path")
235
koder aka kdanilove06762a2015-03-22 23:32:09 +0200236 def __init__(self):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300237 for name in self.conn_uri_attrs:
koder aka kdanilove06762a2015-03-22 23:32:09 +0200238 setattr(self, name, None)
239
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300240 def __str__(self):
241 return str(self.__dict__)
242
koder aka kdanilove06762a2015-03-22 23:32:09 +0200243
244uri_reg_exprs = []
245
246
247class URIsNamespace(object):
248 class ReParts(object):
249 user_rr = "[^:]*?"
250 host_rr = "[^:]*?"
251 port_rr = "\\d+"
252 key_file_rr = "[^:@]*"
253 passwd_rr = ".*?"
254
255 re_dct = ReParts.__dict__
256
257 for attr_name, val in re_dct.items():
258 if attr_name.endswith('_rr'):
259 new_rr = "(?P<{0}>{1})".format(attr_name[:-3], val)
260 setattr(ReParts, attr_name, new_rr)
261
262 re_dct = ReParts.__dict__
263
264 templs = [
265 "^{host_rr}$",
266 "^{user_rr}@{host_rr}::{key_file_rr}$",
267 "^{user_rr}@{host_rr}:{port_rr}:{key_file_rr}$",
268 "^{user_rr}:{passwd_rr}@@{host_rr}$",
269 "^{user_rr}:{passwd_rr}@@{host_rr}:{port_rr}$",
270 ]
271
272 for templ in templs:
273 uri_reg_exprs.append(templ.format(**re_dct))
274
275
276def parse_ssh_uri(uri):
277 # user:passwd@@ip_host:port
278 # user:passwd@@ip_host
279 # user@ip_host:port
280 # user@ip_host
281 # ip_host:port
282 # ip_host
283 # user@ip_host:port:path_to_key_file
284 # user@ip_host::path_to_key_file
285 # ip_host:port:path_to_key_file
286 # ip_host::path_to_key_file
287
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300288 if uri.startswith("ssh://"):
289 uri = uri[len("ssh://"):]
290
koder aka kdanilove06762a2015-03-22 23:32:09 +0200291 res = ConnCreds()
292 res.port = "22"
293 res.key_file = None
294 res.passwd = None
koder aka kdanilova4a570f2015-04-23 22:11:40 +0300295 res.user = getpass.getuser()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200296
297 for rr in uri_reg_exprs:
298 rrm = re.match(rr, uri)
299 if rrm is not None:
300 res.__dict__.update(rrm.groupdict())
301 return res
koder aka kdanilov652cd802015-04-13 12:21:07 +0300302
koder aka kdanilove06762a2015-03-22 23:32:09 +0200303 raise ValueError("Can't parse {0!r} as ssh uri value".format(uri))
304
305
koder aka kdanilov168f6092015-04-19 02:33:38 +0300306def connect(uri, **params):
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300307 if uri == 'local':
308 return Local
309
koder aka kdanilove06762a2015-03-22 23:32:09 +0200310 creds = parse_ssh_uri(uri)
311 creds.port = int(creds.port)
koder aka kdanilov168f6092015-04-19 02:33:38 +0300312 return ssh_connect(creds, **params)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200313
314
koder aka kdanilov652cd802015-04-13 12:21:07 +0300315all_sessions_lock = threading.Lock()
316all_sessions = []
koder aka kdanilove06762a2015-03-22 23:32:09 +0200317
koder aka kdanilove06762a2015-03-22 23:32:09 +0200318
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300319def run_over_ssh(conn, cmd, stdin_data=None, timeout=60,
320 nolog=False, node=None):
koder aka kdanilov652cd802015-04-13 12:21:07 +0300321 "should be replaces by normal implementation, with select"
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300322
323 if conn is Local:
324 if not nolog:
325 logger.debug("SSH:local Exec {0!r}".format(cmd))
326 proc = subprocess.Popen(cmd, shell=True,
327 stdin=subprocess.PIPE,
328 stdout=subprocess.PIPE,
329 stderr=subprocess.STDOUT)
330
331 stdoutdata, _ = proc.communicate(input=stdin_data)
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300332 if proc.returncode != 0:
333 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
334 raise OSError(templ.format(node, cmd, proc.returncode, stdoutdata))
335
336 return stdoutdata
337
koder aka kdanilov652cd802015-04-13 12:21:07 +0300338 transport = conn.get_transport()
339 session = transport.open_session()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200340
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300341 if node is None:
342 node = ""
343
koder aka kdanilov652cd802015-04-13 12:21:07 +0300344 with all_sessions_lock:
345 all_sessions.append(session)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200346
koder aka kdanilov652cd802015-04-13 12:21:07 +0300347 try:
348 session.set_combine_stderr(True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200349
koder aka kdanilov652cd802015-04-13 12:21:07 +0300350 stime = time.time()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200351
koder aka kdanilov652cd802015-04-13 12:21:07 +0300352 if not nolog:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300353 logger.debug("SSH:{0} Exec {1!r}".format(node, cmd))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200354
koder aka kdanilov652cd802015-04-13 12:21:07 +0300355 session.exec_command(cmd)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200356
koder aka kdanilov652cd802015-04-13 12:21:07 +0300357 if stdin_data is not None:
358 session.sendall(stdin_data)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200359
koder aka kdanilov652cd802015-04-13 12:21:07 +0300360 session.settimeout(1)
361 session.shutdown_write()
362 output = ""
363
364 while True:
365 try:
366 ndata = session.recv(1024)
367 output += ndata
368 if "" == ndata:
369 break
370 except socket.timeout:
371 pass
372
373 if time.time() - stime > timeout:
374 raise OSError(output + "\nExecution timeout")
375
376 code = session.recv_exit_status()
377 finally:
378 session.close()
379
380 if code != 0:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300381 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
382 raise OSError(templ.format(node, cmd, code, output))
koder aka kdanilov652cd802015-04-13 12:21:07 +0300383
384 return output
385
386
387def close_all_sessions():
388 with all_sessions_lock:
389 for session in all_sessions:
390 try:
391 session.sendall('\x03')
392 session.close()
393 except:
394 pass