blob: f4de3b672946ee41ef4bb925e60e9f9b50dd98cc [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):
21 return cls
22
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):
31 shutil.copyfile(localfile, remfile)
32
33 @classmethod
34 def chmod(cls, path, mode):
35 os.chmod(path, mode)
36
37 @classmethod
38 def copytree(cls, src, dst):
39 shutil.copytree(src, dst)
40
41 @classmethod
42 def remove(cls, path):
43 os.unlink(path)
44
45 @classmethod
46 def close(cls):
47 pass
48
49 @classmethod
50 def open(cls, *args, **kwarhgs):
51 return open(*args, **kwarhgs)
52
53
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030054def ssh_connect(creds, conn_timeout=60):
koder aka kdanilov0c598a12015-04-21 03:01:40 +030055 if creds == 'local':
56 return Local
57
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030058 tcp_timeout = 30
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020059 ssh = paramiko.SSHClient()
60 ssh.load_host_keys('/dev/null')
61 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
62 ssh.known_hosts = None
koder aka kdanilov168f6092015-04-19 02:33:38 +030063
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030064 etime = time.time() + conn_timeout
65
66 while True:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020067 try:
68 if creds.user is None:
69 user = getpass.getuser()
70 else:
71 user = creds.user
72
73 if creds.passwd is not None:
74 ssh.connect(creds.host,
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030075 timeout=tcp_timeout,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020076 username=user,
77 password=creds.passwd,
78 port=creds.port,
79 allow_agent=False,
80 look_for_keys=False)
81 return ssh
82
83 if creds.key_file is not None:
84 ssh.connect(creds.host,
85 username=user,
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030086 timeout=tcp_timeout,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020087 key_filename=creds.key_file,
88 look_for_keys=False,
89 port=creds.port)
90 return ssh
91
92 key_file = os.path.expanduser('~/.ssh/id_rsa')
93 ssh.connect(creds.host,
94 username=user,
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030095 timeout=tcp_timeout,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020096 key_filename=key_file,
97 look_for_keys=False,
98 port=creds.port)
99 return ssh
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200100 except paramiko.PasswordRequiredException:
101 raise
koder aka kdanilov168f6092015-04-19 02:33:38 +0300102 except socket.error:
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300103 if time.time() > etime:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200104 raise
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300105 time.sleep(1)
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200106
107
koder aka kdanilove06762a2015-03-22 23:32:09 +0200108def normalize_dirpath(dirpath):
109 while dirpath.endswith("/"):
110 dirpath = dirpath[:-1]
111 return dirpath
112
113
koder aka kdanilov2c473092015-03-29 17:12:13 +0300114ALL_RWX_MODE = ((1 << 9) - 1)
115
116
117def ssh_mkdir(sftp, remotepath, mode=ALL_RWX_MODE, intermediate=False):
koder aka kdanilove06762a2015-03-22 23:32:09 +0200118 remotepath = normalize_dirpath(remotepath)
119 if intermediate:
120 try:
121 sftp.mkdir(remotepath, mode=mode)
122 except IOError:
koder aka kdanilov168f6092015-04-19 02:33:38 +0300123 upper_dir = remotepath.rsplit("/", 1)[0]
124
125 if upper_dir == '' or upper_dir == '/':
126 raise
127
128 ssh_mkdir(sftp, upper_dir, mode=mode, intermediate=True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200129 return sftp.mkdir(remotepath, mode=mode)
130 else:
131 sftp.mkdir(remotepath, mode=mode)
132
133
134def ssh_copy_file(sftp, localfile, remfile, preserve_perm=True):
135 sftp.put(localfile, remfile)
136 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300137 sftp.chmod(remfile, os.stat(localfile).st_mode & ALL_RWX_MODE)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200138
139
140def put_dir_recursively(sftp, localpath, remotepath, preserve_perm=True):
141 "upload local directory to remote recursively"
142
143 # hack for localhost connection
144 if hasattr(sftp, "copytree"):
145 sftp.copytree(localpath, remotepath)
146 return
147
148 assert remotepath.startswith("/"), "%s must be absolute path" % remotepath
149
150 # normalize
151 localpath = normalize_dirpath(localpath)
152 remotepath = normalize_dirpath(remotepath)
153
154 try:
155 sftp.chdir(remotepath)
156 localsuffix = localpath.rsplit("/", 1)[1]
157 remotesuffix = remotepath.rsplit("/", 1)[1]
158 if localsuffix != remotesuffix:
159 remotepath = os.path.join(remotepath, localsuffix)
160 except IOError:
161 pass
162
163 for root, dirs, fls in os.walk(localpath):
164 prefix = os.path.commonprefix([localpath, root])
165 suffix = root.split(prefix, 1)[1]
166 if suffix.startswith("/"):
167 suffix = suffix[1:]
168
169 remroot = os.path.join(remotepath, suffix)
170
171 try:
172 sftp.chdir(remroot)
173 except IOError:
174 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300175 mode = os.stat(root).st_mode & ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200176 else:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300177 mode = ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200178 ssh_mkdir(sftp, remroot, mode=mode, intermediate=True)
179 sftp.chdir(remroot)
180
181 for f in fls:
182 remfile = os.path.join(remroot, f)
183 localfile = os.path.join(root, f)
184 ssh_copy_file(sftp, localfile, remfile, preserve_perm)
185
186
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300187def delete_file(conn, path):
188 sftp = conn.open_sftp()
189 sftp.remove(path)
190 sftp.close()
191
192
koder aka kdanilove06762a2015-03-22 23:32:09 +0200193def copy_paths(conn, paths):
194 sftp = conn.open_sftp()
195 try:
196 for src, dst in paths.items():
197 try:
198 if os.path.isfile(src):
199 ssh_copy_file(sftp, src, dst)
200 elif os.path.isdir(src):
201 put_dir_recursively(sftp, src, dst)
202 else:
203 templ = "Can't copy {0!r} - " + \
204 "it neither a file not a directory"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300205 raise OSError(templ.format(src))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200206 except Exception as exc:
207 tmpl = "Scp {0!r} => {1!r} failed - {2!r}"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300208 raise OSError(tmpl.format(src, dst, exc))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200209 finally:
210 sftp.close()
211
212
213class ConnCreds(object):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300214 conn_uri_attrs = ("user", "passwd", "host", "port", "path")
215
koder aka kdanilove06762a2015-03-22 23:32:09 +0200216 def __init__(self):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300217 for name in self.conn_uri_attrs:
koder aka kdanilove06762a2015-03-22 23:32:09 +0200218 setattr(self, name, None)
219
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300220 def __str__(self):
221 return str(self.__dict__)
222
koder aka kdanilove06762a2015-03-22 23:32:09 +0200223
224uri_reg_exprs = []
225
226
227class URIsNamespace(object):
228 class ReParts(object):
229 user_rr = "[^:]*?"
230 host_rr = "[^:]*?"
231 port_rr = "\\d+"
232 key_file_rr = "[^:@]*"
233 passwd_rr = ".*?"
234
235 re_dct = ReParts.__dict__
236
237 for attr_name, val in re_dct.items():
238 if attr_name.endswith('_rr'):
239 new_rr = "(?P<{0}>{1})".format(attr_name[:-3], val)
240 setattr(ReParts, attr_name, new_rr)
241
242 re_dct = ReParts.__dict__
243
244 templs = [
245 "^{host_rr}$",
246 "^{user_rr}@{host_rr}::{key_file_rr}$",
247 "^{user_rr}@{host_rr}:{port_rr}:{key_file_rr}$",
248 "^{user_rr}:{passwd_rr}@@{host_rr}$",
249 "^{user_rr}:{passwd_rr}@@{host_rr}:{port_rr}$",
250 ]
251
252 for templ in templs:
253 uri_reg_exprs.append(templ.format(**re_dct))
254
255
256def parse_ssh_uri(uri):
257 # user:passwd@@ip_host:port
258 # user:passwd@@ip_host
259 # user@ip_host:port
260 # user@ip_host
261 # ip_host:port
262 # ip_host
263 # user@ip_host:port:path_to_key_file
264 # user@ip_host::path_to_key_file
265 # ip_host:port:path_to_key_file
266 # ip_host::path_to_key_file
267
268 res = ConnCreds()
269 res.port = "22"
270 res.key_file = None
271 res.passwd = None
272
273 for rr in uri_reg_exprs:
274 rrm = re.match(rr, uri)
275 if rrm is not None:
276 res.__dict__.update(rrm.groupdict())
277 return res
koder aka kdanilov652cd802015-04-13 12:21:07 +0300278
koder aka kdanilove06762a2015-03-22 23:32:09 +0200279 raise ValueError("Can't parse {0!r} as ssh uri value".format(uri))
280
281
koder aka kdanilov168f6092015-04-19 02:33:38 +0300282def connect(uri, **params):
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300283 if uri == 'local':
284 return Local
285
koder aka kdanilove06762a2015-03-22 23:32:09 +0200286 creds = parse_ssh_uri(uri)
287 creds.port = int(creds.port)
koder aka kdanilov168f6092015-04-19 02:33:38 +0300288 return ssh_connect(creds, **params)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200289
290
koder aka kdanilov652cd802015-04-13 12:21:07 +0300291all_sessions_lock = threading.Lock()
292all_sessions = []
koder aka kdanilove06762a2015-03-22 23:32:09 +0200293
koder aka kdanilove06762a2015-03-22 23:32:09 +0200294
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300295def run_over_ssh(conn, cmd, stdin_data=None, timeout=60,
296 nolog=False, node=None):
koder aka kdanilov652cd802015-04-13 12:21:07 +0300297 "should be replaces by normal implementation, with select"
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300298
299 if conn is Local:
300 if not nolog:
301 logger.debug("SSH:local Exec {0!r}".format(cmd))
302 proc = subprocess.Popen(cmd, shell=True,
303 stdin=subprocess.PIPE,
304 stdout=subprocess.PIPE,
305 stderr=subprocess.STDOUT)
306
307 stdoutdata, _ = proc.communicate(input=stdin_data)
308
309 if proc.returncode != 0:
310 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
311 raise OSError(templ.format(node, cmd, proc.returncode, stdoutdata))
312
313 return stdoutdata
314
koder aka kdanilov652cd802015-04-13 12:21:07 +0300315 transport = conn.get_transport()
316 session = transport.open_session()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200317
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300318 if node is None:
319 node = ""
320
koder aka kdanilov652cd802015-04-13 12:21:07 +0300321 with all_sessions_lock:
322 all_sessions.append(session)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200323
koder aka kdanilov652cd802015-04-13 12:21:07 +0300324 try:
325 session.set_combine_stderr(True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200326
koder aka kdanilov652cd802015-04-13 12:21:07 +0300327 stime = time.time()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200328
koder aka kdanilov652cd802015-04-13 12:21:07 +0300329 if not nolog:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300330 logger.debug("SSH:{0} Exec {1!r}".format(node, cmd))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200331
koder aka kdanilov652cd802015-04-13 12:21:07 +0300332 session.exec_command(cmd)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200333
koder aka kdanilov652cd802015-04-13 12:21:07 +0300334 if stdin_data is not None:
335 session.sendall(stdin_data)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200336
koder aka kdanilov652cd802015-04-13 12:21:07 +0300337 session.settimeout(1)
338 session.shutdown_write()
339 output = ""
340
341 while True:
342 try:
343 ndata = session.recv(1024)
344 output += ndata
345 if "" == ndata:
346 break
347 except socket.timeout:
348 pass
349
350 if time.time() - stime > timeout:
351 raise OSError(output + "\nExecution timeout")
352
353 code = session.recv_exit_status()
354 finally:
355 session.close()
356
357 if code != 0:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300358 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
359 raise OSError(templ.format(node, cmd, code, output))
koder aka kdanilov652cd802015-04-13 12:21:07 +0300360
361 return output
362
363
364def close_all_sessions():
365 with all_sessions_lock:
366 for session in all_sessions:
367 try:
368 session.sendall('\x03')
369 session.close()
370 except:
371 pass