blob: 7a37c4b19f65cbc964b1f007c17fe0dd2533c8ae [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 kdanilove2de58c2015-04-24 22:59:36 +030056 @classmethod
57 def stat(cls, path):
58 return os.stat(path)
59
koder aka kdanilov783b4542015-04-23 18:57:04 +030060 def __enter__(self):
61 return self
62
63 def __exit__(self, x, y, z):
64 return False
65
koder aka kdanilov0c598a12015-04-21 03:01:40 +030066
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030067def ssh_connect(creds, conn_timeout=60):
koder aka kdanilov0c598a12015-04-21 03:01:40 +030068 if creds == 'local':
69 return Local
70
koder aka kdanilov46d4f392015-04-24 11:35:00 +030071 tcp_timeout = 15
72 banner_timeout = 30
73
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020074 ssh = paramiko.SSHClient()
75 ssh.load_host_keys('/dev/null')
76 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
77 ssh.known_hosts = None
koder aka kdanilov168f6092015-04-19 02:33:38 +030078
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030079 etime = time.time() + conn_timeout
80
81 while True:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020082 try:
koder aka kdanilov46d4f392015-04-24 11:35:00 +030083 tleft = etime - time.time()
84 c_tcp_timeout = min(tcp_timeout, tleft)
85 c_banner_timeout = min(banner_timeout, tleft)
86
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020087 if creds.passwd is not None:
88 ssh.connect(creds.host,
koder aka kdanilov46d4f392015-04-24 11:35:00 +030089 timeout=c_tcp_timeout,
koder aka kdanilova4a570f2015-04-23 22:11:40 +030090 username=creds.user,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020091 password=creds.passwd,
92 port=creds.port,
93 allow_agent=False,
koder aka kdanilov46d4f392015-04-24 11:35:00 +030094 look_for_keys=False,
95 banner_timeout=c_banner_timeout)
96 elif creds.key_file is not None:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020097 ssh.connect(creds.host,
koder aka kdanilova4a570f2015-04-23 22:11:40 +030098 username=creds.user,
koder aka kdanilov46d4f392015-04-24 11:35:00 +030099 timeout=c_tcp_timeout,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200100 key_filename=creds.key_file,
101 look_for_keys=False,
koder aka kdanilov46d4f392015-04-24 11:35:00 +0300102 port=creds.port,
103 banner_timeout=c_banner_timeout)
104 else:
105 key_file = os.path.expanduser('~/.ssh/id_rsa')
106 ssh.connect(creds.host,
107 username=creds.user,
108 timeout=c_tcp_timeout,
109 key_filename=key_file,
110 look_for_keys=False,
111 port=creds.port,
112 banner_timeout=c_banner_timeout)
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200113 return ssh
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200114 except paramiko.PasswordRequiredException:
115 raise
koder aka kdanilov46d4f392015-04-24 11:35:00 +0300116 except (socket.error, paramiko.SSHException):
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300117 if time.time() > etime:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200118 raise
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300119 time.sleep(1)
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200120
121
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300122def save_to_remote(sftp, path, content):
123 with sftp.open(path, "wb") as fd:
124 fd.write(content)
125
126
127def read_from_remote(sftp, path):
128 with sftp.open(path, "rb") as fd:
129 return fd.read()
130
131
koder aka kdanilove06762a2015-03-22 23:32:09 +0200132def normalize_dirpath(dirpath):
133 while dirpath.endswith("/"):
134 dirpath = dirpath[:-1]
135 return dirpath
136
137
koder aka kdanilov2c473092015-03-29 17:12:13 +0300138ALL_RWX_MODE = ((1 << 9) - 1)
139
140
141def ssh_mkdir(sftp, remotepath, mode=ALL_RWX_MODE, intermediate=False):
koder aka kdanilove06762a2015-03-22 23:32:09 +0200142 remotepath = normalize_dirpath(remotepath)
143 if intermediate:
144 try:
145 sftp.mkdir(remotepath, mode=mode)
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300146 except (IOError, OSError):
koder aka kdanilov168f6092015-04-19 02:33:38 +0300147 upper_dir = remotepath.rsplit("/", 1)[0]
148
149 if upper_dir == '' or upper_dir == '/':
150 raise
151
152 ssh_mkdir(sftp, upper_dir, mode=mode, intermediate=True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200153 return sftp.mkdir(remotepath, mode=mode)
154 else:
155 sftp.mkdir(remotepath, mode=mode)
156
157
158def ssh_copy_file(sftp, localfile, remfile, preserve_perm=True):
159 sftp.put(localfile, remfile)
160 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300161 sftp.chmod(remfile, os.stat(localfile).st_mode & ALL_RWX_MODE)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200162
163
164def put_dir_recursively(sftp, localpath, remotepath, preserve_perm=True):
165 "upload local directory to remote recursively"
166
167 # hack for localhost connection
168 if hasattr(sftp, "copytree"):
169 sftp.copytree(localpath, remotepath)
170 return
171
172 assert remotepath.startswith("/"), "%s must be absolute path" % remotepath
173
174 # normalize
175 localpath = normalize_dirpath(localpath)
176 remotepath = normalize_dirpath(remotepath)
177
178 try:
179 sftp.chdir(remotepath)
180 localsuffix = localpath.rsplit("/", 1)[1]
181 remotesuffix = remotepath.rsplit("/", 1)[1]
182 if localsuffix != remotesuffix:
183 remotepath = os.path.join(remotepath, localsuffix)
184 except IOError:
185 pass
186
187 for root, dirs, fls in os.walk(localpath):
188 prefix = os.path.commonprefix([localpath, root])
189 suffix = root.split(prefix, 1)[1]
190 if suffix.startswith("/"):
191 suffix = suffix[1:]
192
193 remroot = os.path.join(remotepath, suffix)
194
195 try:
196 sftp.chdir(remroot)
197 except IOError:
198 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300199 mode = os.stat(root).st_mode & ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200200 else:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300201 mode = ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200202 ssh_mkdir(sftp, remroot, mode=mode, intermediate=True)
203 sftp.chdir(remroot)
204
205 for f in fls:
206 remfile = os.path.join(remroot, f)
207 localfile = os.path.join(root, f)
208 ssh_copy_file(sftp, localfile, remfile, preserve_perm)
209
210
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300211def delete_file(conn, path):
212 sftp = conn.open_sftp()
213 sftp.remove(path)
214 sftp.close()
215
216
koder aka kdanilove06762a2015-03-22 23:32:09 +0200217def copy_paths(conn, paths):
218 sftp = conn.open_sftp()
219 try:
220 for src, dst in paths.items():
221 try:
222 if os.path.isfile(src):
223 ssh_copy_file(sftp, src, dst)
224 elif os.path.isdir(src):
225 put_dir_recursively(sftp, src, dst)
226 else:
227 templ = "Can't copy {0!r} - " + \
228 "it neither a file not a directory"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300229 raise OSError(templ.format(src))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200230 except Exception as exc:
231 tmpl = "Scp {0!r} => {1!r} failed - {2!r}"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300232 raise OSError(tmpl.format(src, dst, exc))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200233 finally:
234 sftp.close()
235
236
237class ConnCreds(object):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300238 conn_uri_attrs = ("user", "passwd", "host", "port", "path")
239
koder aka kdanilove06762a2015-03-22 23:32:09 +0200240 def __init__(self):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300241 for name in self.conn_uri_attrs:
koder aka kdanilove06762a2015-03-22 23:32:09 +0200242 setattr(self, name, None)
243
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300244 def __str__(self):
245 return str(self.__dict__)
246
koder aka kdanilove06762a2015-03-22 23:32:09 +0200247
248uri_reg_exprs = []
249
250
251class URIsNamespace(object):
252 class ReParts(object):
253 user_rr = "[^:]*?"
koder aka kdanilov7e0f7cf2015-05-01 17:24:35 +0300254 host_rr = "[^:@]*?"
koder aka kdanilove06762a2015-03-22 23:32:09 +0200255 port_rr = "\\d+"
256 key_file_rr = "[^:@]*"
257 passwd_rr = ".*?"
258
259 re_dct = ReParts.__dict__
260
261 for attr_name, val in re_dct.items():
262 if attr_name.endswith('_rr'):
263 new_rr = "(?P<{0}>{1})".format(attr_name[:-3], val)
264 setattr(ReParts, attr_name, new_rr)
265
266 re_dct = ReParts.__dict__
267
268 templs = [
269 "^{host_rr}$",
koder aka kdanilov7e0f7cf2015-05-01 17:24:35 +0300270 "^{host_rr}:{port_rr}$",
271 "^{user_rr}@{host_rr}$",
272 "^{user_rr}@{host_rr}:{port_rr}$",
koder aka kdanilove06762a2015-03-22 23:32:09 +0200273 "^{user_rr}@{host_rr}::{key_file_rr}$",
274 "^{user_rr}@{host_rr}:{port_rr}:{key_file_rr}$",
koder aka kdanilov7e0f7cf2015-05-01 17:24:35 +0300275 "^{user_rr}:{passwd_rr}@{host_rr}$",
276 "^{user_rr}:{passwd_rr}@{host_rr}:{port_rr}$",
koder aka kdanilove06762a2015-03-22 23:32:09 +0200277 ]
278
279 for templ in templs:
280 uri_reg_exprs.append(templ.format(**re_dct))
281
282
283def parse_ssh_uri(uri):
koder aka kdanilov7e0f7cf2015-05-01 17:24:35 +0300284 # user:passwd@ip_host:port
285 # user:passwd@ip_host
koder aka kdanilove06762a2015-03-22 23:32:09 +0200286 # user@ip_host:port
287 # user@ip_host
288 # ip_host:port
289 # ip_host
290 # user@ip_host:port:path_to_key_file
291 # user@ip_host::path_to_key_file
292 # ip_host:port:path_to_key_file
293 # ip_host::path_to_key_file
294
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300295 if uri.startswith("ssh://"):
296 uri = uri[len("ssh://"):]
297
koder aka kdanilove06762a2015-03-22 23:32:09 +0200298 res = ConnCreds()
299 res.port = "22"
300 res.key_file = None
301 res.passwd = None
koder aka kdanilova4a570f2015-04-23 22:11:40 +0300302 res.user = getpass.getuser()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200303
304 for rr in uri_reg_exprs:
305 rrm = re.match(rr, uri)
306 if rrm is not None:
307 res.__dict__.update(rrm.groupdict())
308 return res
koder aka kdanilov652cd802015-04-13 12:21:07 +0300309
koder aka kdanilove06762a2015-03-22 23:32:09 +0200310 raise ValueError("Can't parse {0!r} as ssh uri value".format(uri))
311
312
koder aka kdanilov168f6092015-04-19 02:33:38 +0300313def connect(uri, **params):
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300314 if uri == 'local':
315 return Local
316
koder aka kdanilove06762a2015-03-22 23:32:09 +0200317 creds = parse_ssh_uri(uri)
318 creds.port = int(creds.port)
koder aka kdanilov168f6092015-04-19 02:33:38 +0300319 return ssh_connect(creds, **params)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200320
321
koder aka kdanilov652cd802015-04-13 12:21:07 +0300322all_sessions_lock = threading.Lock()
323all_sessions = []
koder aka kdanilove06762a2015-03-22 23:32:09 +0200324
koder aka kdanilove06762a2015-03-22 23:32:09 +0200325
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300326def run_over_ssh(conn, cmd, stdin_data=None, timeout=60,
327 nolog=False, node=None):
koder aka kdanilov652cd802015-04-13 12:21:07 +0300328 "should be replaces by normal implementation, with select"
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300329
330 if conn is Local:
331 if not nolog:
332 logger.debug("SSH:local Exec {0!r}".format(cmd))
333 proc = subprocess.Popen(cmd, shell=True,
334 stdin=subprocess.PIPE,
335 stdout=subprocess.PIPE,
336 stderr=subprocess.STDOUT)
337
338 stdoutdata, _ = proc.communicate(input=stdin_data)
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300339 if proc.returncode != 0:
340 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
341 raise OSError(templ.format(node, cmd, proc.returncode, stdoutdata))
342
343 return stdoutdata
344
koder aka kdanilov652cd802015-04-13 12:21:07 +0300345 transport = conn.get_transport()
346 session = transport.open_session()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200347
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300348 if node is None:
349 node = ""
350
koder aka kdanilov652cd802015-04-13 12:21:07 +0300351 with all_sessions_lock:
352 all_sessions.append(session)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200353
koder aka kdanilov652cd802015-04-13 12:21:07 +0300354 try:
355 session.set_combine_stderr(True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200356
koder aka kdanilov652cd802015-04-13 12:21:07 +0300357 stime = time.time()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200358
koder aka kdanilov652cd802015-04-13 12:21:07 +0300359 if not nolog:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300360 logger.debug("SSH:{0} Exec {1!r}".format(node, cmd))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200361
koder aka kdanilov652cd802015-04-13 12:21:07 +0300362 session.exec_command(cmd)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200363
koder aka kdanilov652cd802015-04-13 12:21:07 +0300364 if stdin_data is not None:
365 session.sendall(stdin_data)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200366
koder aka kdanilov652cd802015-04-13 12:21:07 +0300367 session.settimeout(1)
368 session.shutdown_write()
369 output = ""
370
371 while True:
372 try:
373 ndata = session.recv(1024)
374 output += ndata
375 if "" == ndata:
376 break
377 except socket.timeout:
378 pass
379
380 if time.time() - stime > timeout:
381 raise OSError(output + "\nExecution timeout")
382
383 code = session.recv_exit_status()
384 finally:
385 session.close()
386
387 if code != 0:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300388 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
389 raise OSError(templ.format(node, cmd, code, output))
koder aka kdanilov652cd802015-04-13 12:21:07 +0300390
391 return output
392
393
394def close_all_sessions():
395 with all_sessions_lock:
396 for session in all_sessions:
397 try:
398 session.sendall('\x03')
399 session.close()
400 except:
401 pass