blob: 0de7816e661ee81bce0dbf2c835c08b9e578ce26 [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 kdanilove06762a2015-03-22 23:32:09 +02004import logging
5import os.path
koder aka kdanilov3a6633e2015-03-26 18:20:00 +02006import getpass
koder aka kdanilov652cd802015-04-13 12:21:07 +03007import threading
koder aka kdanilove06762a2015-03-22 23:32:09 +02008
koder aka kdanilov652cd802015-04-13 12:21:07 +03009
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020010import paramiko
koder aka kdanilove06762a2015-03-22 23:32:09 +020011
koder aka kdanilove06762a2015-03-22 23:32:09 +020012
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030013logger = logging.getLogger("wally")
koder aka kdanilove06762a2015-03-22 23:32:09 +020014
15
koder aka kdanilov168f6092015-04-19 02:33:38 +030016def ssh_connect(creds, retry_count=6, timeout=10, log_warns=True):
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020017 ssh = paramiko.SSHClient()
18 ssh.load_host_keys('/dev/null')
19 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
20 ssh.known_hosts = None
koder aka kdanilov168f6092015-04-19 02:33:38 +030021
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020022 for i in range(retry_count):
23 try:
24 if creds.user is None:
25 user = getpass.getuser()
26 else:
27 user = creds.user
28
29 if creds.passwd is not None:
30 ssh.connect(creds.host,
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030031 timeout=timeout, # tcp connect timeout
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020032 username=user,
33 password=creds.passwd,
34 port=creds.port,
35 allow_agent=False,
36 look_for_keys=False)
37 return ssh
38
39 if creds.key_file is not None:
40 ssh.connect(creds.host,
41 username=user,
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030042 timeout=timeout, # tcp connect timeout
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020043 key_filename=creds.key_file,
44 look_for_keys=False,
45 port=creds.port)
46 return ssh
47
48 key_file = os.path.expanduser('~/.ssh/id_rsa')
49 ssh.connect(creds.host,
50 username=user,
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030051 timeout=timeout, # tcp connect timeout
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020052 key_filename=key_file,
53 look_for_keys=False,
54 port=creds.port)
55 return ssh
56 # raise ValueError("Wrong credentials {0}".format(creds.__dict__))
57 except paramiko.PasswordRequiredException:
58 raise
koder aka kdanilov168f6092015-04-19 02:33:38 +030059 except socket.error:
60 retry_left = retry_count - i - 1
61
62 if log_warns:
63 msg = "Node {0.host}:{0.port} connection timeout."
64
65 if 0 != retry_left:
66 msg += " {0} retry left.".format(retry_left)
67
68 logger.warning(msg.format(creds))
69
70 if 0 == retry_left:
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020071 raise
koder aka kdanilov168f6092015-04-19 02:33:38 +030072
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030073 time.sleep(1)
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020074
75
koder aka kdanilove06762a2015-03-22 23:32:09 +020076def normalize_dirpath(dirpath):
77 while dirpath.endswith("/"):
78 dirpath = dirpath[:-1]
79 return dirpath
80
81
koder aka kdanilov2c473092015-03-29 17:12:13 +030082ALL_RWX_MODE = ((1 << 9) - 1)
83
84
85def ssh_mkdir(sftp, remotepath, mode=ALL_RWX_MODE, intermediate=False):
koder aka kdanilove06762a2015-03-22 23:32:09 +020086 remotepath = normalize_dirpath(remotepath)
87 if intermediate:
88 try:
89 sftp.mkdir(remotepath, mode=mode)
90 except IOError:
koder aka kdanilov168f6092015-04-19 02:33:38 +030091 upper_dir = remotepath.rsplit("/", 1)[0]
92
93 if upper_dir == '' or upper_dir == '/':
94 raise
95
96 ssh_mkdir(sftp, upper_dir, mode=mode, intermediate=True)
koder aka kdanilove06762a2015-03-22 23:32:09 +020097 return sftp.mkdir(remotepath, mode=mode)
98 else:
99 sftp.mkdir(remotepath, mode=mode)
100
101
102def ssh_copy_file(sftp, localfile, remfile, preserve_perm=True):
103 sftp.put(localfile, remfile)
104 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300105 sftp.chmod(remfile, os.stat(localfile).st_mode & ALL_RWX_MODE)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200106
107
108def put_dir_recursively(sftp, localpath, remotepath, preserve_perm=True):
109 "upload local directory to remote recursively"
110
111 # hack for localhost connection
112 if hasattr(sftp, "copytree"):
113 sftp.copytree(localpath, remotepath)
114 return
115
116 assert remotepath.startswith("/"), "%s must be absolute path" % remotepath
117
118 # normalize
119 localpath = normalize_dirpath(localpath)
120 remotepath = normalize_dirpath(remotepath)
121
122 try:
123 sftp.chdir(remotepath)
124 localsuffix = localpath.rsplit("/", 1)[1]
125 remotesuffix = remotepath.rsplit("/", 1)[1]
126 if localsuffix != remotesuffix:
127 remotepath = os.path.join(remotepath, localsuffix)
128 except IOError:
129 pass
130
131 for root, dirs, fls in os.walk(localpath):
132 prefix = os.path.commonprefix([localpath, root])
133 suffix = root.split(prefix, 1)[1]
134 if suffix.startswith("/"):
135 suffix = suffix[1:]
136
137 remroot = os.path.join(remotepath, suffix)
138
139 try:
140 sftp.chdir(remroot)
141 except IOError:
142 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300143 mode = os.stat(root).st_mode & ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200144 else:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300145 mode = ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200146 ssh_mkdir(sftp, remroot, mode=mode, intermediate=True)
147 sftp.chdir(remroot)
148
149 for f in fls:
150 remfile = os.path.join(remroot, f)
151 localfile = os.path.join(root, f)
152 ssh_copy_file(sftp, localfile, remfile, preserve_perm)
153
154
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300155def delete_file(conn, path):
156 sftp = conn.open_sftp()
157 sftp.remove(path)
158 sftp.close()
159
160
koder aka kdanilove06762a2015-03-22 23:32:09 +0200161def copy_paths(conn, paths):
162 sftp = conn.open_sftp()
163 try:
164 for src, dst in paths.items():
165 try:
166 if os.path.isfile(src):
167 ssh_copy_file(sftp, src, dst)
168 elif os.path.isdir(src):
169 put_dir_recursively(sftp, src, dst)
170 else:
171 templ = "Can't copy {0!r} - " + \
172 "it neither a file not a directory"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300173 raise OSError(templ.format(src))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200174 except Exception as exc:
175 tmpl = "Scp {0!r} => {1!r} failed - {2!r}"
koder aka kdanilov168f6092015-04-19 02:33:38 +0300176 raise OSError(tmpl.format(src, dst, exc))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200177 finally:
178 sftp.close()
179
180
181class ConnCreds(object):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300182 conn_uri_attrs = ("user", "passwd", "host", "port", "path")
183
koder aka kdanilove06762a2015-03-22 23:32:09 +0200184 def __init__(self):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300185 for name in self.conn_uri_attrs:
koder aka kdanilove06762a2015-03-22 23:32:09 +0200186 setattr(self, name, None)
187
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300188 def __str__(self):
189 return str(self.__dict__)
190
koder aka kdanilove06762a2015-03-22 23:32:09 +0200191
192uri_reg_exprs = []
193
194
195class URIsNamespace(object):
196 class ReParts(object):
197 user_rr = "[^:]*?"
198 host_rr = "[^:]*?"
199 port_rr = "\\d+"
200 key_file_rr = "[^:@]*"
201 passwd_rr = ".*?"
202
203 re_dct = ReParts.__dict__
204
205 for attr_name, val in re_dct.items():
206 if attr_name.endswith('_rr'):
207 new_rr = "(?P<{0}>{1})".format(attr_name[:-3], val)
208 setattr(ReParts, attr_name, new_rr)
209
210 re_dct = ReParts.__dict__
211
212 templs = [
213 "^{host_rr}$",
214 "^{user_rr}@{host_rr}::{key_file_rr}$",
215 "^{user_rr}@{host_rr}:{port_rr}:{key_file_rr}$",
216 "^{user_rr}:{passwd_rr}@@{host_rr}$",
217 "^{user_rr}:{passwd_rr}@@{host_rr}:{port_rr}$",
218 ]
219
220 for templ in templs:
221 uri_reg_exprs.append(templ.format(**re_dct))
222
223
224def parse_ssh_uri(uri):
225 # user:passwd@@ip_host:port
226 # user:passwd@@ip_host
227 # user@ip_host:port
228 # user@ip_host
229 # ip_host:port
230 # ip_host
231 # user@ip_host:port:path_to_key_file
232 # user@ip_host::path_to_key_file
233 # ip_host:port:path_to_key_file
234 # ip_host::path_to_key_file
235
236 res = ConnCreds()
237 res.port = "22"
238 res.key_file = None
239 res.passwd = None
240
241 for rr in uri_reg_exprs:
242 rrm = re.match(rr, uri)
243 if rrm is not None:
244 res.__dict__.update(rrm.groupdict())
245 return res
koder aka kdanilov652cd802015-04-13 12:21:07 +0300246
koder aka kdanilove06762a2015-03-22 23:32:09 +0200247 raise ValueError("Can't parse {0!r} as ssh uri value".format(uri))
248
249
koder aka kdanilov168f6092015-04-19 02:33:38 +0300250def connect(uri, **params):
koder aka kdanilove06762a2015-03-22 23:32:09 +0200251 creds = parse_ssh_uri(uri)
252 creds.port = int(creds.port)
koder aka kdanilov168f6092015-04-19 02:33:38 +0300253 return ssh_connect(creds, **params)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200254
255
koder aka kdanilov652cd802015-04-13 12:21:07 +0300256all_sessions_lock = threading.Lock()
257all_sessions = []
koder aka kdanilove06762a2015-03-22 23:32:09 +0200258
koder aka kdanilove06762a2015-03-22 23:32:09 +0200259
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300260def run_over_ssh(conn, cmd, stdin_data=None, timeout=60,
261 nolog=False, node=None):
koder aka kdanilov652cd802015-04-13 12:21:07 +0300262 "should be replaces by normal implementation, with select"
263 transport = conn.get_transport()
264 session = transport.open_session()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200265
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300266 if node is None:
267 node = ""
268
koder aka kdanilov652cd802015-04-13 12:21:07 +0300269 with all_sessions_lock:
270 all_sessions.append(session)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200271
koder aka kdanilov652cd802015-04-13 12:21:07 +0300272 try:
273 session.set_combine_stderr(True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200274
koder aka kdanilov652cd802015-04-13 12:21:07 +0300275 stime = time.time()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200276
koder aka kdanilov652cd802015-04-13 12:21:07 +0300277 if not nolog:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300278 logger.debug("SSH:{0} Exec {1!r}".format(node, cmd))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200279
koder aka kdanilov652cd802015-04-13 12:21:07 +0300280 session.exec_command(cmd)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200281
koder aka kdanilov652cd802015-04-13 12:21:07 +0300282 if stdin_data is not None:
283 session.sendall(stdin_data)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200284
koder aka kdanilov652cd802015-04-13 12:21:07 +0300285 session.settimeout(1)
286 session.shutdown_write()
287 output = ""
288
289 while True:
290 try:
291 ndata = session.recv(1024)
292 output += ndata
293 if "" == ndata:
294 break
295 except socket.timeout:
296 pass
297
298 if time.time() - stime > timeout:
299 raise OSError(output + "\nExecution timeout")
300
301 code = session.recv_exit_status()
302 finally:
303 session.close()
304
305 if code != 0:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300306 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
307 raise OSError(templ.format(node, cmd, code, output))
koder aka kdanilov652cd802015-04-13 12:21:07 +0300308
309 return output
310
311
312def close_all_sessions():
313 with all_sessions_lock:
314 for session in all_sessions:
315 try:
316 session.sendall('\x03')
317 session.close()
318 except:
319 pass