blob: 7728dfd2cd95b3cf99212d51a7f1f150650b2174 [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 kdanilov3d2bc4f2016-11-12 18:31:18 +02003import errno
koder aka kdanilov652cd802015-04-13 12:21:07 +03004import socket
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 kdanilov3d2bc4f2016-11-12 18:31:18 +02008import selectors
koder aka kdanilov22d134e2016-11-08 11:33:19 +02009from io import BytesIO
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020010from typing import Union, Optional, cast, Dict, List, Tuple
koder aka kdanilov652cd802015-04-13 12:21:07 +030011
koder aka kdanilov3a6633e2015-03-26 18:20:00 +020012import paramiko
koder aka kdanilove06762a2015-03-22 23:32:09 +020013
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020014from . import utils
koder aka kdanilov22d134e2016-11-08 11:33:19 +020015
koder aka kdanilove06762a2015-03-22 23:32:09 +020016
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030017logger = logging.getLogger("wally")
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020018IPAddr = Tuple[str, int]
koder aka kdanilove06762a2015-03-22 23:32:09 +020019
20
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020021class URIsNamespace:
22 class ReParts:
koder aka kdanilove06762a2015-03-22 23:32:09 +020023 user_rr = "[^:]*?"
koder aka kdanilov7e0f7cf2015-05-01 17:24:35 +030024 host_rr = "[^:@]*?"
koder aka kdanilove06762a2015-03-22 23:32:09 +020025 port_rr = "\\d+"
26 key_file_rr = "[^:@]*"
27 passwd_rr = ".*?"
28
29 re_dct = ReParts.__dict__
30
31 for attr_name, val in re_dct.items():
32 if attr_name.endswith('_rr'):
33 new_rr = "(?P<{0}>{1})".format(attr_name[:-3], val)
34 setattr(ReParts, attr_name, new_rr)
35
36 re_dct = ReParts.__dict__
37
38 templs = [
39 "^{host_rr}$",
koder aka kdanilov7e0f7cf2015-05-01 17:24:35 +030040 "^{host_rr}:{port_rr}$",
koder aka kdanilov416b87a2015-05-12 00:26:04 +030041 "^{host_rr}::{key_file_rr}$",
42 "^{host_rr}:{port_rr}:{key_file_rr}$",
koder aka kdanilov7e0f7cf2015-05-01 17:24:35 +030043 "^{user_rr}@{host_rr}$",
44 "^{user_rr}@{host_rr}:{port_rr}$",
koder aka kdanilove06762a2015-03-22 23:32:09 +020045 "^{user_rr}@{host_rr}::{key_file_rr}$",
46 "^{user_rr}@{host_rr}:{port_rr}:{key_file_rr}$",
koder aka kdanilov7e0f7cf2015-05-01 17:24:35 +030047 "^{user_rr}:{passwd_rr}@{host_rr}$",
48 "^{user_rr}:{passwd_rr}@{host_rr}:{port_rr}$",
koder aka kdanilove06762a2015-03-22 23:32:09 +020049 ]
50
koder aka kdanilov22d134e2016-11-08 11:33:19 +020051 uri_reg_exprs = [] # type: List[str]
koder aka kdanilove06762a2015-03-22 23:32:09 +020052 for templ in templs:
53 uri_reg_exprs.append(templ.format(**re_dct))
54
55
koder aka kdanilov22d134e2016-11-08 11:33:19 +020056class ConnCreds:
57 conn_uri_attrs = ("user", "passwd", "host", "port", "key_file")
58
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020059 def __init__(self, host: str, user: str, passwd: str = None, port: int = 22, key_file: str = None) -> None:
60 self.user = user
61 self.passwd = passwd
62 self.host = host
63 self.port = port
64 self.key_file = key_file
koder aka kdanilov22d134e2016-11-08 11:33:19 +020065
66 def __str__(self) -> str:
67 return str(self.__dict__)
68
69
koder aka kdanilov22d134e2016-11-08 11:33:19 +020070def parse_ssh_uri(uri: str) -> ConnCreds:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020071 """Parse ssh connection URL from one of following form
72 [ssh://]user:passwd@host[:port]
73 [ssh://][user@]host[:port][:key_file]
74 """
koder aka kdanilove06762a2015-03-22 23:32:09 +020075
koder aka kdanilov4d4771c2015-04-23 01:32:02 +030076 if uri.startswith("ssh://"):
77 uri = uri[len("ssh://"):]
78
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020079 res = ConnCreds("", getpass.getuser())
koder aka kdanilove06762a2015-03-22 23:32:09 +020080
koder aka kdanilov22d134e2016-11-08 11:33:19 +020081 for rr in URIsNamespace.uri_reg_exprs:
koder aka kdanilove06762a2015-03-22 23:32:09 +020082 rrm = re.match(rr, uri)
83 if rrm is not None:
84 res.__dict__.update(rrm.groupdict())
85 return res
koder aka kdanilov652cd802015-04-13 12:21:07 +030086
koder aka kdanilove06762a2015-03-22 23:32:09 +020087 raise ValueError("Can't parse {0!r} as ssh uri value".format(uri))
88
89
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020090NODE_KEYS = {} # type: Dict[IPAddr, paramiko.RSAKey]
koder aka kdanilov22d134e2016-11-08 11:33:19 +020091
92
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020093def set_key_for_node(host_port: IPAddr, key: bytes) -> None:
94 with BytesIO(key) as sio:
95 NODE_KEYS[host_port] = paramiko.RSAKey.from_private_key(sio)
koder aka kdanilov22d134e2016-11-08 11:33:19 +020096
97
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020098def ssh_connect(creds: ConnCreds,
99 conn_timeout: int = 60,
100 tcp_timeout: int = 15,
101 default_banner_timeout: int = 30) -> Tuple[paramiko.SSHClient, str, str]:
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200102
103 ssh = paramiko.SSHClient()
104 ssh.load_host_keys('/dev/null')
105 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
106 ssh.known_hosts = None
107
108 end_time = time.time() + conn_timeout # type: float
109
110 while True:
111 try:
112 time_left = end_time - time.time()
113 c_tcp_timeout = min(tcp_timeout, time_left)
114
115 banner_timeout_arg = {} # type: Dict[str, int]
116 if paramiko.__version_info__ >= (1, 15, 2):
117 banner_timeout_arg['banner_timeout'] = int(min(default_banner_timeout, time_left))
118
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200119 if creds.passwd is not None:
120 ssh.connect(creds.host,
121 timeout=c_tcp_timeout,
122 username=creds.user,
123 password=cast(str, creds.passwd),
124 port=creds.port,
125 allow_agent=False,
126 look_for_keys=False,
127 **banner_timeout_arg)
128 elif creds.key_file is not None:
129 ssh.connect(creds.host,
130 username=creds.user,
131 timeout=c_tcp_timeout,
132 key_filename=cast(str, creds.key_file),
133 look_for_keys=False,
134 port=creds.port,
135 **banner_timeout_arg)
136 elif (creds.host, creds.port) in NODE_KEYS:
137 ssh.connect(creds.host,
138 username=creds.user,
139 timeout=c_tcp_timeout,
140 pkey=NODE_KEYS[(creds.host, creds.port)],
141 look_for_keys=False,
142 port=creds.port,
143 **banner_timeout_arg)
144 else:
145 key_file = os.path.expanduser('~/.ssh/id_rsa')
146 ssh.connect(creds.host,
147 username=creds.user,
148 timeout=c_tcp_timeout,
149 key_filename=key_file,
150 look_for_keys=False,
151 port=creds.port,
152 **banner_timeout_arg)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200153 return ssh, "{0.host}:{0.port}".format(creds), creds.host
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200154 except paramiko.PasswordRequiredException:
155 raise
156 except (socket.error, paramiko.SSHException):
157 if time.time() > end_time:
158 raise
159 time.sleep(1)
160
161
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200162def wait_ssh_available(addrs: List[IPAddr],
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200163 timeout: int = 300,
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200164 tcp_timeout: float = 1.0) -> None:
165 addrs = set(addrs)
166 for _ in utils.Timeout(timeout):
167 with selectors.DefaultSelector() as selector: # type: selectors.BaseSelector
168 for addr in addrs:
169 sock = socket.socket()
170 sock.setblocking(False)
171 try:
172 sock.connect(addr)
173 except BlockingIOError:
174 pass
175 selector.register(sock, selectors.EVENT_READ, data=addr)
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200176
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200177 etime = time.time() + tcp_timeout
178 ltime = etime - time.time()
179 while ltime > 0:
180 for key, _ in selector.select(timeout=ltime):
181 selector.unregister(key.fileobj)
182 try:
183 key.fileobj.getpeername()
184 addrs.remove(key.data)
185 except OSError as exc:
186 if exc.errno == errno.ENOTCONN:
187 pass
188 ltime = etime - time.time()
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300189
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200190 if not addrs:
191 break
koder aka kdanilove06762a2015-03-22 23:32:09 +0200192
koder aka kdanilov0c598a12015-04-21 03:01:40 +0300193