blob: 2addaf41976aab1ff84ae551629c2e898a2a84a8 [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.request
6import urllib.parse
koder aka kdanilov73084622016-11-16 21:51:08 +02007from typing import Dict, Any, Iterator, Match, List, Callable
8from functools import partial
Yulia Portnova3556a062015-03-17 16:30:11 +02009
10import netaddr
Yulia Portnova3556a062015-03-17 16:30:11 +020011from keystoneclient import exceptions
koder aka kdanilov73084622016-11-16 21:51:08 +020012from keystoneclient.v2_0 import Client as keystoneclient
Yulia Portnova3556a062015-03-17 16:30:11 +020013
14
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030015logger = logging.getLogger("wally.fuel_api")
Yulia Portnova3556a062015-03-17 16:30:11 +020016
17
koder aka kdanilov73084622016-11-16 21:51:08 +020018class Connection(metaclass=abc.ABCMeta):
19 @abc.abstractmethod
20 def do(self, method: str, path: str, params: Dict = None) -> Dict:
21 pass
22
23 @abc.abstractmethod
24 def get(self, path: str, params: Dict = None) -> Dict:
25 pass
26
27
28class Urllib2HTTP(Connection):
Yulia Portnova3556a062015-03-17 16:30:11 +020029 """
30 class for making HTTP requests
31 """
32
33 allowed_methods = ('get', 'put', 'post', 'delete', 'patch', 'head')
34
koder aka kdanilov73084622016-11-16 21:51:08 +020035 def __init__(self, root_url: str, headers: Dict[str, str] = None) -> None:
Yulia Portnova3556a062015-03-17 16:30:11 +020036 """
37 """
38 if root_url.endswith('/'):
39 self.root_url = root_url[:-1]
40 else:
41 self.root_url = root_url
42
koder aka kdanilov73084622016-11-16 21:51:08 +020043 self.host = urllib.parse.urlparse(self.root_url).hostname
Yulia Portnova3556a062015-03-17 16:30:11 +020044
koder aka kdanilov73084622016-11-16 21:51:08 +020045 if headers is None:
46 self.headers = {} # type: Dict[str, str]
47 else:
48 self.headers = headers
Yulia Portnova3556a062015-03-17 16:30:11 +020049
koder aka kdanilov73084622016-11-16 21:51:08 +020050 def do(self, method: str, path: str, params: Dict = None) -> Dict:
Yulia Portnova3556a062015-03-17 16:30:11 +020051 if path.startswith('/'):
52 url = self.root_url + path
53 else:
54 url = self.root_url + '/' + path
55
56 if method == 'get':
57 assert params == {} or params is None
58 data_json = None
59 else:
60 data_json = json.dumps(params)
61
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030062 logger.debug("HTTP: {0} {1}".format(method.upper(), url))
Yulia Portnova3556a062015-03-17 16:30:11 +020063
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030064 request = urllib.request.Request(url,
65 data=data_json,
66 headers=self.headers)
Yulia Portnova3556a062015-03-17 16:30:11 +020067 if data_json is not None:
68 request.add_header('Content-Type', 'application/json')
69
70 request.get_method = lambda: method.upper()
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030071 response = urllib.request.urlopen(request)
Yulia Portnova3556a062015-03-17 16:30:11 +020072
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030073 logger.debug("HTTP Responce: {0}".format(response.code))
Yulia Portnova3556a062015-03-17 16:30:11 +020074
75 if response.code < 200 or response.code > 209:
76 raise IndexError(url)
77
78 content = response.read()
79
80 if '' == content:
81 return None
82
83 return json.loads(content)
84
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030085 def __getattr__(self, name: str):
Yulia Portnova3556a062015-03-17 16:30:11 +020086 if name in self.allowed_methods:
87 return partial(self.do, name)
88 raise AttributeError(name)
89
90
91class KeystoneAuth(Urllib2HTTP):
koder aka kdanilov22d134e2016-11-08 11:33:19 +020092 def __init__(self, root_url: str, creds: Dict[str, str], headers: Dict[str, str] = None) -> None:
koder aka kdanilovcee43342015-04-14 22:52:53 +030093 super(KeystoneAuth, self).__init__(root_url, headers)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030094 admin_node_ip = urllib.parse.urlparse(root_url).hostname
Yulia Portnova3556a062015-03-17 16:30:11 +020095 self.keystone_url = "http://{0}:5000/v2.0".format(admin_node_ip)
96 self.keystone = keystoneclient(
97 auth_url=self.keystone_url, **creds)
98 self.refresh_token()
99
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200100 def refresh_token(self) -> None:
Yulia Portnova3556a062015-03-17 16:30:11 +0200101 """Get new token from keystone and update headers"""
102 try:
103 self.keystone.authenticate()
104 self.headers['X-Auth-Token'] = self.keystone.auth_token
105 except exceptions.AuthorizationFailure:
koder aka kdanilovcee43342015-04-14 22:52:53 +0300106 logger.warning(
107 'Cant establish connection to keystone with url %s',
108 self.keystone_url)
Yulia Portnova3556a062015-03-17 16:30:11 +0200109
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200110 def do(self, method: str, path: str, params: Dict[str, str] = None) -> Dict[str, Any]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200111 """Do request. If gets 401 refresh token"""
112 try:
113 return super(KeystoneAuth, self).do(method, path, params)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300114 except urllib.request.HTTPError as e:
Yulia Portnova3556a062015-03-17 16:30:11 +0200115 if e.code == 401:
koder aka kdanilovcee43342015-04-14 22:52:53 +0300116 logger.warning(
117 'Authorization failure: {0}'.format(e.read()))
Yulia Portnova3556a062015-03-17 16:30:11 +0200118 self.refresh_token()
119 return super(KeystoneAuth, self).do(method, path, params)
120 else:
121 raise
122
123
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200124def get_inline_param_list(url: str) -> Iterator[Match]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200125 format_param_rr = re.compile(r"\{([a-zA-Z_]+)\}")
126 for match in format_param_rr.finditer(url):
127 yield match.group(1)
128
129
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300130class RestObj:
Yulia Portnova3556a062015-03-17 16:30:11 +0200131 name = None
132 id = None
133
koder aka kdanilov73084622016-11-16 21:51:08 +0200134 def __init__(self, conn, **kwargs) -> None:
Yulia Portnova3556a062015-03-17 16:30:11 +0200135 self.__dict__.update(kwargs)
136 self.__connection__ = conn
137
koder aka kdanilov73084622016-11-16 21:51:08 +0200138 def __str__(self) -> str:
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300139 res = ["{0}({1}):".format(self.__class__.__name__, self.name)]
Yulia Portnova3556a062015-03-17 16:30:11 +0200140 for k, v in sorted(self.__dict__.items()):
141 if k.startswith('__') or k.endswith('__'):
142 continue
143 if k != 'name':
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300144 res.append(" {0}={1!r}".format(k, v))
Yulia Portnova3556a062015-03-17 16:30:11 +0200145 return "\n".join(res)
146
koder aka kdanilov73084622016-11-16 21:51:08 +0200147 def __getitem__(self, item: str) -> Any:
Yulia Portnova3556a062015-03-17 16:30:11 +0200148 return getattr(self, item)
149
150
koder aka kdanilov73084622016-11-16 21:51:08 +0200151def make_call(method: str, url: str) -> Callable[[Any, Any], Dict]:
152 def closure(obj: Any, entire_obj: Any = None, **data) -> Dict:
Yulia Portnova3556a062015-03-17 16:30:11 +0200153 inline_params_vals = {}
154 for name in get_inline_param_list(url):
155 if name in data:
156 inline_params_vals[name] = data[name]
157 del data[name]
158 else:
159 inline_params_vals[name] = getattr(obj, name)
160 result_url = url.format(**inline_params_vals)
161
162 if entire_obj is not None:
163 if data != {}:
164 raise ValueError("Both entire_obj and data provided")
165 data = entire_obj
166 return obj.__connection__.do(method, result_url, params=data)
167 return closure
168
169
170PUT = partial(make_call, 'put')
171GET = partial(make_call, 'get')
172DELETE = partial(make_call, 'delete')
173
Yulia Portnova3556a062015-03-17 16:30:11 +0200174# ------------------------------- ORM ----------------------------------------
175
176
koder aka kdanilov73084622016-11-16 21:51:08 +0200177def get_fuel_info(url: str) -> 'FuelInfo':
Yulia Portnova3556a062015-03-17 16:30:11 +0200178 conn = Urllib2HTTP(url)
179 return FuelInfo(conn)
180
181
182class FuelInfo(RestObj):
183
184 """Class represents Fuel installation info"""
185
186 get_nodes = GET('api/nodes')
187 get_clusters = GET('api/clusters')
188 get_cluster = GET('api/clusters/{id}')
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300189 get_info = GET('api/releases')
Yulia Portnova3556a062015-03-17 16:30:11 +0200190
191 @property
koder aka kdanilov73084622016-11-16 21:51:08 +0200192 def nodes(self) -> 'NodeList':
Yulia Portnova3556a062015-03-17 16:30:11 +0200193 """Get all fuel nodes"""
194 return NodeList([Node(self.__connection__, **node) for node
195 in self.get_nodes()])
196
197 @property
koder aka kdanilov73084622016-11-16 21:51:08 +0200198 def free_nodes(self) -> 'NodeList':
Yulia Portnova3556a062015-03-17 16:30:11 +0200199 """Get unallocated nodes"""
200 return NodeList([Node(self.__connection__, **node) for node in
201 self.get_nodes() if not node['cluster']])
202
203 @property
koder aka kdanilov73084622016-11-16 21:51:08 +0200204 def clusters(self) -> List['Cluster']:
Yulia Portnova3556a062015-03-17 16:30:11 +0200205 """List clusters in fuel"""
206 return [Cluster(self.__connection__, **cluster) for cluster
207 in self.get_clusters()]
208
koder aka kdanilov73084622016-11-16 21:51:08 +0200209 def get_version(self) -> List[int]:
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300210 for info in self.get_info():
211 vers = info['version'].split("-")[1].split('.')
koder aka kdanilov73084622016-11-16 21:51:08 +0200212 return list(map(int, vers))
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300213 raise ValueError("No version found")
214
Yulia Portnova3556a062015-03-17 16:30:11 +0200215
216class Node(RestObj):
217 """Represents node in Fuel"""
218
219 get_info = GET('/api/nodes/{id}')
220 get_interfaces = GET('/api/nodes/{id}/interfaces')
Yulia Portnova3556a062015-03-17 16:30:11 +0200221
koder aka kdanilov73084622016-11-16 21:51:08 +0200222 def get_network_data(self) -> Dict:
Yulia Portnova3556a062015-03-17 16:30:11 +0200223 """Returns node network data"""
koder aka kdanilov73084622016-11-16 21:51:08 +0200224 return self.get_info().get('network_data')
Yulia Portnova3556a062015-03-17 16:30:11 +0200225
koder aka kdanilov73084622016-11-16 21:51:08 +0200226 def get_roles(self) -> List[str]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200227 """Get node roles
228
229 Returns: (roles, pending_roles)
230 """
koder aka kdanilov73084622016-11-16 21:51:08 +0200231 return self.get_info().get('roles')
Yulia Portnova3556a062015-03-17 16:30:11 +0200232
koder aka kdanilov73084622016-11-16 21:51:08 +0200233 def get_ip(self, network='public') -> netaddr.IPAddress:
Yulia Portnova3556a062015-03-17 16:30:11 +0200234 """Get node ip
235
236 :param network: network to pick
237 """
238 nets = self.get_network_data()
239 for net in nets:
240 if net['name'] == network:
241 iface_name = net['dev']
242 for iface in self.get_info()['meta']['interfaces']:
243 if iface['name'] == iface_name:
244 try:
245 return iface['ip']
246 except KeyError:
247 return netaddr.IPNetwork(net['ip']).ip
248 raise Exception('Network %s not found' % network)
249
250
251class NodeList(list):
252 """Class for filtering nodes through attributes"""
253 allowed_roles = ['controller', 'compute', 'cinder', 'ceph-osd', 'mongo',
254 'zabbix-server']
255
koder aka kdanilov73084622016-11-16 21:51:08 +0200256 def __getattr__(self, name: str) -> List[Node]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200257 if name in self.allowed_roles:
258 return [node for node in self if name in node.roles]
259
260
261class Cluster(RestObj):
262 """Class represents Cluster in Fuel"""
263
Yulia Portnova3556a062015-03-17 16:30:11 +0200264 get_status = GET('api/clusters/{id}')
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300265 get_networks = GET('api/clusters/{id}/network_configuration/neutron')
266 get_attributes = GET('api/clusters/{id}/attributes')
Yulia Portnova3556a062015-03-17 16:30:11 +0200267 _get_nodes = GET('api/nodes?cluster_id={id}')
268
koder aka kdanilov73084622016-11-16 21:51:08 +0200269 def __init__(self, *dt, **mp) -> None:
Yulia Portnova3556a062015-03-17 16:30:11 +0200270 super(Cluster, self).__init__(*dt, **mp)
Yulia Portnovad9767042015-04-10 17:32:06 +0300271 self.nodes = NodeList([Node(self.__connection__, **node) for node in
272 self._get_nodes()])
Yulia Portnova3556a062015-03-17 16:30:11 +0200273 self.network_roles = {}
274
koder aka kdanilov73084622016-11-16 21:51:08 +0200275 def check_exists(self) -> bool:
Yulia Portnova3556a062015-03-17 16:30:11 +0200276 """Check if cluster exists"""
277 try:
278 self.get_status()
279 return True
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300280 except urllib.request.HTTPError as err:
Yulia Portnova3556a062015-03-17 16:30:11 +0200281 if err.code == 404:
282 return False
283 raise
284
koder aka kdanilov73084622016-11-16 21:51:08 +0200285 def get_openrc(self) -> Dict[str, str]:
Yulia Portnova00025a52015-04-07 12:17:32 +0300286 access = self.get_attributes()['editable']['access']
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300287 creds = {'username': access['user']['value'],
288 'password': access['password']['value'],
289 'tenant_name': access['tenant']['value']}
Michael Semenov8d6c0572015-08-25 12:59:05 +0300290
291 version = FuelInfo(self.__connection__).get_version()
koder aka kdanilov5ea9df02015-12-04 21:46:06 +0200292 # only HTTPS since 7.0
293 if version >= [7, 0]:
Michael Semenov8d6c0572015-08-25 12:59:05 +0300294 creds['insecure'] = "True"
295 creds['os_auth_url'] = "https://{0}:5000/v2.0".format(
296 self.get_networks()['public_vip'])
297 else:
298 creds['os_auth_url'] = "http://{0}:5000/v2.0".format(
299 self.get_networks()['public_vip'])
Yulia Portnova00025a52015-04-07 12:17:32 +0300300 return creds
301
koder aka kdanilov73084622016-11-16 21:51:08 +0200302 def get_nodes(self) -> Iterator[Node]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200303 for node_descr in self._get_nodes():
304 yield Node(self.__connection__, **node_descr)
305
Yulia Portnova3556a062015-03-17 16:30:11 +0200306
koder aka kdanilov73084622016-11-16 21:51:08 +0200307def reflect_cluster(conn: Connection, cluster_id: int) -> Cluster:
Yulia Portnova3556a062015-03-17 16:30:11 +0200308 """Returns cluster object by id"""
309 c = Cluster(conn, id=cluster_id)
310 c.nodes = NodeList(list(c.get_nodes()))
311 return c
312
313
koder aka kdanilov73084622016-11-16 21:51:08 +0200314def get_all_nodes(conn: Connection) -> Iterator[Node]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200315 """Get all nodes from Fuel"""
316 for node_desc in conn.get('api/nodes'):
317 yield Node(conn, **node_desc)
318
319
koder aka kdanilov73084622016-11-16 21:51:08 +0200320def get_all_clusters(conn: Connection) -> Iterator[Cluster]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200321 """Get all clusters"""
322 for cluster_desc in conn.get('api/clusters'):
323 yield Cluster(conn, **cluster_desc)
324
325
koder aka kdanilov73084622016-11-16 21:51:08 +0200326def get_cluster_id(conn: Connection, name: str) -> int:
Yulia Portnova3556a062015-03-17 16:30:11 +0200327 """Get cluster id by name"""
328 for cluster in get_all_clusters(conn):
329 if cluster.name == name:
Yulia Portnova3556a062015-03-17 16:30:11 +0200330 return cluster.id
331
koder aka kdanilovda45e882015-04-06 02:24:42 +0300332 raise ValueError("Cluster {0} not found".format(name))
333