blob: 7b6d5938841451dbd9f2c19ac5a34c6b4b5dd964 [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 kdanilovf86d7af2015-05-06 04:01:54 +03008import StringIO
koder aka kdanilov652cd802015-04-13 12:21:07 +03009import threading
koder aka kdanilov0c598a12015-04-21 03:01:40 +030010import subprocess
koder aka kdanilov652cd802015-04-13 12:21:07 +030011
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020012import paramiko
koder aka kdanilove06762a2015-03-22 23:32:09 +020013
koder aka kdanilove06762a2015-03-22 23:32:09 +020014
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030015logger = logging.getLogger("wally")
koder aka kdanilove06762a2015-03-22 23:32:09 +020016
17
koder aka kdanilov0c598a12015-04-21 03:01:40 +030018class Local(object):
19 "placeholder for local node"
20 @classmethod
21 def open_sftp(cls):
koder aka kdanilovafd98742015-04-24 01:27:22 +030022 return cls()
koder aka kdanilov0c598a12015-04-21 03:01:40 +030023
24 @classmethod
25 def mkdir(cls, remotepath, mode=None):
26 os.mkdir(remotepath)
27 if mode is not None:
28 os.chmod(remotepath, mode)
29
30 @classmethod
31 def put(cls, localfile, remfile):
koder aka kdanilov4d4771c2015-04-23 01:32:02 +030032 dirname = os.path.dirname(remfile)
33 if not os.path.exists(dirname):
34 os.makedirs(dirname)
koder aka kdanilov0c598a12015-04-21 03:01:40 +030035 shutil.copyfile(localfile, remfile)
36
37 @classmethod
38 def chmod(cls, path, mode):
39 os.chmod(path, mode)
40
41 @classmethod
42 def copytree(cls, src, dst):
43 shutil.copytree(src, dst)
44
45 @classmethod
46 def remove(cls, path):
47 os.unlink(path)
48
49 @classmethod
50 def close(cls):
51 pass
52
53 @classmethod
54 def open(cls, *args, **kwarhgs):
55 return open(*args, **kwarhgs)
56
koder aka kdanilove2de58c2015-04-24 22:59:36 +030057 @classmethod
58 def stat(cls, path):
59 return os.stat(path)
60
koder aka kdanilov783b4542015-04-23 18:57:04 +030061 def __enter__(self):
62 return self
63
64 def __exit__(self, x, y, z):
65 return False
66
koder aka kdanilov0c598a12015-04-21 03:01:40 +030067
koder aka kdanilovf86d7af2015-05-06 04:01:54 +030068NODE_KEYS = {}
69
70
71def set_key_for_node(host_port, key):
72 sio = StringIO.StringIO(key)
73 NODE_KEYS[host_port] = paramiko.RSAKey.from_private_key(sio)
74 sio.close()
75
76
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030077def ssh_connect(creds, conn_timeout=60):
koder aka kdanilov0c598a12015-04-21 03:01:40 +030078 if creds == 'local':
79 return Local
80
koder aka kdanilov46d4f392015-04-24 11:35:00 +030081 tcp_timeout = 15
82 banner_timeout = 30
83
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020084 ssh = paramiko.SSHClient()
85 ssh.load_host_keys('/dev/null')
86 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
87 ssh.known_hosts = None
koder aka kdanilov168f6092015-04-19 02:33:38 +030088
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030089 etime = time.time() + conn_timeout
90
91 while True:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020092 try:
koder aka kdanilov46d4f392015-04-24 11:35:00 +030093 tleft = etime - time.time()
94 c_tcp_timeout = min(tcp_timeout, tleft)
95 c_banner_timeout = min(banner_timeout, tleft)
96
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020097 if creds.passwd is not None:
98 ssh.connect(creds.host,
koder aka kdanilov46d4f392015-04-24 11:35:00 +030099 timeout=c_tcp_timeout,
koder aka kdanilova4a570f2015-04-23 22:11:40 +0300100 username=creds.user,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200101 password=creds.passwd,
102 port=creds.port,
103 allow_agent=False,
koder aka kdanilov46d4f392015-04-24 11:35:00 +0300104 look_for_keys=False,
105 banner_timeout=c_banner_timeout)
106 elif creds.key_file is not None:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200107 ssh.connect(creds.host,
koder aka kdanilova4a570f2015-04-23 22:11:40 +0300108 username=creds.user,
koder aka kdanilov46d4f392015-04-24 11:35:00 +0300109 timeout=c_tcp_timeout,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200110 key_filename=creds.key_file,
111 look_for_keys=False,
koder aka kdanilov46d4f392015-04-24 11:35:00 +0300112 port=creds.port,
113 banner_timeout=c_banner_timeout)
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300114 elif (creds.host, creds.port) in NODE_KEYS:
115 ssh.connect(creds.host,
116 username=creds.user,
117 timeout=c_tcp_timeout,
118 pkey=NODE_KEYS[(creds.host, creds.port)],
119 look_for_keys=False,
120 port=creds.port,
121 banner_timeout=c_banner_timeout)
koder aka kdanilov46d4f392015-04-24 11:35:00 +0300122 else:
123 key_file = os.path.expanduser('~/.ssh/id_rsa')
124 ssh.connect(creds.host,
125 username=creds.user,
126 timeout=c_tcp_timeout,
127 key_filename=key_file,
128 look_for_keys=False,
129 port=creds.port,
130 banner_timeout=c_banner_timeout)
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200131 return ssh
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200132 except paramiko.PasswordRequiredException:
133 raise
koder aka kdanilov46d4f392015-04-24 11:35:00 +0300134 except (socket.error, paramiko.SSHException):
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300135 if time.time() > etime:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200136 raise
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300137 time.sleep(1)
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200138
139
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300140def save_to_remote(sftp, path, content):
141 with sftp.open(path, "wb") as fd:
142 fd.write(content)
143
144
145def read_from_remote(sftp, path):
146 with sftp.open(path, "rb") as fd:
147 return fd.read()
148
149
koder aka kdanilove06762a2015-03-22 23:32:09 +0200150def normalize_dirpath(dirpath):
151 while dirpath.endswith("/"):
152 dirpath = dirpath[:-1]
153 return dirpath
154
155
koder aka kdanilov2c473092015-03-29 17:12:13 +0300156ALL_RWX_MODE = ((1 << 9) - 1)
157
158
159def ssh_mkdir(sftp, remotepath, mode=ALL_RWX_MODE, intermediate=False):
koder aka kdanilove06762a2015-03-22 23:32:09 +0200160 remotepath = normalize_dirpath(remotepath)
161 if intermediate:
162 try:
163 sftp.mkdir(remotepath, mode=mode)
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300164 except (IOError, OSError):
koder aka kdanilov168f6092015-04-19 02:33:38 +0300165 upper_dir = remotepath.rsplit("/", 1)[0]
166
167 if upper_dir == '' or upper_dir == '/':
168 raise
169
170 ssh_mkdir(sftp, upper_dir, mode=mode, intermediate=True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200171 return sftp.mkdir(remotepath, mode=mode)
172 else:
173 sftp.mkdir(remotepath, mode=mode)
174
175
176def ssh_copy_file(sftp, localfile, remfile, preserve_perm=True):
177 sftp.put(localfile, remfile)
178 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300179 sftp.chmod(remfile, os.stat(localfile).st_mode & ALL_RWX_MODE)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200180
181
182def put_dir_recursively(sftp, localpath, remotepath, preserve_perm=True):
183 "upload local directory to remote recursively"
184
185 # hack for localhost connection
186 if hasattr(sftp, "copytree"):
187 sftp.copytree(localpath, remotepath)
188 return
189
190 assert remotepath.startswith("/"), "%s must be absolute path" % remotepath
191
192 # normalize
193 localpath = normalize_dirpath(localpath)
194 remotepath = normalize_dirpath(remotepath)
195
196 try:
197 sftp.chdir(remotepath)
198 localsuffix = localpath.rsplit("/", 1)[1]
199 remotesuffix = remotepath.rsplit("/", 1)[1]
200 if localsuffix != remotesuffix:
201 remotepath = os.path.join(remotepath, localsuffix)
202 except IOError:
203 pass
204
205 for root, dirs, fls in os.walk(localpath):
206 prefix = os.path.commonprefix([localpath, root])
207 suffix = root.split(prefix, 1)[1]
208 if suffix.startswith("/"):
209 suffix = suffix[1:]
210
211 remroot = os.path.join(remotepath, suffix)
212
213 try:
214 sftp.chdir(remroot)
215 except IOError:
216 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300217 mode = os.stat(root).st_mode & ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200218 else:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300219 mode = ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200220 ssh_mkdir(sftp, remroot, mode=mode, intermediate=True)
221 sftp.chdir(remroot)
222
223 for f in fls:
224 remfile = os.path.join(remroot, f)
225 localfile = os.path.join(root, f)
226 ssh_copy_file(sftp, localfile, remfile, preserve_perm)
227
228
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300229def delete_file(conn, path):
230 sftp = conn.open_sftp()
231 sftp.remove(path)
232 sftp.close()
233
234
koder aka kdanilove06762a2015-03-22 23:32:09 +0200235def copy_paths(conn, paths):
236 sftp = conn.open_sftp()
237 try:
238 for src, dst in paths.items():
239 try:
240 if os.path.isfile(src):
241 ssh_copy_file(sftp, src, dst)
242 elif os.path.isdir(src):
243 put_dir_recursively(sftp, src, dst)
244 else:
245 templ = "Can't copy {0!r} - " + \
246 "it neither a file not a directory"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300247 raise OSError(templ.format(src))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200248 except Exception as exc:
249 tmpl = "Scp {0!r} => {1!r} failed - {2!r}"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300250 raise OSError(tmpl.format(src, dst, exc))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200251 finally:
252 sftp.close()
253
254
255class ConnCreds(object):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300256 conn_uri_attrs = ("user", "passwd", "host", "port", "path")
257
koder aka kdanilove06762a2015-03-22 23:32:09 +0200258 def __init__(self):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300259 for name in self.conn_uri_attrs:
koder aka kdanilove06762a2015-03-22 23:32:09 +0200260 setattr(self, name, None)
261
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300262 def __str__(self):
263 return str(self.__dict__)
264
koder aka kdanilove06762a2015-03-22 23:32:09 +0200265
266uri_reg_exprs = []
267
268
269class URIsNamespace(object):
270 class ReParts(object):
271 user_rr = "[^:]*?"
koder aka kdanilov7e0f7cf2015-05-01 17:24:35 +0300272 host_rr = "[^:@]*?"
koder aka kdanilove06762a2015-03-22 23:32:09 +0200273 port_rr = "\\d+"
274 key_file_rr = "[^:@]*"
275 passwd_rr = ".*?"
276
277 re_dct = ReParts.__dict__
278
279 for attr_name, val in re_dct.items():
280 if attr_name.endswith('_rr'):
281 new_rr = "(?P<{0}>{1})".format(attr_name[:-3], val)
282 setattr(ReParts, attr_name, new_rr)
283
284 re_dct = ReParts.__dict__
285
286 templs = [
287 "^{host_rr}$",
koder aka kdanilov7e0f7cf2015-05-01 17:24:35 +0300288 "^{host_rr}:{port_rr}$",
289 "^{user_rr}@{host_rr}$",
290 "^{user_rr}@{host_rr}:{port_rr}$",
koder aka kdanilove06762a2015-03-22 23:32:09 +0200291 "^{user_rr}@{host_rr}::{key_file_rr}$",
292 "^{user_rr}@{host_rr}:{port_rr}:{key_file_rr}$",
koder aka kdanilov7e0f7cf2015-05-01 17:24:35 +0300293 "^{user_rr}:{passwd_rr}@{host_rr}$",
294 "^{user_rr}:{passwd_rr}@{host_rr}:{port_rr}$",
koder aka kdanilove06762a2015-03-22 23:32:09 +0200295 ]
296
297 for templ in templs:
298 uri_reg_exprs.append(templ.format(**re_dct))
299
300
301def parse_ssh_uri(uri):
koder aka kdanilov7e0f7cf2015-05-01 17:24:35 +0300302 # user:passwd@ip_host:port
303 # user:passwd@ip_host
koder aka kdanilove06762a2015-03-22 23:32:09 +0200304 # user@ip_host:port
305 # user@ip_host
306 # ip_host:port
307 # ip_host
308 # user@ip_host:port:path_to_key_file
309 # user@ip_host::path_to_key_file
310 # ip_host:port:path_to_key_file
311 # ip_host::path_to_key_file
312
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300313 if uri.startswith("ssh://"):
314 uri = uri[len("ssh://"):]
315
koder aka kdanilove06762a2015-03-22 23:32:09 +0200316 res = ConnCreds()
317 res.port = "22"
318 res.key_file = None
319 res.passwd = None
koder aka kdanilova4a570f2015-04-23 22:11:40 +0300320 res.user = getpass.getuser()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200321
322 for rr in uri_reg_exprs:
323 rrm = re.match(rr, uri)
324 if rrm is not None:
325 res.__dict__.update(rrm.groupdict())
326 return res
koder aka kdanilov652cd802015-04-13 12:21:07 +0300327
koder aka kdanilove06762a2015-03-22 23:32:09 +0200328 raise ValueError("Can't parse {0!r} as ssh uri value".format(uri))
329
330
koder aka kdanilov168f6092015-04-19 02:33:38 +0300331def connect(uri, **params):
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300332 if uri == 'local':
333 return Local
334
koder aka kdanilove06762a2015-03-22 23:32:09 +0200335 creds = parse_ssh_uri(uri)
336 creds.port = int(creds.port)
koder aka kdanilov168f6092015-04-19 02:33:38 +0300337 return ssh_connect(creds, **params)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200338
339
koder aka kdanilov652cd802015-04-13 12:21:07 +0300340all_sessions_lock = threading.Lock()
341all_sessions = []
koder aka kdanilove06762a2015-03-22 23:32:09 +0200342
koder aka kdanilove06762a2015-03-22 23:32:09 +0200343
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300344def run_over_ssh(conn, cmd, stdin_data=None, timeout=60,
345 nolog=False, node=None):
koder aka kdanilov652cd802015-04-13 12:21:07 +0300346 "should be replaces by normal implementation, with select"
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300347
348 if conn is Local:
349 if not nolog:
350 logger.debug("SSH:local Exec {0!r}".format(cmd))
351 proc = subprocess.Popen(cmd, shell=True,
352 stdin=subprocess.PIPE,
353 stdout=subprocess.PIPE,
354 stderr=subprocess.STDOUT)
355
356 stdoutdata, _ = proc.communicate(input=stdin_data)
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300357 if proc.returncode != 0:
358 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
359 raise OSError(templ.format(node, cmd, proc.returncode, stdoutdata))
360
361 return stdoutdata
362
koder aka kdanilov652cd802015-04-13 12:21:07 +0300363 transport = conn.get_transport()
364 session = transport.open_session()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200365
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300366 if node is None:
367 node = ""
368
koder aka kdanilov652cd802015-04-13 12:21:07 +0300369 with all_sessions_lock:
370 all_sessions.append(session)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200371
koder aka kdanilov652cd802015-04-13 12:21:07 +0300372 try:
373 session.set_combine_stderr(True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200374
koder aka kdanilov652cd802015-04-13 12:21:07 +0300375 stime = time.time()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200376
koder aka kdanilov652cd802015-04-13 12:21:07 +0300377 if not nolog:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300378 logger.debug("SSH:{0} Exec {1!r}".format(node, cmd))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200379
koder aka kdanilov652cd802015-04-13 12:21:07 +0300380 session.exec_command(cmd)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200381
koder aka kdanilov652cd802015-04-13 12:21:07 +0300382 if stdin_data is not None:
383 session.sendall(stdin_data)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200384
koder aka kdanilov652cd802015-04-13 12:21:07 +0300385 session.settimeout(1)
386 session.shutdown_write()
387 output = ""
388
389 while True:
390 try:
391 ndata = session.recv(1024)
392 output += ndata
393 if "" == ndata:
394 break
395 except socket.timeout:
396 pass
397
398 if time.time() - stime > timeout:
399 raise OSError(output + "\nExecution timeout")
400
401 code = session.recv_exit_status()
402 finally:
403 session.close()
404
405 if code != 0:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300406 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
407 raise OSError(templ.format(node, cmd, code, output))
koder aka kdanilov652cd802015-04-13 12:21:07 +0300408
409 return output
410
411
412def close_all_sessions():
413 with all_sessions_lock:
414 for session in all_sessions:
415 try:
416 session.sendall('\x03')
417 session.close()
418 except:
419 pass