blob: d3d654747485745e55c9dccd7b76c18e35374feb [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):
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
56
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030057def ssh_connect(creds, conn_timeout=60):
koder aka kdanilov0c598a12015-04-21 03:01:40 +030058 if creds == 'local':
59 return Local
60
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030061 tcp_timeout = 30
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020062 ssh = paramiko.SSHClient()
63 ssh.load_host_keys('/dev/null')
64 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
65 ssh.known_hosts = None
koder aka kdanilov168f6092015-04-19 02:33:38 +030066
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030067 etime = time.time() + conn_timeout
68
69 while True:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020070 try:
71 if creds.user is None:
72 user = getpass.getuser()
73 else:
74 user = creds.user
75
76 if creds.passwd is not None:
77 ssh.connect(creds.host,
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030078 timeout=tcp_timeout,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020079 username=user,
80 password=creds.passwd,
81 port=creds.port,
82 allow_agent=False,
83 look_for_keys=False)
84 return ssh
85
86 if creds.key_file is not None:
87 ssh.connect(creds.host,
88 username=user,
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030089 timeout=tcp_timeout,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020090 key_filename=creds.key_file,
91 look_for_keys=False,
92 port=creds.port)
93 return ssh
94
95 key_file = os.path.expanduser('~/.ssh/id_rsa')
96 ssh.connect(creds.host,
97 username=user,
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030098 timeout=tcp_timeout,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020099 key_filename=key_file,
100 look_for_keys=False,
101 port=creds.port)
102 return ssh
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200103 except paramiko.PasswordRequiredException:
104 raise
koder aka kdanilov168f6092015-04-19 02:33:38 +0300105 except socket.error:
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300106 if time.time() > etime:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200107 raise
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300108 time.sleep(1)
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200109
110
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300111def save_to_remote(sftp, path, content):
112 with sftp.open(path, "wb") as fd:
113 fd.write(content)
114
115
116def read_from_remote(sftp, path):
117 with sftp.open(path, "rb") as fd:
118 return fd.read()
119
120
koder aka kdanilove06762a2015-03-22 23:32:09 +0200121def normalize_dirpath(dirpath):
122 while dirpath.endswith("/"):
123 dirpath = dirpath[:-1]
124 return dirpath
125
126
koder aka kdanilov2c473092015-03-29 17:12:13 +0300127ALL_RWX_MODE = ((1 << 9) - 1)
128
129
130def ssh_mkdir(sftp, remotepath, mode=ALL_RWX_MODE, intermediate=False):
koder aka kdanilove06762a2015-03-22 23:32:09 +0200131 remotepath = normalize_dirpath(remotepath)
132 if intermediate:
133 try:
134 sftp.mkdir(remotepath, mode=mode)
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300135 except (IOError, OSError):
koder aka kdanilov168f6092015-04-19 02:33:38 +0300136 upper_dir = remotepath.rsplit("/", 1)[0]
137
138 if upper_dir == '' or upper_dir == '/':
139 raise
140
141 ssh_mkdir(sftp, upper_dir, mode=mode, intermediate=True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200142 return sftp.mkdir(remotepath, mode=mode)
143 else:
144 sftp.mkdir(remotepath, mode=mode)
145
146
147def ssh_copy_file(sftp, localfile, remfile, preserve_perm=True):
148 sftp.put(localfile, remfile)
149 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300150 sftp.chmod(remfile, os.stat(localfile).st_mode & ALL_RWX_MODE)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200151
152
153def put_dir_recursively(sftp, localpath, remotepath, preserve_perm=True):
154 "upload local directory to remote recursively"
155
156 # hack for localhost connection
157 if hasattr(sftp, "copytree"):
158 sftp.copytree(localpath, remotepath)
159 return
160
161 assert remotepath.startswith("/"), "%s must be absolute path" % remotepath
162
163 # normalize
164 localpath = normalize_dirpath(localpath)
165 remotepath = normalize_dirpath(remotepath)
166
167 try:
168 sftp.chdir(remotepath)
169 localsuffix = localpath.rsplit("/", 1)[1]
170 remotesuffix = remotepath.rsplit("/", 1)[1]
171 if localsuffix != remotesuffix:
172 remotepath = os.path.join(remotepath, localsuffix)
173 except IOError:
174 pass
175
176 for root, dirs, fls in os.walk(localpath):
177 prefix = os.path.commonprefix([localpath, root])
178 suffix = root.split(prefix, 1)[1]
179 if suffix.startswith("/"):
180 suffix = suffix[1:]
181
182 remroot = os.path.join(remotepath, suffix)
183
184 try:
185 sftp.chdir(remroot)
186 except IOError:
187 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300188 mode = os.stat(root).st_mode & ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200189 else:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300190 mode = ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200191 ssh_mkdir(sftp, remroot, mode=mode, intermediate=True)
192 sftp.chdir(remroot)
193
194 for f in fls:
195 remfile = os.path.join(remroot, f)
196 localfile = os.path.join(root, f)
197 ssh_copy_file(sftp, localfile, remfile, preserve_perm)
198
199
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300200def delete_file(conn, path):
201 sftp = conn.open_sftp()
202 sftp.remove(path)
203 sftp.close()
204
205
koder aka kdanilove06762a2015-03-22 23:32:09 +0200206def copy_paths(conn, paths):
207 sftp = conn.open_sftp()
208 try:
209 for src, dst in paths.items():
210 try:
211 if os.path.isfile(src):
212 ssh_copy_file(sftp, src, dst)
213 elif os.path.isdir(src):
214 put_dir_recursively(sftp, src, dst)
215 else:
216 templ = "Can't copy {0!r} - " + \
217 "it neither a file not a directory"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300218 raise OSError(templ.format(src))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200219 except Exception as exc:
220 tmpl = "Scp {0!r} => {1!r} failed - {2!r}"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300221 raise OSError(tmpl.format(src, dst, exc))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200222 finally:
223 sftp.close()
224
225
226class ConnCreds(object):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300227 conn_uri_attrs = ("user", "passwd", "host", "port", "path")
228
koder aka kdanilove06762a2015-03-22 23:32:09 +0200229 def __init__(self):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300230 for name in self.conn_uri_attrs:
koder aka kdanilove06762a2015-03-22 23:32:09 +0200231 setattr(self, name, None)
232
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300233 def __str__(self):
234 return str(self.__dict__)
235
koder aka kdanilove06762a2015-03-22 23:32:09 +0200236
237uri_reg_exprs = []
238
239
240class URIsNamespace(object):
241 class ReParts(object):
242 user_rr = "[^:]*?"
243 host_rr = "[^:]*?"
244 port_rr = "\\d+"
245 key_file_rr = "[^:@]*"
246 passwd_rr = ".*?"
247
248 re_dct = ReParts.__dict__
249
250 for attr_name, val in re_dct.items():
251 if attr_name.endswith('_rr'):
252 new_rr = "(?P<{0}>{1})".format(attr_name[:-3], val)
253 setattr(ReParts, attr_name, new_rr)
254
255 re_dct = ReParts.__dict__
256
257 templs = [
258 "^{host_rr}$",
259 "^{user_rr}@{host_rr}::{key_file_rr}$",
260 "^{user_rr}@{host_rr}:{port_rr}:{key_file_rr}$",
261 "^{user_rr}:{passwd_rr}@@{host_rr}$",
262 "^{user_rr}:{passwd_rr}@@{host_rr}:{port_rr}$",
263 ]
264
265 for templ in templs:
266 uri_reg_exprs.append(templ.format(**re_dct))
267
268
269def parse_ssh_uri(uri):
270 # user:passwd@@ip_host:port
271 # user:passwd@@ip_host
272 # user@ip_host:port
273 # user@ip_host
274 # ip_host:port
275 # ip_host
276 # user@ip_host:port:path_to_key_file
277 # user@ip_host::path_to_key_file
278 # ip_host:port:path_to_key_file
279 # ip_host::path_to_key_file
280
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300281 if uri.startswith("ssh://"):
282 uri = uri[len("ssh://"):]
283
koder aka kdanilove06762a2015-03-22 23:32:09 +0200284 res = ConnCreds()
285 res.port = "22"
286 res.key_file = None
287 res.passwd = None
288
289 for rr in uri_reg_exprs:
290 rrm = re.match(rr, uri)
291 if rrm is not None:
292 res.__dict__.update(rrm.groupdict())
293 return res
koder aka kdanilov652cd802015-04-13 12:21:07 +0300294
koder aka kdanilove06762a2015-03-22 23:32:09 +0200295 raise ValueError("Can't parse {0!r} as ssh uri value".format(uri))
296
297
koder aka kdanilov168f6092015-04-19 02:33:38 +0300298def connect(uri, **params):
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300299 if uri == 'local':
300 return Local
301
koder aka kdanilove06762a2015-03-22 23:32:09 +0200302 creds = parse_ssh_uri(uri)
303 creds.port = int(creds.port)
koder aka kdanilov168f6092015-04-19 02:33:38 +0300304 return ssh_connect(creds, **params)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200305
306
koder aka kdanilov652cd802015-04-13 12:21:07 +0300307all_sessions_lock = threading.Lock()
308all_sessions = []
koder aka kdanilove06762a2015-03-22 23:32:09 +0200309
koder aka kdanilove06762a2015-03-22 23:32:09 +0200310
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300311def run_over_ssh(conn, cmd, stdin_data=None, timeout=60,
312 nolog=False, node=None):
koder aka kdanilov652cd802015-04-13 12:21:07 +0300313 "should be replaces by normal implementation, with select"
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300314
315 if conn is Local:
316 if not nolog:
317 logger.debug("SSH:local Exec {0!r}".format(cmd))
318 proc = subprocess.Popen(cmd, shell=True,
319 stdin=subprocess.PIPE,
320 stdout=subprocess.PIPE,
321 stderr=subprocess.STDOUT)
322
323 stdoutdata, _ = proc.communicate(input=stdin_data)
324
325 if proc.returncode != 0:
326 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
327 raise OSError(templ.format(node, cmd, proc.returncode, stdoutdata))
328
329 return stdoutdata
330
koder aka kdanilov652cd802015-04-13 12:21:07 +0300331 transport = conn.get_transport()
332 session = transport.open_session()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200333
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300334 if node is None:
335 node = ""
336
koder aka kdanilov652cd802015-04-13 12:21:07 +0300337 with all_sessions_lock:
338 all_sessions.append(session)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200339
koder aka kdanilov652cd802015-04-13 12:21:07 +0300340 try:
341 session.set_combine_stderr(True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200342
koder aka kdanilov652cd802015-04-13 12:21:07 +0300343 stime = time.time()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200344
koder aka kdanilov652cd802015-04-13 12:21:07 +0300345 if not nolog:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300346 logger.debug("SSH:{0} Exec {1!r}".format(node, cmd))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200347
koder aka kdanilov652cd802015-04-13 12:21:07 +0300348 session.exec_command(cmd)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200349
koder aka kdanilov652cd802015-04-13 12:21:07 +0300350 if stdin_data is not None:
351 session.sendall(stdin_data)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200352
koder aka kdanilov652cd802015-04-13 12:21:07 +0300353 session.settimeout(1)
354 session.shutdown_write()
355 output = ""
356
357 while True:
358 try:
359 ndata = session.recv(1024)
360 output += ndata
361 if "" == ndata:
362 break
363 except socket.timeout:
364 pass
365
366 if time.time() - stime > timeout:
367 raise OSError(output + "\nExecution timeout")
368
369 code = session.recv_exit_status()
370 finally:
371 session.close()
372
373 if code != 0:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300374 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
375 raise OSError(templ.format(node, cmd, code, output))
koder aka kdanilov652cd802015-04-13 12:21:07 +0300376
377 return output
378
379
380def close_all_sessions():
381 with all_sessions_lock:
382 for session in all_sessions:
383 try:
384 session.sendall('\x03')
385 session.close()
386 except:
387 pass