blob: 68d4017ec6d8cbe7f8b179d4164646c3d257a94f [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 kdanilovcff7b2e2015-04-18 20:48:15 +030016def ssh_connect(creds, retry_count=6, timeout=10):
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
21 for i in range(retry_count):
22 try:
23 if creds.user is None:
24 user = getpass.getuser()
25 else:
26 user = creds.user
27
28 if creds.passwd is not None:
29 ssh.connect(creds.host,
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030030 timeout=timeout, # tcp connect timeout
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020031 username=user,
32 password=creds.passwd,
33 port=creds.port,
34 allow_agent=False,
35 look_for_keys=False)
36 return ssh
37
38 if creds.key_file is not None:
39 ssh.connect(creds.host,
40 username=user,
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030041 timeout=timeout, # tcp connect timeout
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020042 key_filename=creds.key_file,
43 look_for_keys=False,
44 port=creds.port)
45 return ssh
46
47 key_file = os.path.expanduser('~/.ssh/id_rsa')
48 ssh.connect(creds.host,
49 username=user,
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030050 timeout=timeout, # tcp connect timeout
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020051 key_filename=key_file,
52 look_for_keys=False,
53 port=creds.port)
54 return ssh
55 # raise ValueError("Wrong credentials {0}".format(creds.__dict__))
56 except paramiko.PasswordRequiredException:
57 raise
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030058 except socket.error as err:
59 print err
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020060 if i == retry_count - 1:
61 raise
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030062 time.sleep(1)
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020063
64
koder aka kdanilove06762a2015-03-22 23:32:09 +020065def normalize_dirpath(dirpath):
66 while dirpath.endswith("/"):
67 dirpath = dirpath[:-1]
68 return dirpath
69
70
koder aka kdanilov2c473092015-03-29 17:12:13 +030071ALL_RWX_MODE = ((1 << 9) - 1)
72
73
74def ssh_mkdir(sftp, remotepath, mode=ALL_RWX_MODE, intermediate=False):
koder aka kdanilove06762a2015-03-22 23:32:09 +020075 remotepath = normalize_dirpath(remotepath)
76 if intermediate:
77 try:
78 sftp.mkdir(remotepath, mode=mode)
79 except IOError:
80 ssh_mkdir(sftp, remotepath.rsplit("/", 1)[0], mode=mode,
81 intermediate=True)
82 return sftp.mkdir(remotepath, mode=mode)
83 else:
84 sftp.mkdir(remotepath, mode=mode)
85
86
87def ssh_copy_file(sftp, localfile, remfile, preserve_perm=True):
88 sftp.put(localfile, remfile)
89 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +030090 sftp.chmod(remfile, os.stat(localfile).st_mode & ALL_RWX_MODE)
koder aka kdanilove06762a2015-03-22 23:32:09 +020091
92
93def put_dir_recursively(sftp, localpath, remotepath, preserve_perm=True):
94 "upload local directory to remote recursively"
95
96 # hack for localhost connection
97 if hasattr(sftp, "copytree"):
98 sftp.copytree(localpath, remotepath)
99 return
100
101 assert remotepath.startswith("/"), "%s must be absolute path" % remotepath
102
103 # normalize
104 localpath = normalize_dirpath(localpath)
105 remotepath = normalize_dirpath(remotepath)
106
107 try:
108 sftp.chdir(remotepath)
109 localsuffix = localpath.rsplit("/", 1)[1]
110 remotesuffix = remotepath.rsplit("/", 1)[1]
111 if localsuffix != remotesuffix:
112 remotepath = os.path.join(remotepath, localsuffix)
113 except IOError:
114 pass
115
116 for root, dirs, fls in os.walk(localpath):
117 prefix = os.path.commonprefix([localpath, root])
118 suffix = root.split(prefix, 1)[1]
119 if suffix.startswith("/"):
120 suffix = suffix[1:]
121
122 remroot = os.path.join(remotepath, suffix)
123
124 try:
125 sftp.chdir(remroot)
126 except IOError:
127 if preserve_perm:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300128 mode = os.stat(root).st_mode & ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200129 else:
koder aka kdanilov2c473092015-03-29 17:12:13 +0300130 mode = ALL_RWX_MODE
koder aka kdanilove06762a2015-03-22 23:32:09 +0200131 ssh_mkdir(sftp, remroot, mode=mode, intermediate=True)
132 sftp.chdir(remroot)
133
134 for f in fls:
135 remfile = os.path.join(remroot, f)
136 localfile = os.path.join(root, f)
137 ssh_copy_file(sftp, localfile, remfile, preserve_perm)
138
139
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300140def delete_file(conn, path):
141 sftp = conn.open_sftp()
142 sftp.remove(path)
143 sftp.close()
144
145
koder aka kdanilove06762a2015-03-22 23:32:09 +0200146def copy_paths(conn, paths):
147 sftp = conn.open_sftp()
148 try:
149 for src, dst in paths.items():
150 try:
151 if os.path.isfile(src):
152 ssh_copy_file(sftp, src, dst)
153 elif os.path.isdir(src):
154 put_dir_recursively(sftp, src, dst)
155 else:
156 templ = "Can't copy {0!r} - " + \
157 "it neither a file not a directory"
158 msg = templ.format(src)
159 raise OSError(msg)
160 except Exception as exc:
161 tmpl = "Scp {0!r} => {1!r} failed - {2!r}"
162 msg = tmpl.format(src, dst, exc)
163 raise OSError(msg)
164 finally:
165 sftp.close()
166
167
168class ConnCreds(object):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300169 conn_uri_attrs = ("user", "passwd", "host", "port", "path")
170
koder aka kdanilove06762a2015-03-22 23:32:09 +0200171 def __init__(self):
koder aka kdanilov2c473092015-03-29 17:12:13 +0300172 for name in self.conn_uri_attrs:
koder aka kdanilove06762a2015-03-22 23:32:09 +0200173 setattr(self, name, None)
174
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300175 def __str__(self):
176 return str(self.__dict__)
177
koder aka kdanilove06762a2015-03-22 23:32:09 +0200178
179uri_reg_exprs = []
180
181
182class URIsNamespace(object):
183 class ReParts(object):
184 user_rr = "[^:]*?"
185 host_rr = "[^:]*?"
186 port_rr = "\\d+"
187 key_file_rr = "[^:@]*"
188 passwd_rr = ".*?"
189
190 re_dct = ReParts.__dict__
191
192 for attr_name, val in re_dct.items():
193 if attr_name.endswith('_rr'):
194 new_rr = "(?P<{0}>{1})".format(attr_name[:-3], val)
195 setattr(ReParts, attr_name, new_rr)
196
197 re_dct = ReParts.__dict__
198
199 templs = [
200 "^{host_rr}$",
201 "^{user_rr}@{host_rr}::{key_file_rr}$",
202 "^{user_rr}@{host_rr}:{port_rr}:{key_file_rr}$",
203 "^{user_rr}:{passwd_rr}@@{host_rr}$",
204 "^{user_rr}:{passwd_rr}@@{host_rr}:{port_rr}$",
205 ]
206
207 for templ in templs:
208 uri_reg_exprs.append(templ.format(**re_dct))
209
210
211def parse_ssh_uri(uri):
212 # user:passwd@@ip_host:port
213 # user:passwd@@ip_host
214 # user@ip_host:port
215 # user@ip_host
216 # ip_host:port
217 # ip_host
218 # user@ip_host:port:path_to_key_file
219 # user@ip_host::path_to_key_file
220 # ip_host:port:path_to_key_file
221 # ip_host::path_to_key_file
222
223 res = ConnCreds()
224 res.port = "22"
225 res.key_file = None
226 res.passwd = None
227
228 for rr in uri_reg_exprs:
229 rrm = re.match(rr, uri)
230 if rrm is not None:
231 res.__dict__.update(rrm.groupdict())
232 return res
koder aka kdanilov652cd802015-04-13 12:21:07 +0300233
koder aka kdanilove06762a2015-03-22 23:32:09 +0200234 raise ValueError("Can't parse {0!r} as ssh uri value".format(uri))
235
236
237def connect(uri):
238 creds = parse_ssh_uri(uri)
239 creds.port = int(creds.port)
240 return ssh_connect(creds)
241
242
koder aka kdanilov652cd802015-04-13 12:21:07 +0300243all_sessions_lock = threading.Lock()
244all_sessions = []
koder aka kdanilove06762a2015-03-22 23:32:09 +0200245
koder aka kdanilove06762a2015-03-22 23:32:09 +0200246
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300247def run_over_ssh(conn, cmd, stdin_data=None, timeout=60,
248 nolog=False, node=None):
koder aka kdanilov652cd802015-04-13 12:21:07 +0300249 "should be replaces by normal implementation, with select"
250 transport = conn.get_transport()
251 session = transport.open_session()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200252
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300253 if node is None:
254 node = ""
255
koder aka kdanilov652cd802015-04-13 12:21:07 +0300256 with all_sessions_lock:
257 all_sessions.append(session)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200258
koder aka kdanilov652cd802015-04-13 12:21:07 +0300259 try:
260 session.set_combine_stderr(True)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200261
koder aka kdanilov652cd802015-04-13 12:21:07 +0300262 stime = time.time()
koder aka kdanilove06762a2015-03-22 23:32:09 +0200263
koder aka kdanilov652cd802015-04-13 12:21:07 +0300264 if not nolog:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300265 logger.debug("SSH:{0} Exec {1!r}".format(node, cmd))
koder aka kdanilove06762a2015-03-22 23:32:09 +0200266
koder aka kdanilov652cd802015-04-13 12:21:07 +0300267 session.exec_command(cmd)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200268
koder aka kdanilov652cd802015-04-13 12:21:07 +0300269 if stdin_data is not None:
270 session.sendall(stdin_data)
koder aka kdanilove06762a2015-03-22 23:32:09 +0200271
koder aka kdanilov652cd802015-04-13 12:21:07 +0300272 session.settimeout(1)
273 session.shutdown_write()
274 output = ""
275
276 while True:
277 try:
278 ndata = session.recv(1024)
279 output += ndata
280 if "" == ndata:
281 break
282 except socket.timeout:
283 pass
284
285 if time.time() - stime > timeout:
286 raise OSError(output + "\nExecution timeout")
287
288 code = session.recv_exit_status()
289 finally:
290 session.close()
291
292 if code != 0:
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300293 templ = "SSH:{0} Cmd {1!r} failed with code {2}. Output: {3}"
294 raise OSError(templ.format(node, cmd, code, output))
koder aka kdanilov652cd802015-04-13 12:21:07 +0300295
296 return output
297
298
299def close_all_sessions():
300 with all_sessions_lock:
301 for session in all_sessions:
302 try:
303 session.sendall('\x03')
304 session.close()
305 except:
306 pass