blob: af3424c50a1c7c8b115dd204e0f42ec120c4bd0f [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
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:
77 if creds.user is None:
78 user = getpass.getuser()
79 else:
80 user = creds.user
81
82 if creds.passwd is not None:
83 ssh.connect(creds.host,
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030084 timeout=tcp_timeout,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020085 username=user,
86 password=creds.passwd,
87 port=creds.port,
88 allow_agent=False,
89 look_for_keys=False)
90 return ssh
91
92 if creds.key_file is not None:
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=creds.key_file,
97 look_for_keys=False,
98 port=creds.port)
99 return ssh
100
101 key_file = os.path.expanduser('~/.ssh/id_rsa')
102 ssh.connect(creds.host,
103 username=user,
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300104 timeout=tcp_timeout,
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200105 key_filename=key_file,
106 look_for_keys=False,
107 port=creds.port)
108 return ssh
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200109 except paramiko.PasswordRequiredException:
110 raise
koder aka kdanilov168f6092015-04-19 02:33:38 +0300111 except socket.error:
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300112 if time.time() > etime:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200113 raise
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300114 time.sleep(1)
koder aka kdanilov3a6633e2015-03-26 18:20:00 +0200115
116
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300117def save_to_remote(sftp, path, content):
118 with sftp.open(path, "wb") as fd:
119 fd.write(content)
120
121
122def read_from_remote(sftp, path):
123 with sftp.open(path, "rb") as fd:
124 return fd.read()
125
126
koder aka kdanilove06762a2015-03-22 23:32:09 +0200127def normalize_dirpath(dirpath):
128 while dirpath.endswith("/"):
129 dirpath = dirpath[:-1]
130 return dirpath
131
132
koder aka kdanilov2c473092015-03-29 17:12:13 +0300133ALL_RWX_MODE = ((1 << 9) - 1)
134
135
136def ssh_mkdir(sftp, remotepath, mode=ALL_RWX_MODE, intermediate=False):
koder aka kdanilove06762a2015-03-22 23:32:09 +0200137 remotepath = normalize_dirpath(remotepath)
138 if intermediate:
139 try:
140 sftp.mkdir(remotepath, mode=mode)
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300141 except (IOError, OSError):
koder aka kdanilov168f6092015-04-19 02:33:38 +0300142 upper_dir = remotepath.rsplit("/", 1)[0]
143
144 if upper_dir == '' or upper_dir == '/':
145 raise
146
147 ssh_mkdir(sftp, upper_dir, mode=mode, intermediate=True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200148 return sftp.mkdir(remotepath, mode=mode)
149 else:
150 sftp.mkdir(remotepath, mode=mode)
151
152
153def ssh_copy_file(sftp, localfile, remfile, preserve_perm=True):
154 sftp.put(localfile, remfile)
155 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300156 sftp.chmod(remfile, os.stat(localfile).st_mode & ALL_RWX_MODE)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200157
158
159def put_dir_recursively(sftp, localpath, remotepath, preserve_perm=True):
160 "upload local directory to remote recursively"
161
162 # hack for localhost connection
163 if hasattr(sftp, "copytree"):
164 sftp.copytree(localpath, remotepath)
165 return
166
167 assert remotepath.startswith("/"), "%s must be absolute path" % remotepath
168
169 # normalize
170 localpath = normalize_dirpath(localpath)
171 remotepath = normalize_dirpath(remotepath)
172
173 try:
174 sftp.chdir(remotepath)
175 localsuffix = localpath.rsplit("/", 1)[1]
176 remotesuffix = remotepath.rsplit("/", 1)[1]
177 if localsuffix != remotesuffix:
178 remotepath = os.path.join(remotepath, localsuffix)
179 except IOError:
180 pass
181
182 for root, dirs, fls in os.walk(localpath):
183 prefix = os.path.commonprefix([localpath, root])
184 suffix = root.split(prefix, 1)[1]
185 if suffix.startswith("/"):
186 suffix = suffix[1:]
187
188 remroot = os.path.join(remotepath, suffix)
189
190 try:
191 sftp.chdir(remroot)
192 except IOError:
193 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300194 mode = os.stat(root).st_mode & ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200195 else:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300196 mode = ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200197 ssh_mkdir(sftp, remroot, mode=mode, intermediate=True)
198 sftp.chdir(remroot)
199
200 for f in fls:
201 remfile = os.path.join(remroot, f)
202 localfile = os.path.join(root, f)
203 ssh_copy_file(sftp, localfile, remfile, preserve_perm)
204
205
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300206def delete_file(conn, path):
207 sftp = conn.open_sftp()
208 sftp.remove(path)
209 sftp.close()
210
211
koder aka kdanilove06762a2015-03-22 23:32:09 +0200212def copy_paths(conn, paths):
213 sftp = conn.open_sftp()
214 try:
215 for src, dst in paths.items():
216 try:
217 if os.path.isfile(src):
218 ssh_copy_file(sftp, src, dst)
219 elif os.path.isdir(src):
220 put_dir_recursively(sftp, src, dst)
221 else:
222 templ = "Can't copy {0!r} - " + \
223 "it neither a file not a directory"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300224 raise OSError(templ.format(src))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200225 except Exception as exc:
226 tmpl = "Scp {0!r} => {1!r} failed - {2!r}"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300227 raise OSError(tmpl.format(src, dst, exc))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200228 finally:
229 sftp.close()
230
231
232class ConnCreds(object):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300233 conn_uri_attrs = ("user", "passwd", "host", "port", "path")
234
koder aka kdanilove06762a2015-03-22 23:32:09 +0200235 def __init__(self):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300236 for name in self.conn_uri_attrs:
koder aka kdanilove06762a2015-03-22 23:32:09 +0200237 setattr(self, name, None)
238
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300239 def __str__(self):
240 return str(self.__dict__)
241
koder aka kdanilove06762a2015-03-22 23:32:09 +0200242
243uri_reg_exprs = []
244
245
246class URIsNamespace(object):
247 class ReParts(object):
248 user_rr = "[^:]*?"
249 host_rr = "[^:]*?"
250 port_rr = "\\d+"
251 key_file_rr = "[^:@]*"
252 passwd_rr = ".*?"
253
254 re_dct = ReParts.__dict__
255
256 for attr_name, val in re_dct.items():
257 if attr_name.endswith('_rr'):
258 new_rr = "(?P<{0}>{1})".format(attr_name[:-3], val)
259 setattr(ReParts, attr_name, new_rr)
260
261 re_dct = ReParts.__dict__
262
263 templs = [
264 "^{host_rr}$",
265 "^{user_rr}@{host_rr}::{key_file_rr}$",
266 "^{user_rr}@{host_rr}:{port_rr}:{key_file_rr}$",
267 "^{user_rr}:{passwd_rr}@@{host_rr}$",
268 "^{user_rr}:{passwd_rr}@@{host_rr}:{port_rr}$",
269 ]
270
271 for templ in templs:
272 uri_reg_exprs.append(templ.format(**re_dct))
273
274
275def parse_ssh_uri(uri):
276 # user:passwd@@ip_host:port
277 # user:passwd@@ip_host
278 # user@ip_host:port
279 # user@ip_host
280 # ip_host:port
281 # ip_host
282 # user@ip_host:port:path_to_key_file
283 # user@ip_host::path_to_key_file
284 # ip_host:port:path_to_key_file
285 # ip_host::path_to_key_file
286
koder aka kdanilov4d4771c2015-04-23 01:32:02 +0300287 if uri.startswith("ssh://"):
288 uri = uri[len("ssh://"):]
289
koder aka kdanilove06762a2015-03-22 23:32:09 +0200290 res = ConnCreds()
291 res.port = "22"
292 res.key_file = None
293 res.passwd = None
294
295 for rr in uri_reg_exprs:
296 rrm = re.match(rr, uri)
297 if rrm is not None:
298 res.__dict__.update(rrm.groupdict())
299 return res
koder aka kdanilov652cd802015-04-13 12:21:07 +0300300
koder aka kdanilove06762a2015-03-22 23:32:09 +0200301 raise ValueError("Can't parse {0!r} as ssh uri value".format(uri))
302
303
koder aka kdanilov168f6092015-04-19 02:33:38 +0300304def connect(uri, **params):
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300305 if uri == 'local':
306 return Local
307
koder aka kdanilove06762a2015-03-22 23:32:09 +0200308 creds = parse_ssh_uri(uri)
309 creds.port = int(creds.port)
koder aka kdanilov168f6092015-04-19 02:33:38 +0300310 return ssh_connect(creds, **params)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200311
312
koder aka kdanilov652cd802015-04-13 12:21:07 +0300313all_sessions_lock = threading.Lock()
314all_sessions = []
koder aka kdanilove06762a2015-03-22 23:32:09 +0200315
koder aka kdanilove06762a2015-03-22 23:32:09 +0200316
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300317def run_over_ssh(conn, cmd, stdin_data=None, timeout=60,
318 nolog=False, node=None):
koder aka kdanilov652cd802015-04-13 12:21:07 +0300319 "should be replaces by normal implementation, with select"
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300320
321 if conn is Local:
322 if not nolog:
323 logger.debug("SSH:local Exec {0!r}".format(cmd))
324 proc = subprocess.Popen(cmd, shell=True,
325 stdin=subprocess.PIPE,
326 stdout=subprocess.PIPE,
327 stderr=subprocess.STDOUT)
328
329 stdoutdata, _ = proc.communicate(input=stdin_data)
330
331 if proc.returncode != 0:
332 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
333 raise OSError(templ.format(node, cmd, proc.returncode, stdoutdata))
334
335 return stdoutdata
336
koder aka kdanilov652cd802015-04-13 12:21:07 +0300337 transport = conn.get_transport()
338 session = transport.open_session()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200339
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300340 if node is None:
341 node = ""
342
koder aka kdanilov652cd802015-04-13 12:21:07 +0300343 with all_sessions_lock:
344 all_sessions.append(session)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200345
koder aka kdanilov652cd802015-04-13 12:21:07 +0300346 try:
347 session.set_combine_stderr(True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200348
koder aka kdanilov652cd802015-04-13 12:21:07 +0300349 stime = time.time()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200350
koder aka kdanilov652cd802015-04-13 12:21:07 +0300351 if not nolog:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300352 logger.debug("SSH:{0} Exec {1!r}".format(node, cmd))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200353
koder aka kdanilov652cd802015-04-13 12:21:07 +0300354 session.exec_command(cmd)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200355
koder aka kdanilov652cd802015-04-13 12:21:07 +0300356 if stdin_data is not None:
357 session.sendall(stdin_data)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200358
koder aka kdanilov652cd802015-04-13 12:21:07 +0300359 session.settimeout(1)
360 session.shutdown_write()
361 output = ""
362
363 while True:
364 try:
365 ndata = session.recv(1024)
366 output += ndata
367 if "" == ndata:
368 break
369 except socket.timeout:
370 pass
371
372 if time.time() - stime > timeout:
373 raise OSError(output + "\nExecution timeout")
374
375 code = session.recv_exit_status()
376 finally:
377 session.close()
378
379 if code != 0:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300380 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
381 raise OSError(templ.format(node, cmd, code, output))
koder aka kdanilov652cd802015-04-13 12:21:07 +0300382
383 return output
384
385
386def close_all_sessions():
387 with all_sessions_lock:
388 for session in all_sessions:
389 try:
390 session.sendall('\x03')
391 session.close()
392 except:
393 pass