blob: 8907b81dd70557a63773705f447d2207acb72cd6 [file] [log] [blame]
Yulia Portnova3556a062015-03-17 16:30:11 +02001import re
koder aka kdanilov73084622016-11-16 21:51:08 +02002import abc
Yulia Portnova3556a062015-03-17 16:30:11 +02003import json
koder aka kdanilovcee43342015-04-14 22:52:53 +03004import logging
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +03005import urllib.parse
koder aka kdanilov70227062016-11-26 23:23:21 +02006import urllib.error
7import urllib.request
8from typing import Dict, Any, Iterator, List, Callable, cast
koder aka kdanilov73084622016-11-16 21:51:08 +02009from functools import partial
Yulia Portnova3556a062015-03-17 16:30:11 +020010
11import netaddr
Yulia Portnova3556a062015-03-17 16:30:11 +020012from keystoneclient import exceptions
koder aka kdanilov73084622016-11-16 21:51:08 +020013from keystoneclient.v2_0 import Client as keystoneclient
Yulia Portnova3556a062015-03-17 16:30:11 +020014
15
koder aka kdanilov962ee5f2016-12-19 02:40:08 +020016logger = logging.getLogger("wally")
Yulia Portnova3556a062015-03-17 16:30:11 +020017
18
koder aka kdanilov73084622016-11-16 21:51:08 +020019class Connection(metaclass=abc.ABCMeta):
koder aka kdanilov70227062016-11-26 23:23:21 +020020 host = None # type: str
21
koder aka kdanilov73084622016-11-16 21:51:08 +020022 @abc.abstractmethod
23 def do(self, method: str, path: str, params: Dict = None) -> Dict:
24 pass
25
koder aka kdanilov73084622016-11-16 21:51:08 +020026 def get(self, path: str, params: Dict = None) -> Dict:
koder aka kdanilov70227062016-11-26 23:23:21 +020027 return self.do("GET", path, params)
koder aka kdanilov73084622016-11-16 21:51:08 +020028
29
30class Urllib2HTTP(Connection):
Yulia Portnova3556a062015-03-17 16:30:11 +020031 """
32 class for making HTTP requests
33 """
34
35 allowed_methods = ('get', 'put', 'post', 'delete', 'patch', 'head')
36
koder aka kdanilov73084622016-11-16 21:51:08 +020037 def __init__(self, root_url: str, headers: Dict[str, str] = None) -> None:
Yulia Portnova3556a062015-03-17 16:30:11 +020038 """
39 """
40 if root_url.endswith('/'):
41 self.root_url = root_url[:-1]
42 else:
43 self.root_url = root_url
44
koder aka kdanilov73084622016-11-16 21:51:08 +020045 self.host = urllib.parse.urlparse(self.root_url).hostname
Yulia Portnova3556a062015-03-17 16:30:11 +020046
koder aka kdanilov73084622016-11-16 21:51:08 +020047 if headers is None:
48 self.headers = {} # type: Dict[str, str]
49 else:
50 self.headers = headers
Yulia Portnova3556a062015-03-17 16:30:11 +020051
koder aka kdanilov70227062016-11-26 23:23:21 +020052 def do(self, method: str, path: str, params: Dict = None) -> Any:
Yulia Portnova3556a062015-03-17 16:30:11 +020053 if path.startswith('/'):
54 url = self.root_url + path
55 else:
56 url = self.root_url + '/' + path
57
58 if method == 'get':
59 assert params == {} or params is None
60 data_json = None
61 else:
62 data_json = json.dumps(params)
63
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030064 logger.debug("HTTP: {0} {1}".format(method.upper(), url))
Yulia Portnova3556a062015-03-17 16:30:11 +020065
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030066 request = urllib.request.Request(url,
koder aka kdanilov70227062016-11-26 23:23:21 +020067 data=data_json.encode("utf8"),
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030068 headers=self.headers)
Yulia Portnova3556a062015-03-17 16:30:11 +020069 if data_json is not None:
70 request.add_header('Content-Type', 'application/json')
71
koder aka kdanilov70227062016-11-26 23:23:21 +020072 request.get_method = lambda: method.upper() # type: ignore
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030073 response = urllib.request.urlopen(request)
koder aka kdanilov70227062016-11-26 23:23:21 +020074 code = response.code # type: ignore
Yulia Portnova3556a062015-03-17 16:30:11 +020075
koder aka kdanilov70227062016-11-26 23:23:21 +020076 logger.debug("HTTP Responce: {0}".format(code))
77 if code < 200 or code > 209:
Yulia Portnova3556a062015-03-17 16:30:11 +020078 raise IndexError(url)
79
80 content = response.read()
81
82 if '' == content:
83 return None
84
koder aka kdanilov70227062016-11-26 23:23:21 +020085 return json.loads(content.decode("utf8"))
Yulia Portnova3556a062015-03-17 16:30:11 +020086
koder aka kdanilov70227062016-11-26 23:23:21 +020087 def __getattr__(self, name: str) -> Any:
Yulia Portnova3556a062015-03-17 16:30:11 +020088 if name in self.allowed_methods:
89 return partial(self.do, name)
90 raise AttributeError(name)
91
92
93class KeystoneAuth(Urllib2HTTP):
koder aka kdanilov22d134e2016-11-08 11:33:19 +020094 def __init__(self, root_url: str, creds: Dict[str, str], headers: Dict[str, str] = None) -> None:
koder aka kdanilovcee43342015-04-14 22:52:53 +030095 super(KeystoneAuth, self).__init__(root_url, headers)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030096 admin_node_ip = urllib.parse.urlparse(root_url).hostname
Yulia Portnova3556a062015-03-17 16:30:11 +020097 self.keystone_url = "http://{0}:5000/v2.0".format(admin_node_ip)
98 self.keystone = keystoneclient(
99 auth_url=self.keystone_url, **creds)
100 self.refresh_token()
101
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200102 def refresh_token(self) -> None:
Yulia Portnova3556a062015-03-17 16:30:11 +0200103 """Get new token from keystone and update headers"""
104 try:
105 self.keystone.authenticate()
106 self.headers['X-Auth-Token'] = self.keystone.auth_token
107 except exceptions.AuthorizationFailure:
koder aka kdanilovcee43342015-04-14 22:52:53 +0300108 logger.warning(
109 'Cant establish connection to keystone with url %s',
110 self.keystone_url)
Yulia Portnova3556a062015-03-17 16:30:11 +0200111
koder aka kdanilov70227062016-11-26 23:23:21 +0200112 def do(self, method: str, path: str, params: Dict[str, str] = None) -> Any:
Yulia Portnova3556a062015-03-17 16:30:11 +0200113 """Do request. If gets 401 refresh token"""
114 try:
115 return super(KeystoneAuth, self).do(method, path, params)
koder aka kdanilov70227062016-11-26 23:23:21 +0200116 except urllib.error.HTTPError as e:
Yulia Portnova3556a062015-03-17 16:30:11 +0200117 if e.code == 401:
koder aka kdanilovcee43342015-04-14 22:52:53 +0300118 logger.warning(
119 'Authorization failure: {0}'.format(e.read()))
Yulia Portnova3556a062015-03-17 16:30:11 +0200120 self.refresh_token()
121 return super(KeystoneAuth, self).do(method, path, params)
122 else:
123 raise
124
125
koder aka kdanilov70227062016-11-26 23:23:21 +0200126def get_inline_param_list(url: str) -> Iterator[str]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200127 format_param_rr = re.compile(r"\{([a-zA-Z_]+)\}")
128 for match in format_param_rr.finditer(url):
129 yield match.group(1)
130
131
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300132class RestObj:
koder aka kdanilov70227062016-11-26 23:23:21 +0200133 name = None # type: str
134 id = None # type: int
Yulia Portnova3556a062015-03-17 16:30:11 +0200135
koder aka kdanilov70227062016-11-26 23:23:21 +0200136 def __init__(self, conn: Connection, **kwargs: Any) -> None:
Yulia Portnova3556a062015-03-17 16:30:11 +0200137 self.__dict__.update(kwargs)
138 self.__connection__ = conn
139
koder aka kdanilov73084622016-11-16 21:51:08 +0200140 def __str__(self) -> str:
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300141 res = ["{0}({1}):".format(self.__class__.__name__, self.name)]
Yulia Portnova3556a062015-03-17 16:30:11 +0200142 for k, v in sorted(self.__dict__.items()):
143 if k.startswith('__') or k.endswith('__'):
144 continue
145 if k != 'name':
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300146 res.append(" {0}={1!r}".format(k, v))
Yulia Portnova3556a062015-03-17 16:30:11 +0200147 return "\n".join(res)
148
koder aka kdanilov73084622016-11-16 21:51:08 +0200149 def __getitem__(self, item: str) -> Any:
Yulia Portnova3556a062015-03-17 16:30:11 +0200150 return getattr(self, item)
151
152
koder aka kdanilov70227062016-11-26 23:23:21 +0200153def make_call(method: str, url: str) -> Callable:
154 def closure(obj: Any, entire_obj: Any = None, **data: Any) -> Any:
Yulia Portnova3556a062015-03-17 16:30:11 +0200155 inline_params_vals = {}
156 for name in get_inline_param_list(url):
157 if name in data:
158 inline_params_vals[name] = data[name]
159 del data[name]
160 else:
161 inline_params_vals[name] = getattr(obj, name)
162 result_url = url.format(**inline_params_vals)
163
164 if entire_obj is not None:
165 if data != {}:
166 raise ValueError("Both entire_obj and data provided")
167 data = entire_obj
168 return obj.__connection__.do(method, result_url, params=data)
169 return closure
170
171
koder aka kdanilov70227062016-11-26 23:23:21 +0200172RequestMethod = Callable[[str], Callable]
173
174
175PUT = cast(RequestMethod, partial(make_call, 'put')) # type: RequestMethod
176GET = cast(RequestMethod, partial(make_call, 'get')) # type: RequestMethod
177DELETE = cast(RequestMethod, partial(make_call, 'delete')) # type: RequestMethod
Yulia Portnova3556a062015-03-17 16:30:11 +0200178
Yulia Portnova3556a062015-03-17 16:30:11 +0200179# ------------------------------- ORM ----------------------------------------
180
181
koder aka kdanilov73084622016-11-16 21:51:08 +0200182def get_fuel_info(url: str) -> 'FuelInfo':
Yulia Portnova3556a062015-03-17 16:30:11 +0200183 conn = Urllib2HTTP(url)
184 return FuelInfo(conn)
185
186
187class FuelInfo(RestObj):
188
189 """Class represents Fuel installation info"""
190
191 get_nodes = GET('api/nodes')
192 get_clusters = GET('api/clusters')
193 get_cluster = GET('api/clusters/{id}')
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300194 get_info = GET('api/releases')
Yulia Portnova3556a062015-03-17 16:30:11 +0200195
196 @property
koder aka kdanilov73084622016-11-16 21:51:08 +0200197 def nodes(self) -> 'NodeList':
Yulia Portnova3556a062015-03-17 16:30:11 +0200198 """Get all fuel nodes"""
199 return NodeList([Node(self.__connection__, **node) for node
200 in self.get_nodes()])
201
202 @property
koder aka kdanilov73084622016-11-16 21:51:08 +0200203 def free_nodes(self) -> 'NodeList':
Yulia Portnova3556a062015-03-17 16:30:11 +0200204 """Get unallocated nodes"""
205 return NodeList([Node(self.__connection__, **node) for node in
206 self.get_nodes() if not node['cluster']])
207
208 @property
koder aka kdanilov73084622016-11-16 21:51:08 +0200209 def clusters(self) -> List['Cluster']:
Yulia Portnova3556a062015-03-17 16:30:11 +0200210 """List clusters in fuel"""
211 return [Cluster(self.__connection__, **cluster) for cluster
212 in self.get_clusters()]
213
koder aka kdanilov73084622016-11-16 21:51:08 +0200214 def get_version(self) -> List[int]:
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300215 for info in self.get_info():
216 vers = info['version'].split("-")[1].split('.')
koder aka kdanilov73084622016-11-16 21:51:08 +0200217 return list(map(int, vers))
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300218 raise ValueError("No version found")
219
Yulia Portnova3556a062015-03-17 16:30:11 +0200220
221class Node(RestObj):
222 """Represents node in Fuel"""
223
224 get_info = GET('/api/nodes/{id}')
225 get_interfaces = GET('/api/nodes/{id}/interfaces')
Yulia Portnova3556a062015-03-17 16:30:11 +0200226
koder aka kdanilov73084622016-11-16 21:51:08 +0200227 def get_network_data(self) -> Dict:
Yulia Portnova3556a062015-03-17 16:30:11 +0200228 """Returns node network data"""
koder aka kdanilov73084622016-11-16 21:51:08 +0200229 return self.get_info().get('network_data')
Yulia Portnova3556a062015-03-17 16:30:11 +0200230
koder aka kdanilov73084622016-11-16 21:51:08 +0200231 def get_roles(self) -> List[str]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200232 """Get node roles
233
234 Returns: (roles, pending_roles)
235 """
koder aka kdanilov73084622016-11-16 21:51:08 +0200236 return self.get_info().get('roles')
Yulia Portnova3556a062015-03-17 16:30:11 +0200237
koder aka kdanilov73084622016-11-16 21:51:08 +0200238 def get_ip(self, network='public') -> netaddr.IPAddress:
Yulia Portnova3556a062015-03-17 16:30:11 +0200239 """Get node ip
240
241 :param network: network to pick
242 """
243 nets = self.get_network_data()
244 for net in nets:
245 if net['name'] == network:
246 iface_name = net['dev']
247 for iface in self.get_info()['meta']['interfaces']:
248 if iface['name'] == iface_name:
249 try:
250 return iface['ip']
251 except KeyError:
252 return netaddr.IPNetwork(net['ip']).ip
253 raise Exception('Network %s not found' % network)
254
255
256class NodeList(list):
257 """Class for filtering nodes through attributes"""
258 allowed_roles = ['controller', 'compute', 'cinder', 'ceph-osd', 'mongo',
259 'zabbix-server']
260
koder aka kdanilov73084622016-11-16 21:51:08 +0200261 def __getattr__(self, name: str) -> List[Node]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200262 if name in self.allowed_roles:
263 return [node for node in self if name in node.roles]
264
265
266class Cluster(RestObj):
267 """Class represents Cluster in Fuel"""
268
Yulia Portnova3556a062015-03-17 16:30:11 +0200269 get_status = GET('api/clusters/{id}')
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300270 get_networks = GET('api/clusters/{id}/network_configuration/neutron')
271 get_attributes = GET('api/clusters/{id}/attributes')
Yulia Portnova3556a062015-03-17 16:30:11 +0200272 _get_nodes = GET('api/nodes?cluster_id={id}')
273
koder aka kdanilov73084622016-11-16 21:51:08 +0200274 def __init__(self, *dt, **mp) -> None:
Yulia Portnova3556a062015-03-17 16:30:11 +0200275 super(Cluster, self).__init__(*dt, **mp)
Yulia Portnovad9767042015-04-10 17:32:06 +0300276 self.nodes = NodeList([Node(self.__connection__, **node) for node in
277 self._get_nodes()])
Yulia Portnova3556a062015-03-17 16:30:11 +0200278
koder aka kdanilov73084622016-11-16 21:51:08 +0200279 def check_exists(self) -> bool:
Yulia Portnova3556a062015-03-17 16:30:11 +0200280 """Check if cluster exists"""
281 try:
282 self.get_status()
283 return True
koder aka kdanilov70227062016-11-26 23:23:21 +0200284 except urllib.error.HTTPError as err:
Yulia Portnova3556a062015-03-17 16:30:11 +0200285 if err.code == 404:
286 return False
287 raise
288
koder aka kdanilov73084622016-11-16 21:51:08 +0200289 def get_openrc(self) -> Dict[str, str]:
Yulia Portnova00025a52015-04-07 12:17:32 +0300290 access = self.get_attributes()['editable']['access']
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300291 creds = {'username': access['user']['value'],
292 'password': access['password']['value'],
293 'tenant_name': access['tenant']['value']}
Michael Semenov8d6c0572015-08-25 12:59:05 +0300294
295 version = FuelInfo(self.__connection__).get_version()
koder aka kdanilov5ea9df02015-12-04 21:46:06 +0200296 # only HTTPS since 7.0
297 if version >= [7, 0]:
Michael Semenov8d6c0572015-08-25 12:59:05 +0300298 creds['insecure'] = "True"
299 creds['os_auth_url'] = "https://{0}:5000/v2.0".format(
300 self.get_networks()['public_vip'])
301 else:
302 creds['os_auth_url'] = "http://{0}:5000/v2.0".format(
303 self.get_networks()['public_vip'])
Yulia Portnova00025a52015-04-07 12:17:32 +0300304 return creds
305
koder aka kdanilov73084622016-11-16 21:51:08 +0200306 def get_nodes(self) -> Iterator[Node]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200307 for node_descr in self._get_nodes():
308 yield Node(self.__connection__, **node_descr)
309
Yulia Portnova3556a062015-03-17 16:30:11 +0200310
koder aka kdanilov73084622016-11-16 21:51:08 +0200311def reflect_cluster(conn: Connection, cluster_id: int) -> Cluster:
Yulia Portnova3556a062015-03-17 16:30:11 +0200312 """Returns cluster object by id"""
313 c = Cluster(conn, id=cluster_id)
314 c.nodes = NodeList(list(c.get_nodes()))
315 return c
316
317
koder aka kdanilov73084622016-11-16 21:51:08 +0200318def get_all_nodes(conn: Connection) -> Iterator[Node]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200319 """Get all nodes from Fuel"""
320 for node_desc in conn.get('api/nodes'):
321 yield Node(conn, **node_desc)
322
323
koder aka kdanilov73084622016-11-16 21:51:08 +0200324def get_all_clusters(conn: Connection) -> Iterator[Cluster]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200325 """Get all clusters"""
326 for cluster_desc in conn.get('api/clusters'):
327 yield Cluster(conn, **cluster_desc)
328
329
koder aka kdanilov73084622016-11-16 21:51:08 +0200330def get_cluster_id(conn: Connection, name: str) -> int:
Yulia Portnova3556a062015-03-17 16:30:11 +0200331 """Get cluster id by name"""
332 for cluster in get_all_clusters(conn):
333 if cluster.name == name:
Yulia Portnova3556a062015-03-17 16:30:11 +0200334 return cluster.id
335
koder aka kdanilovda45e882015-04-06 02:24:42 +0300336 raise ValueError("Cluster {0} not found".format(name))
337