blob: 03beb37796dd9e05440cd540ef2eadc293d7b192 [file] [log] [blame]
Yulia Portnova3556a062015-03-17 16:30:11 +02001import re
2import json
3import time
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 kdanilov22d134e2016-11-08 11:33:19 +02007from typing import Dict, Any, Iterator, Match
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +03008from functools import partial, wraps
Yulia Portnova3556a062015-03-17 16:30:11 +02009
10import netaddr
11
12from keystoneclient.v2_0 import Client as keystoneclient
13from keystoneclient import exceptions
14
15
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030016logger = logging.getLogger("wally.fuel_api")
Yulia Portnova3556a062015-03-17 16:30:11 +020017
18
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030019class Urllib2HTTP:
Yulia Portnova3556a062015-03-17 16:30:11 +020020 """
21 class for making HTTP requests
22 """
23
24 allowed_methods = ('get', 'put', 'post', 'delete', 'patch', 'head')
25
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030026 def __init__(self, root_url: str, headers: Dict[str, str]=None):
Yulia Portnova3556a062015-03-17 16:30:11 +020027 """
28 """
29 if root_url.endswith('/'):
30 self.root_url = root_url[:-1]
31 else:
32 self.root_url = root_url
33
34 self.headers = headers if headers is not None else {}
Yulia Portnova3556a062015-03-17 16:30:11 +020035
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030036 def host(self) -> str:
Yulia Portnova3556a062015-03-17 16:30:11 +020037 return self.root_url.split('/')[2]
38
koder aka kdanilov22d134e2016-11-08 11:33:19 +020039 def do(self, method: str, path: str, params: Dict[Any, Any]=None) -> Dict[str, Any]:
Yulia Portnova3556a062015-03-17 16:30:11 +020040 if path.startswith('/'):
41 url = self.root_url + path
42 else:
43 url = self.root_url + '/' + path
44
45 if method == 'get':
46 assert params == {} or params is None
47 data_json = None
48 else:
49 data_json = json.dumps(params)
50
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030051 logger.debug("HTTP: {0} {1}".format(method.upper(), url))
Yulia Portnova3556a062015-03-17 16:30:11 +020052
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030053 request = urllib.request.Request(url,
54 data=data_json,
55 headers=self.headers)
Yulia Portnova3556a062015-03-17 16:30:11 +020056 if data_json is not None:
57 request.add_header('Content-Type', 'application/json')
58
59 request.get_method = lambda: method.upper()
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030060 response = urllib.request.urlopen(request)
Yulia Portnova3556a062015-03-17 16:30:11 +020061
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030062 logger.debug("HTTP Responce: {0}".format(response.code))
Yulia Portnova3556a062015-03-17 16:30:11 +020063
64 if response.code < 200 or response.code > 209:
65 raise IndexError(url)
66
67 content = response.read()
68
69 if '' == content:
70 return None
71
72 return json.loads(content)
73
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030074 def __getattr__(self, name: str):
Yulia Portnova3556a062015-03-17 16:30:11 +020075 if name in self.allowed_methods:
76 return partial(self.do, name)
77 raise AttributeError(name)
78
79
80class KeystoneAuth(Urllib2HTTP):
koder aka kdanilov22d134e2016-11-08 11:33:19 +020081 def __init__(self, root_url: str, creds: Dict[str, str], headers: Dict[str, str] = None) -> None:
koder aka kdanilovcee43342015-04-14 22:52:53 +030082 super(KeystoneAuth, self).__init__(root_url, headers)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030083 admin_node_ip = urllib.parse.urlparse(root_url).hostname
Yulia Portnova3556a062015-03-17 16:30:11 +020084 self.keystone_url = "http://{0}:5000/v2.0".format(admin_node_ip)
85 self.keystone = keystoneclient(
86 auth_url=self.keystone_url, **creds)
87 self.refresh_token()
88
koder aka kdanilov22d134e2016-11-08 11:33:19 +020089 def refresh_token(self) -> None:
Yulia Portnova3556a062015-03-17 16:30:11 +020090 """Get new token from keystone and update headers"""
91 try:
92 self.keystone.authenticate()
93 self.headers['X-Auth-Token'] = self.keystone.auth_token
94 except exceptions.AuthorizationFailure:
koder aka kdanilovcee43342015-04-14 22:52:53 +030095 logger.warning(
96 'Cant establish connection to keystone with url %s',
97 self.keystone_url)
Yulia Portnova3556a062015-03-17 16:30:11 +020098
koder aka kdanilov22d134e2016-11-08 11:33:19 +020099 def do(self, method: str, path: str, params: Dict[str, str] = None) -> Dict[str, Any]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200100 """Do request. If gets 401 refresh token"""
101 try:
102 return super(KeystoneAuth, self).do(method, path, params)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300103 except urllib.request.HTTPError as e:
Yulia Portnova3556a062015-03-17 16:30:11 +0200104 if e.code == 401:
koder aka kdanilovcee43342015-04-14 22:52:53 +0300105 logger.warning(
106 'Authorization failure: {0}'.format(e.read()))
Yulia Portnova3556a062015-03-17 16:30:11 +0200107 self.refresh_token()
108 return super(KeystoneAuth, self).do(method, path, params)
109 else:
110 raise
111
112
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200113def get_inline_param_list(url: str) -> Iterator[Match]:
Yulia Portnova3556a062015-03-17 16:30:11 +0200114 format_param_rr = re.compile(r"\{([a-zA-Z_]+)\}")
115 for match in format_param_rr.finditer(url):
116 yield match.group(1)
117
118
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300119class RestObj:
Yulia Portnova3556a062015-03-17 16:30:11 +0200120 name = None
121 id = None
122
123 def __init__(self, conn, **kwargs):
124 self.__dict__.update(kwargs)
125 self.__connection__ = conn
126
127 def __str__(self):
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300128 res = ["{0}({1}):".format(self.__class__.__name__, self.name)]
Yulia Portnova3556a062015-03-17 16:30:11 +0200129 for k, v in sorted(self.__dict__.items()):
130 if k.startswith('__') or k.endswith('__'):
131 continue
132 if k != 'name':
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300133 res.append(" {0}={1!r}".format(k, v))
Yulia Portnova3556a062015-03-17 16:30:11 +0200134 return "\n".join(res)
135
136 def __getitem__(self, item):
137 return getattr(self, item)
138
139
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300140def make_call(method: str, url: str):
Yulia Portnova3556a062015-03-17 16:30:11 +0200141 def closure(obj, entire_obj=None, **data):
142 inline_params_vals = {}
143 for name in get_inline_param_list(url):
144 if name in data:
145 inline_params_vals[name] = data[name]
146 del data[name]
147 else:
148 inline_params_vals[name] = getattr(obj, name)
149 result_url = url.format(**inline_params_vals)
150
151 if entire_obj is not None:
152 if data != {}:
153 raise ValueError("Both entire_obj and data provided")
154 data = entire_obj
155 return obj.__connection__.do(method, result_url, params=data)
156 return closure
157
158
159PUT = partial(make_call, 'put')
160GET = partial(make_call, 'get')
161DELETE = partial(make_call, 'delete')
162
163
164def with_timeout(tout, message):
165 def closure(func):
166 @wraps(func)
167 def closure2(*dt, **mp):
168 ctime = time.time()
169 etime = ctime + tout
170
171 while ctime < etime:
172 if func(*dt, **mp):
173 return
174 sleep_time = ctime + 1 - time.time()
175 if sleep_time > 0:
176 time.sleep(sleep_time)
177 ctime = time.time()
178 raise RuntimeError("Timeout during " + message)
179 return closure2
180 return closure
181
182
183# ------------------------------- ORM ----------------------------------------
184
185
186def get_fuel_info(url):
187 conn = Urllib2HTTP(url)
188 return FuelInfo(conn)
189
190
191class FuelInfo(RestObj):
192
193 """Class represents Fuel installation info"""
194
195 get_nodes = GET('api/nodes')
196 get_clusters = GET('api/clusters')
197 get_cluster = GET('api/clusters/{id}')
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300198 get_info = GET('api/releases')
Yulia Portnova3556a062015-03-17 16:30:11 +0200199
200 @property
201 def nodes(self):
202 """Get all fuel nodes"""
203 return NodeList([Node(self.__connection__, **node) for node
204 in self.get_nodes()])
205
206 @property
207 def free_nodes(self):
208 """Get unallocated nodes"""
209 return NodeList([Node(self.__connection__, **node) for node in
210 self.get_nodes() if not node['cluster']])
211
212 @property
213 def clusters(self):
214 """List clusters in fuel"""
215 return [Cluster(self.__connection__, **cluster) for cluster
216 in self.get_clusters()]
217
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300218 def get_version(self):
219 for info in self.get_info():
220 vers = info['version'].split("-")[1].split('.')
221 return map(int, vers)
222 raise ValueError("No version found")
223
Yulia Portnova3556a062015-03-17 16:30:11 +0200224
225class Node(RestObj):
226 """Represents node in Fuel"""
227
228 get_info = GET('/api/nodes/{id}')
229 get_interfaces = GET('/api/nodes/{id}/interfaces')
Yulia Portnova3556a062015-03-17 16:30:11 +0200230
231 def get_network_data(self):
232 """Returns node network data"""
233 node_info = self.get_info()
234 return node_info.get('network_data')
235
Yulia Portnova0e64ea22015-03-20 17:27:22 +0200236 def get_roles(self, pending=False):
Yulia Portnova3556a062015-03-17 16:30:11 +0200237 """Get node roles
238
239 Returns: (roles, pending_roles)
240 """
241 node_info = self.get_info()
Yulia Portnova0e64ea22015-03-20 17:27:22 +0200242 if pending:
243 return node_info.get('roles'), node_info.get('pending_roles')
244 else:
245 return node_info.get('roles')
Yulia Portnova3556a062015-03-17 16:30:11 +0200246
247 def get_ip(self, network='public'):
248 """Get node ip
249
250 :param network: network to pick
251 """
252 nets = self.get_network_data()
253 for net in nets:
254 if net['name'] == network:
255 iface_name = net['dev']
256 for iface in self.get_info()['meta']['interfaces']:
257 if iface['name'] == iface_name:
258 try:
259 return iface['ip']
260 except KeyError:
261 return netaddr.IPNetwork(net['ip']).ip
262 raise Exception('Network %s not found' % network)
263
264
265class NodeList(list):
266 """Class for filtering nodes through attributes"""
267 allowed_roles = ['controller', 'compute', 'cinder', 'ceph-osd', 'mongo',
268 'zabbix-server']
269
270 def __getattr__(self, name):
271 if name in self.allowed_roles:
272 return [node for node in self if name in node.roles]
273
274
275class Cluster(RestObj):
276 """Class represents Cluster in Fuel"""
277
Yulia Portnova3556a062015-03-17 16:30:11 +0200278 get_status = GET('api/clusters/{id}')
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300279 get_networks = GET('api/clusters/{id}/network_configuration/neutron')
280 get_attributes = GET('api/clusters/{id}/attributes')
Yulia Portnova3556a062015-03-17 16:30:11 +0200281 _get_nodes = GET('api/nodes?cluster_id={id}')
282
283 def __init__(self, *dt, **mp):
284 super(Cluster, self).__init__(*dt, **mp)
Yulia Portnovad9767042015-04-10 17:32:06 +0300285 self.nodes = NodeList([Node(self.__connection__, **node) for node in
286 self._get_nodes()])
Yulia Portnova3556a062015-03-17 16:30:11 +0200287 self.network_roles = {}
288
289 def check_exists(self):
290 """Check if cluster exists"""
291 try:
292 self.get_status()
293 return True
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300294 except urllib.request.HTTPError as err:
Yulia Portnova3556a062015-03-17 16:30:11 +0200295 if err.code == 404:
296 return False
297 raise
298
Yulia Portnovad9767042015-04-10 17:32:06 +0300299 def get_openrc(self):
Yulia Portnova00025a52015-04-07 12:17:32 +0300300 access = self.get_attributes()['editable']['access']
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300301 creds = {'username': access['user']['value'],
302 'password': access['password']['value'],
303 'tenant_name': access['tenant']['value']}
Michael Semenov8d6c0572015-08-25 12:59:05 +0300304
305 version = FuelInfo(self.__connection__).get_version()
koder aka kdanilov5ea9df02015-12-04 21:46:06 +0200306 # only HTTPS since 7.0
307 if version >= [7, 0]:
Michael Semenov8d6c0572015-08-25 12:59:05 +0300308 creds['insecure'] = "True"
309 creds['os_auth_url'] = "https://{0}:5000/v2.0".format(
310 self.get_networks()['public_vip'])
311 else:
312 creds['os_auth_url'] = "http://{0}:5000/v2.0".format(
313 self.get_networks()['public_vip'])
Yulia Portnova00025a52015-04-07 12:17:32 +0300314 return creds
315
Yulia Portnova3556a062015-03-17 16:30:11 +0200316 def get_nodes(self):
317 for node_descr in self._get_nodes():
318 yield Node(self.__connection__, **node_descr)
319
Yulia Portnova3556a062015-03-17 16:30:11 +0200320
321def reflect_cluster(conn, cluster_id):
322 """Returns cluster object by id"""
323 c = Cluster(conn, id=cluster_id)
324 c.nodes = NodeList(list(c.get_nodes()))
325 return c
326
327
328def get_all_nodes(conn):
329 """Get all nodes from Fuel"""
330 for node_desc in conn.get('api/nodes'):
331 yield Node(conn, **node_desc)
332
333
334def get_all_clusters(conn):
335 """Get all clusters"""
336 for cluster_desc in conn.get('api/clusters'):
337 yield Cluster(conn, **cluster_desc)
338
339
koder aka kdanilovda45e882015-04-06 02:24:42 +0300340def get_cluster_id(conn, name):
Yulia Portnova3556a062015-03-17 16:30:11 +0200341 """Get cluster id by name"""
342 for cluster in get_all_clusters(conn):
343 if cluster.name == name:
Yulia Portnova3556a062015-03-17 16:30:11 +0200344 return cluster.id
345
koder aka kdanilovda45e882015-04-06 02:24:42 +0300346 raise ValueError("Cluster {0} not found".format(name))
347