blob: d928a82459a1c7b28c1ccc96bf5375bdbda178b5 [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 kdanilov168f6092015-04-19 02:33:38 +030054def ssh_connect(creds, retry_count=6, timeout=10, log_warns=True):
koder aka kdanilov0c598a12015-04-21 03:01:40 +030055 if creds == 'local':
56 return Local
57
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020058 ssh = paramiko.SSHClient()
59 ssh.load_host_keys('/dev/null')
60 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
61 ssh.known_hosts = None
koder aka kdanilov168f6092015-04-19 02:33:38 +030062
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020063 for i in range(retry_count):
64 try:
65 if creds.user is None:
66 user = getpass.getuser()
67 else:
68 user = creds.user
69
70 if creds.passwd is not None:
71 ssh.connect(creds.host,
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030072 timeout=timeout, # tcp connect timeout
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020073 username=user,
74 password=creds.passwd,
75 port=creds.port,
76 allow_agent=False,
77 look_for_keys=False)
78 return ssh
79
80 if creds.key_file is not None:
81 ssh.connect(creds.host,
82 username=user,
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030083 timeout=timeout, # tcp connect timeout
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020084 key_filename=creds.key_file,
85 look_for_keys=False,
86 port=creds.port)
87 return ssh
88
89 key_file = os.path.expanduser('~/.ssh/id_rsa')
90 ssh.connect(creds.host,
91 username=user,
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030092 timeout=timeout, # tcp connect timeout
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020093 key_filename=key_file,
94 look_for_keys=False,
95 port=creds.port)
96 return ssh
97 # raise ValueError("Wrong credentials {0}".format(creds.__dict__))
98 except paramiko.PasswordRequiredException:
99 raise
koder aka kdanilov168f6092015-04-19 02:33:38 +0300100 except socket.error:
101 retry_left = retry_count - i - 1
102
koder aka kdanilove87ae652015-04-20 02:14:35 +0300103 if retry_left > 0:
104 if log_warns:
105 msg = "Node {0.host}:{0.port} connection timeout."
koder aka kdanilov168f6092015-04-19 02:33:38 +0300106
koder aka kdanilove87ae652015-04-20 02:14:35 +0300107 if 0 != retry_left:
108 msg += " {0} retry left.".format(retry_left)
koder aka kdanilov168f6092015-04-19 02:33:38 +0300109
koder aka kdanilove87ae652015-04-20 02:14:35 +0300110 logger.warning(msg.format(creds))
111 else:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200112 raise
koder aka kdanilov168f6092015-04-19 02:33:38 +0300113
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300114 time.sleep(1)
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200115
116
koder aka kdanilove06762a2015-03-22 23:32:09 +0200117def normalize_dirpath(dirpath):
118 while dirpath.endswith("/"):
119 dirpath = dirpath[:-1]
120 return dirpath
121
122
koder aka kdanilov2c473092015-03-29 17:12:13 +0300123ALL_RWX_MODE = ((1 << 9) - 1)
124
125
126def ssh_mkdir(sftp, remotepath, mode=ALL_RWX_MODE, intermediate=False):
koder aka kdanilove06762a2015-03-22 23:32:09 +0200127 remotepath = normalize_dirpath(remotepath)
128 if intermediate:
129 try:
130 sftp.mkdir(remotepath, mode=mode)
131 except IOError:
koder aka kdanilov168f6092015-04-19 02:33:38 +0300132 upper_dir = remotepath.rsplit("/", 1)[0]
133
134 if upper_dir == '' or upper_dir == '/':
135 raise
136
137 ssh_mkdir(sftp, upper_dir, mode=mode, intermediate=True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200138 return sftp.mkdir(remotepath, mode=mode)
139 else:
140 sftp.mkdir(remotepath, mode=mode)
141
142
143def ssh_copy_file(sftp, localfile, remfile, preserve_perm=True):
144 sftp.put(localfile, remfile)
145 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300146 sftp.chmod(remfile, os.stat(localfile).st_mode & ALL_RWX_MODE)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200147
148
149def put_dir_recursively(sftp, localpath, remotepath, preserve_perm=True):
150 "upload local directory to remote recursively"
151
152 # hack for localhost connection
153 if hasattr(sftp, "copytree"):
154 sftp.copytree(localpath, remotepath)
155 return
156
157 assert remotepath.startswith("/"), "%s must be absolute path" % remotepath
158
159 # normalize
160 localpath = normalize_dirpath(localpath)
161 remotepath = normalize_dirpath(remotepath)
162
163 try:
164 sftp.chdir(remotepath)
165 localsuffix = localpath.rsplit("/", 1)[1]
166 remotesuffix = remotepath.rsplit("/", 1)[1]
167 if localsuffix != remotesuffix:
168 remotepath = os.path.join(remotepath, localsuffix)
169 except IOError:
170 pass
171
172 for root, dirs, fls in os.walk(localpath):
173 prefix = os.path.commonprefix([localpath, root])
174 suffix = root.split(prefix, 1)[1]
175 if suffix.startswith("/"):
176 suffix = suffix[1:]
177
178 remroot = os.path.join(remotepath, suffix)
179
180 try:
181 sftp.chdir(remroot)
182 except IOError:
183 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300184 mode = os.stat(root).st_mode & ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200185 else:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300186 mode = ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200187 ssh_mkdir(sftp, remroot, mode=mode, intermediate=True)
188 sftp.chdir(remroot)
189
190 for f in fls:
191 remfile = os.path.join(remroot, f)
192 localfile = os.path.join(root, f)
193 ssh_copy_file(sftp, localfile, remfile, preserve_perm)
194
195
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300196def delete_file(conn, path):
197 sftp = conn.open_sftp()
198 sftp.remove(path)
199 sftp.close()
200
201
koder aka kdanilove06762a2015-03-22 23:32:09 +0200202def copy_paths(conn, paths):
203 sftp = conn.open_sftp()
204 try:
205 for src, dst in paths.items():
206 try:
207 if os.path.isfile(src):
208 ssh_copy_file(sftp, src, dst)
209 elif os.path.isdir(src):
210 put_dir_recursively(sftp, src, dst)
211 else:
212 templ = "Can't copy {0!r} - " + \
213 "it neither a file not a directory"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300214 raise OSError(templ.format(src))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200215 except Exception as exc:
216 tmpl = "Scp {0!r} => {1!r} failed - {2!r}"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300217 raise OSError(tmpl.format(src, dst, exc))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200218 finally:
219 sftp.close()
220
221
222class ConnCreds(object):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300223 conn_uri_attrs = ("user", "passwd", "host", "port", "path")
224
koder aka kdanilove06762a2015-03-22 23:32:09 +0200225 def __init__(self):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300226 for name in self.conn_uri_attrs:
koder aka kdanilove06762a2015-03-22 23:32:09 +0200227 setattr(self, name, None)
228
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300229 def __str__(self):
230 return str(self.__dict__)
231
koder aka kdanilove06762a2015-03-22 23:32:09 +0200232
233uri_reg_exprs = []
234
235
236class URIsNamespace(object):
237 class ReParts(object):
238 user_rr = "[^:]*?"
239 host_rr = "[^:]*?"
240 port_rr = "\\d+"
241 key_file_rr = "[^:@]*"
242 passwd_rr = ".*?"
243
244 re_dct = ReParts.__dict__
245
246 for attr_name, val in re_dct.items():
247 if attr_name.endswith('_rr'):
248 new_rr = "(?P<{0}>{1})".format(attr_name[:-3], val)
249 setattr(ReParts, attr_name, new_rr)
250
251 re_dct = ReParts.__dict__
252
253 templs = [
254 "^{host_rr}$",
255 "^{user_rr}@{host_rr}::{key_file_rr}$",
256 "^{user_rr}@{host_rr}:{port_rr}:{key_file_rr}$",
257 "^{user_rr}:{passwd_rr}@@{host_rr}$",
258 "^{user_rr}:{passwd_rr}@@{host_rr}:{port_rr}$",
259 ]
260
261 for templ in templs:
262 uri_reg_exprs.append(templ.format(**re_dct))
263
264
265def parse_ssh_uri(uri):
266 # user:passwd@@ip_host:port
267 # user:passwd@@ip_host
268 # user@ip_host:port
269 # user@ip_host
270 # ip_host:port
271 # ip_host
272 # user@ip_host:port:path_to_key_file
273 # user@ip_host::path_to_key_file
274 # ip_host:port:path_to_key_file
275 # ip_host::path_to_key_file
276
277 res = ConnCreds()
278 res.port = "22"
279 res.key_file = None
280 res.passwd = None
281
282 for rr in uri_reg_exprs:
283 rrm = re.match(rr, uri)
284 if rrm is not None:
285 res.__dict__.update(rrm.groupdict())
286 return res
koder aka kdanilov652cd802015-04-13 12:21:07 +0300287
koder aka kdanilove06762a2015-03-22 23:32:09 +0200288 raise ValueError("Can't parse {0!r} as ssh uri value".format(uri))
289
290
koder aka kdanilov168f6092015-04-19 02:33:38 +0300291def connect(uri, **params):
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300292 if uri == 'local':
293 return Local
294
koder aka kdanilove06762a2015-03-22 23:32:09 +0200295 creds = parse_ssh_uri(uri)
296 creds.port = int(creds.port)
koder aka kdanilov168f6092015-04-19 02:33:38 +0300297 return ssh_connect(creds, **params)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200298
299
koder aka kdanilov652cd802015-04-13 12:21:07 +0300300all_sessions_lock = threading.Lock()
301all_sessions = []
koder aka kdanilove06762a2015-03-22 23:32:09 +0200302
koder aka kdanilove06762a2015-03-22 23:32:09 +0200303
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300304def run_over_ssh(conn, cmd, stdin_data=None, timeout=60,
305 nolog=False, node=None):
koder aka kdanilov652cd802015-04-13 12:21:07 +0300306 "should be replaces by normal implementation, with select"
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300307
308 if conn is Local:
309 if not nolog:
310 logger.debug("SSH:local Exec {0!r}".format(cmd))
311 proc = subprocess.Popen(cmd, shell=True,
312 stdin=subprocess.PIPE,
313 stdout=subprocess.PIPE,
314 stderr=subprocess.STDOUT)
315
316 stdoutdata, _ = proc.communicate(input=stdin_data)
317
318 if proc.returncode != 0:
319 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
320 raise OSError(templ.format(node, cmd, proc.returncode, stdoutdata))
321
322 return stdoutdata
323
koder aka kdanilov652cd802015-04-13 12:21:07 +0300324 transport = conn.get_transport()
325 session = transport.open_session()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200326
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300327 if node is None:
328 node = ""
329
koder aka kdanilov652cd802015-04-13 12:21:07 +0300330 with all_sessions_lock:
331 all_sessions.append(session)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200332
koder aka kdanilov652cd802015-04-13 12:21:07 +0300333 try:
334 session.set_combine_stderr(True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200335
koder aka kdanilov652cd802015-04-13 12:21:07 +0300336 stime = time.time()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200337
koder aka kdanilov652cd802015-04-13 12:21:07 +0300338 if not nolog:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300339 logger.debug("SSH:{0} Exec {1!r}".format(node, cmd))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200340
koder aka kdanilov652cd802015-04-13 12:21:07 +0300341 session.exec_command(cmd)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200342
koder aka kdanilov652cd802015-04-13 12:21:07 +0300343 if stdin_data is not None:
344 session.sendall(stdin_data)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200345
koder aka kdanilov652cd802015-04-13 12:21:07 +0300346 session.settimeout(1)
347 session.shutdown_write()
348 output = ""
349
350 while True:
351 try:
352 ndata = session.recv(1024)
353 output += ndata
354 if "" == ndata:
355 break
356 except socket.timeout:
357 pass
358
359 if time.time() - stime > timeout:
360 raise OSError(output + "\nExecution timeout")
361
362 code = session.recv_exit_status()
363 finally:
364 session.close()
365
366 if code != 0:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300367 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
368 raise OSError(templ.format(node, cmd, code, output))
koder aka kdanilov652cd802015-04-13 12:21:07 +0300369
370 return output
371
372
373def close_all_sessions():
374 with all_sessions_lock:
375 for session in all_sessions:
376 try:
377 session.sendall('\x03')
378 session.close()
379 except:
380 pass