blob: 81f348c9c2e166563a55ca59f35ac8d3f44a117e [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 kdanilov783b4542015-04-23 18:57:04 +030056 def __enter__(self):
57 return self
58
59 def __exit__(self, x, y, z):
60 return False
61
koder aka kdanilov0c598a12015-04-21 03:01:40 +030062
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030063def ssh_connect(creds, conn_timeout=60):
koder aka kdanilov0c598a12015-04-21 03:01:40 +030064 if creds == 'local':
65 return Local
66
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030067 tcp_timeout = 30
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020068 ssh = paramiko.SSHClient()
69 ssh.load_host_keys('/dev/null')
70 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
71 ssh.known_hosts = None
koder aka kdanilov168f6092015-04-19 02:33:38 +030072
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030073 etime = time.time() + conn_timeout
74
75 while True:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020076 try:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020077 if creds.passwd is not None:
78 ssh.connect(creds.host,
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030079 timeout=tcp_timeout,
koder aka kdanilova4a570f2015-04-23 22:11:40 +030080 username=creds.user,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020081 password=creds.passwd,
82 port=creds.port,
83 allow_agent=False,
84 look_for_keys=False)
85 return ssh
86
87 if creds.key_file is not None:
88 ssh.connect(creds.host,
koder aka kdanilova4a570f2015-04-23 22:11:40 +030089 username=creds.user,
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030090 timeout=tcp_timeout,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020091 key_filename=creds.key_file,
92 look_for_keys=False,
93 port=creds.port)
94 return ssh
95
96 key_file = os.path.expanduser('~/.ssh/id_rsa')
97 ssh.connect(creds.host,
koder aka kdanilova4a570f2015-04-23 22:11:40 +030098 username=creds.user,
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030099 timeout=tcp_timeout,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200100 key_filename=key_file,
101 look_for_keys=False,
102 port=creds.port)
103 return ssh
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200104 except paramiko.PasswordRequiredException:
105 raise
koder aka kdanilov168f6092015-04-19 02:33:38 +0300106 except socket.error:
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300107 if time.time() > etime:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200108 raise
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300109 time.sleep(1)
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200110
111
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300112def save_to_remote(sftp, path, content):
113 with sftp.open(path, "wb") as fd:
114 fd.write(content)
115
116
117def read_from_remote(sftp, path):
118 with sftp.open(path, "rb") as fd:
119 return fd.read()
120
121
koder aka kdanilove06762a2015-03-22 23:32:09 +0200122def normalize_dirpath(dirpath):
123 while dirpath.endswith("/"):
124 dirpath = dirpath[:-1]
125 return dirpath
126
127
koder aka kdanilov2c473092015-03-29 17:12:13 +0300128ALL_RWX_MODE = ((1 << 9) - 1)
129
130
131def ssh_mkdir(sftp, remotepath, mode=ALL_RWX_MODE, intermediate=False):
koder aka kdanilove06762a2015-03-22 23:32:09 +0200132 remotepath = normalize_dirpath(remotepath)
133 if intermediate:
134 try:
135 sftp.mkdir(remotepath, mode=mode)
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300136 except (IOError, OSError):
koder aka kdanilov168f6092015-04-19 02:33:38 +0300137 upper_dir = remotepath.rsplit("/", 1)[0]
138
139 if upper_dir == '' or upper_dir == '/':
140 raise
141
142 ssh_mkdir(sftp, upper_dir, mode=mode, intermediate=True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200143 return sftp.mkdir(remotepath, mode=mode)
144 else:
145 sftp.mkdir(remotepath, mode=mode)
146
147
148def ssh_copy_file(sftp, localfile, remfile, preserve_perm=True):
149 sftp.put(localfile, remfile)
150 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300151 sftp.chmod(remfile, os.stat(localfile).st_mode & ALL_RWX_MODE)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200152
153
154def put_dir_recursively(sftp, localpath, remotepath, preserve_perm=True):
155 "upload local directory to remote recursively"
156
157 # hack for localhost connection
158 if hasattr(sftp, "copytree"):
159 sftp.copytree(localpath, remotepath)
160 return
161
162 assert remotepath.startswith("/"), "%s must be absolute path" % remotepath
163
164 # normalize
165 localpath = normalize_dirpath(localpath)
166 remotepath = normalize_dirpath(remotepath)
167
168 try:
169 sftp.chdir(remotepath)
170 localsuffix = localpath.rsplit("/", 1)[1]
171 remotesuffix = remotepath.rsplit("/", 1)[1]
172 if localsuffix != remotesuffix:
173 remotepath = os.path.join(remotepath, localsuffix)
174 except IOError:
175 pass
176
177 for root, dirs, fls in os.walk(localpath):
178 prefix = os.path.commonprefix([localpath, root])
179 suffix = root.split(prefix, 1)[1]
180 if suffix.startswith("/"):
181 suffix = suffix[1:]
182
183 remroot = os.path.join(remotepath, suffix)
184
185 try:
186 sftp.chdir(remroot)
187 except IOError:
188 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300189 mode = os.stat(root).st_mode & ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200190 else:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300191 mode = ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200192 ssh_mkdir(sftp, remroot, mode=mode, intermediate=True)
193 sftp.chdir(remroot)
194
195 for f in fls:
196 remfile = os.path.join(remroot, f)
197 localfile = os.path.join(root, f)
198 ssh_copy_file(sftp, localfile, remfile, preserve_perm)
199
200
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300201def delete_file(conn, path):
202 sftp = conn.open_sftp()
203 sftp.remove(path)
204 sftp.close()
205
206
koder aka kdanilove06762a2015-03-22 23:32:09 +0200207def copy_paths(conn, paths):
208 sftp = conn.open_sftp()
209 try:
210 for src, dst in paths.items():
211 try:
212 if os.path.isfile(src):
213 ssh_copy_file(sftp, src, dst)
214 elif os.path.isdir(src):
215 put_dir_recursively(sftp, src, dst)
216 else:
217 templ = "Can't copy {0!r} - " + \
218 "it neither a file not a directory"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300219 raise OSError(templ.format(src))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200220 except Exception as exc:
221 tmpl = "Scp {0!r} => {1!r} failed - {2!r}"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300222 raise OSError(tmpl.format(src, dst, exc))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200223 finally:
224 sftp.close()
225
226
227class ConnCreds(object):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300228 conn_uri_attrs = ("user", "passwd", "host", "port", "path")
229
koder aka kdanilove06762a2015-03-22 23:32:09 +0200230 def __init__(self):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300231 for name in self.conn_uri_attrs:
koder aka kdanilove06762a2015-03-22 23:32:09 +0200232 setattr(self, name, None)
233
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300234 def __str__(self):
235 return str(self.__dict__)
236
koder aka kdanilove06762a2015-03-22 23:32:09 +0200237
238uri_reg_exprs = []
239
240
241class URIsNamespace(object):
242 class ReParts(object):
243 user_rr = "[^:]*?"
244 host_rr = "[^:]*?"
245 port_rr = "\\d+"
246 key_file_rr = "[^:@]*"
247 passwd_rr = ".*?"
248
249 re_dct = ReParts.__dict__
250
251 for attr_name, val in re_dct.items():
252 if attr_name.endswith('_rr'):
253 new_rr = "(?P<{0}>{1})".format(attr_name[:-3], val)
254 setattr(ReParts, attr_name, new_rr)
255
256 re_dct = ReParts.__dict__
257
258 templs = [
259 "^{host_rr}$",
260 "^{user_rr}@{host_rr}::{key_file_rr}$",
261 "^{user_rr}@{host_rr}:{port_rr}:{key_file_rr}$",
262 "^{user_rr}:{passwd_rr}@@{host_rr}$",
263 "^{user_rr}:{passwd_rr}@@{host_rr}:{port_rr}$",
264 ]
265
266 for templ in templs:
267 uri_reg_exprs.append(templ.format(**re_dct))
268
269
270def parse_ssh_uri(uri):
271 # user:passwd@@ip_host:port
272 # user:passwd@@ip_host
273 # user@ip_host:port
274 # user@ip_host
275 # ip_host:port
276 # ip_host
277 # user@ip_host:port:path_to_key_file
278 # user@ip_host::path_to_key_file
279 # ip_host:port:path_to_key_file
280 # ip_host::path_to_key_file
281
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300282 if uri.startswith("ssh://"):
283 uri = uri[len("ssh://"):]
284
koder aka kdanilove06762a2015-03-22 23:32:09 +0200285 res = ConnCreds()
286 res.port = "22"
287 res.key_file = None
288 res.passwd = None
koder aka kdanilova4a570f2015-04-23 22:11:40 +0300289 res.user = getpass.getuser()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200290
291 for rr in uri_reg_exprs:
292 rrm = re.match(rr, uri)
293 if rrm is not None:
294 res.__dict__.update(rrm.groupdict())
295 return res
koder aka kdanilov652cd802015-04-13 12:21:07 +0300296
koder aka kdanilove06762a2015-03-22 23:32:09 +0200297 raise ValueError("Can't parse {0!r} as ssh uri value".format(uri))
298
299
koder aka kdanilov168f6092015-04-19 02:33:38 +0300300def connect(uri, **params):
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300301 if uri == 'local':
302 return Local
303
koder aka kdanilove06762a2015-03-22 23:32:09 +0200304 creds = parse_ssh_uri(uri)
305 creds.port = int(creds.port)
koder aka kdanilov168f6092015-04-19 02:33:38 +0300306 return ssh_connect(creds, **params)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200307
308
koder aka kdanilov652cd802015-04-13 12:21:07 +0300309all_sessions_lock = threading.Lock()
310all_sessions = []
koder aka kdanilove06762a2015-03-22 23:32:09 +0200311
koder aka kdanilove06762a2015-03-22 23:32:09 +0200312
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300313def run_over_ssh(conn, cmd, stdin_data=None, timeout=60,
314 nolog=False, node=None):
koder aka kdanilov652cd802015-04-13 12:21:07 +0300315 "should be replaces by normal implementation, with select"
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300316
317 if conn is Local:
318 if not nolog:
319 logger.debug("SSH:local Exec {0!r}".format(cmd))
320 proc = subprocess.Popen(cmd, shell=True,
321 stdin=subprocess.PIPE,
322 stdout=subprocess.PIPE,
323 stderr=subprocess.STDOUT)
324
325 stdoutdata, _ = proc.communicate(input=stdin_data)
326
327 if proc.returncode != 0:
328 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
329 raise OSError(templ.format(node, cmd, proc.returncode, stdoutdata))
330
331 return stdoutdata
332
koder aka kdanilov652cd802015-04-13 12:21:07 +0300333 transport = conn.get_transport()
334 session = transport.open_session()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200335
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300336 if node is None:
337 node = ""
338
koder aka kdanilov652cd802015-04-13 12:21:07 +0300339 with all_sessions_lock:
340 all_sessions.append(session)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200341
koder aka kdanilov652cd802015-04-13 12:21:07 +0300342 try:
343 session.set_combine_stderr(True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200344
koder aka kdanilov652cd802015-04-13 12:21:07 +0300345 stime = time.time()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200346
koder aka kdanilov652cd802015-04-13 12:21:07 +0300347 if not nolog:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300348 logger.debug("SSH:{0} Exec {1!r}".format(node, cmd))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200349
koder aka kdanilov652cd802015-04-13 12:21:07 +0300350 session.exec_command(cmd)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200351
koder aka kdanilov652cd802015-04-13 12:21:07 +0300352 if stdin_data is not None:
353 session.sendall(stdin_data)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200354
koder aka kdanilov652cd802015-04-13 12:21:07 +0300355 session.settimeout(1)
356 session.shutdown_write()
357 output = ""
358
359 while True:
360 try:
361 ndata = session.recv(1024)
362 output += ndata
363 if "" == ndata:
364 break
365 except socket.timeout:
366 pass
367
368 if time.time() - stime > timeout:
369 raise OSError(output + "\nExecution timeout")
370
371 code = session.recv_exit_status()
372 finally:
373 session.close()
374
375 if code != 0:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300376 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
377 raise OSError(templ.format(node, cmd, code, output))
koder aka kdanilov652cd802015-04-13 12:21:07 +0300378
379 return output
380
381
382def close_all_sessions():
383 with all_sessions_lock:
384 for session in all_sessions:
385 try:
386 session.sendall('\x03')
387 session.close()
388 except:
389 pass