blob: b110fc2cd7f09608a34c7378accee5aa91eea037 [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
Yulia Portnova3556a062015-03-17 16:30:11 +02005import urllib2
Yulia Portnova3556a062015-03-17 16:30:11 +02006import urlparse
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +03007from functools import partial, wraps
Yulia Portnova3556a062015-03-17 16:30:11 +02008
9import netaddr
10
11from keystoneclient.v2_0 import Client as keystoneclient
12from keystoneclient import exceptions
13
14
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030015logger = logging.getLogger("wally.fuel_api")
Yulia Portnova3556a062015-03-17 16:30:11 +020016
17
18class Urllib2HTTP(object):
19 """
20 class for making HTTP requests
21 """
22
23 allowed_methods = ('get', 'put', 'post', 'delete', 'patch', 'head')
24
koder aka kdanilovcee43342015-04-14 22:52:53 +030025 def __init__(self, root_url, headers=None):
Yulia Portnova3556a062015-03-17 16:30:11 +020026 """
27 """
28 if root_url.endswith('/'):
29 self.root_url = root_url[:-1]
30 else:
31 self.root_url = root_url
32
33 self.headers = headers if headers is not None else {}
Yulia Portnova3556a062015-03-17 16:30:11 +020034
35 def host(self):
36 return self.root_url.split('/')[2]
37
38 def do(self, method, path, params=None):
39 if path.startswith('/'):
40 url = self.root_url + path
41 else:
42 url = self.root_url + '/' + path
43
44 if method == 'get':
45 assert params == {} or params is None
46 data_json = None
47 else:
48 data_json = json.dumps(params)
49
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030050 logger.debug("HTTP: {0} {1}".format(method.upper(), url))
Yulia Portnova3556a062015-03-17 16:30:11 +020051
52 request = urllib2.Request(url,
53 data=data_json,
54 headers=self.headers)
55 if data_json is not None:
56 request.add_header('Content-Type', 'application/json')
57
58 request.get_method = lambda: method.upper()
59 response = urllib2.urlopen(request)
60
koder aka kdanilov6b1341a2015-04-21 22:44:21 +030061 logger.debug("HTTP Responce: {0}".format(response.code))
Yulia Portnova3556a062015-03-17 16:30:11 +020062
63 if response.code < 200 or response.code > 209:
64 raise IndexError(url)
65
66 content = response.read()
67
68 if '' == content:
69 return None
70
71 return json.loads(content)
72
73 def __getattr__(self, name):
74 if name in self.allowed_methods:
75 return partial(self.do, name)
76 raise AttributeError(name)
77
78
79class KeystoneAuth(Urllib2HTTP):
koder aka kdanilovcee43342015-04-14 22:52:53 +030080 def __init__(self, root_url, creds, headers=None):
81 super(KeystoneAuth, self).__init__(root_url, headers)
Yulia Portnova3556a062015-03-17 16:30:11 +020082 admin_node_ip = urlparse.urlparse(root_url).hostname
83 self.keystone_url = "http://{0}:5000/v2.0".format(admin_node_ip)
84 self.keystone = keystoneclient(
85 auth_url=self.keystone_url, **creds)
86 self.refresh_token()
87
88 def refresh_token(self):
89 """Get new token from keystone and update headers"""
90 try:
91 self.keystone.authenticate()
92 self.headers['X-Auth-Token'] = self.keystone.auth_token
93 except exceptions.AuthorizationFailure:
koder aka kdanilovcee43342015-04-14 22:52:53 +030094 logger.warning(
95 'Cant establish connection to keystone with url %s',
96 self.keystone_url)
Yulia Portnova3556a062015-03-17 16:30:11 +020097
98 def do(self, method, path, params=None):
99 """Do request. If gets 401 refresh token"""
100 try:
101 return super(KeystoneAuth, self).do(method, path, params)
102 except urllib2.HTTPError as e:
103 if e.code == 401:
koder aka kdanilovcee43342015-04-14 22:52:53 +0300104 logger.warning(
105 'Authorization failure: {0}'.format(e.read()))
Yulia Portnova3556a062015-03-17 16:30:11 +0200106 self.refresh_token()
107 return super(KeystoneAuth, self).do(method, path, params)
108 else:
109 raise
110
111
112def get_inline_param_list(url):
113 format_param_rr = re.compile(r"\{([a-zA-Z_]+)\}")
114 for match in format_param_rr.finditer(url):
115 yield match.group(1)
116
117
118class RestObj(object):
119 name = None
120 id = None
121
122 def __init__(self, conn, **kwargs):
123 self.__dict__.update(kwargs)
124 self.__connection__ = conn
125
126 def __str__(self):
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300127 res = ["{0}({1}):".format(self.__class__.__name__, self.name)]
Yulia Portnova3556a062015-03-17 16:30:11 +0200128 for k, v in sorted(self.__dict__.items()):
129 if k.startswith('__') or k.endswith('__'):
130 continue
131 if k != 'name':
koder aka kdanilov6b1341a2015-04-21 22:44:21 +0300132 res.append(" {0}={1!r}".format(k, v))
Yulia Portnova3556a062015-03-17 16:30:11 +0200133 return "\n".join(res)
134
135 def __getitem__(self, item):
136 return getattr(self, item)
137
138
139def make_call(method, url):
140 def closure(obj, entire_obj=None, **data):
141 inline_params_vals = {}
142 for name in get_inline_param_list(url):
143 if name in data:
144 inline_params_vals[name] = data[name]
145 del data[name]
146 else:
147 inline_params_vals[name] = getattr(obj, name)
148 result_url = url.format(**inline_params_vals)
149
150 if entire_obj is not None:
151 if data != {}:
152 raise ValueError("Both entire_obj and data provided")
153 data = entire_obj
154 return obj.__connection__.do(method, result_url, params=data)
155 return closure
156
157
158PUT = partial(make_call, 'put')
159GET = partial(make_call, 'get')
160DELETE = partial(make_call, 'delete')
161
162
163def with_timeout(tout, message):
164 def closure(func):
165 @wraps(func)
166 def closure2(*dt, **mp):
167 ctime = time.time()
168 etime = ctime + tout
169
170 while ctime < etime:
171 if func(*dt, **mp):
172 return
173 sleep_time = ctime + 1 - time.time()
174 if sleep_time > 0:
175 time.sleep(sleep_time)
176 ctime = time.time()
177 raise RuntimeError("Timeout during " + message)
178 return closure2
179 return closure
180
181
182# ------------------------------- ORM ----------------------------------------
183
184
185def get_fuel_info(url):
186 conn = Urllib2HTTP(url)
187 return FuelInfo(conn)
188
189
190class FuelInfo(RestObj):
191
192 """Class represents Fuel installation info"""
193
194 get_nodes = GET('api/nodes')
195 get_clusters = GET('api/clusters')
196 get_cluster = GET('api/clusters/{id}')
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300197 get_info = GET('api/releases')
Yulia Portnova3556a062015-03-17 16:30:11 +0200198
199 @property
200 def nodes(self):
201 """Get all fuel nodes"""
202 return NodeList([Node(self.__connection__, **node) for node
203 in self.get_nodes()])
204
205 @property
206 def free_nodes(self):
207 """Get unallocated nodes"""
208 return NodeList([Node(self.__connection__, **node) for node in
209 self.get_nodes() if not node['cluster']])
210
211 @property
212 def clusters(self):
213 """List clusters in fuel"""
214 return [Cluster(self.__connection__, **cluster) for cluster
215 in self.get_clusters()]
216
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300217 def get_version(self):
218 for info in self.get_info():
219 vers = info['version'].split("-")[1].split('.')
220 return map(int, vers)
221 raise ValueError("No version found")
222
Yulia Portnova3556a062015-03-17 16:30:11 +0200223
224class Node(RestObj):
225 """Represents node in Fuel"""
226
227 get_info = GET('/api/nodes/{id}')
228 get_interfaces = GET('/api/nodes/{id}/interfaces')
229 update_interfaces = PUT('/api/nodes/{id}/interfaces')
230
231 def set_network_assigment(self, mapping):
232 """Assings networks to interfaces
233 :param mapping: list (dict) interfaces info
234 """
235
236 curr_interfaces = self.get_interfaces()
237
238 network_ids = {}
239 for interface in curr_interfaces:
240 for net in interface['assigned_networks']:
241 network_ids[net['name']] = net['id']
242
243 # transform mappings
244 new_assigned_networks = {}
245
246 for dev_name, networks in mapping.items():
247 new_assigned_networks[dev_name] = []
248 for net_name in networks:
249 nnet = {'name': net_name, 'id': network_ids[net_name]}
250 new_assigned_networks[dev_name].append(nnet)
251
252 # update by ref
253 for dev_descr in curr_interfaces:
254 if dev_descr['name'] in new_assigned_networks:
255 nass = new_assigned_networks[dev_descr['name']]
256 dev_descr['assigned_networks'] = nass
257
258 self.update_interfaces(curr_interfaces, id=self.id)
259
260 def set_node_name(self, name):
261 """Update node name"""
262 self.__connection__.put('api/nodes', [{'id': self.id, 'name': name}])
263
264 def get_network_data(self):
265 """Returns node network data"""
266 node_info = self.get_info()
267 return node_info.get('network_data')
268
Yulia Portnova0e64ea22015-03-20 17:27:22 +0200269 def get_roles(self, pending=False):
Yulia Portnova3556a062015-03-17 16:30:11 +0200270 """Get node roles
271
272 Returns: (roles, pending_roles)
273 """
274 node_info = self.get_info()
Yulia Portnova0e64ea22015-03-20 17:27:22 +0200275 if pending:
276 return node_info.get('roles'), node_info.get('pending_roles')
277 else:
278 return node_info.get('roles')
Yulia Portnova3556a062015-03-17 16:30:11 +0200279
280 def get_ip(self, network='public'):
281 """Get node ip
282
283 :param network: network to pick
284 """
285 nets = self.get_network_data()
286 for net in nets:
287 if net['name'] == network:
288 iface_name = net['dev']
289 for iface in self.get_info()['meta']['interfaces']:
290 if iface['name'] == iface_name:
291 try:
292 return iface['ip']
293 except KeyError:
294 return netaddr.IPNetwork(net['ip']).ip
295 raise Exception('Network %s not found' % network)
296
297
298class NodeList(list):
299 """Class for filtering nodes through attributes"""
300 allowed_roles = ['controller', 'compute', 'cinder', 'ceph-osd', 'mongo',
301 'zabbix-server']
302
303 def __getattr__(self, name):
304 if name in self.allowed_roles:
305 return [node for node in self if name in node.roles]
306
307
308class Cluster(RestObj):
309 """Class represents Cluster in Fuel"""
310
311 add_node_call = PUT('api/nodes')
312 start_deploy = PUT('api/clusters/{id}/changes')
313 get_status = GET('api/clusters/{id}')
314 delete = DELETE('api/clusters/{id}')
315 get_tasks_status = GET("api/tasks?cluster_id={id}")
316 get_networks = GET(
Yulia Portnova415447a2015-05-12 15:09:21 +0300317 'api/clusters/{id}/network_configuration/neutron')
Yulia Portnova3556a062015-03-17 16:30:11 +0200318
319 get_attributes = GET(
320 'api/clusters/{id}/attributes')
321
322 set_attributes = PUT(
323 'api/clusters/{id}/attributes')
324
325 configure_networks = PUT(
326 'api/clusters/{id}/network_configuration/{net_provider}')
327
328 _get_nodes = GET('api/nodes?cluster_id={id}')
329
330 def __init__(self, *dt, **mp):
331 super(Cluster, self).__init__(*dt, **mp)
Yulia Portnovad9767042015-04-10 17:32:06 +0300332 self.nodes = NodeList([Node(self.__connection__, **node) for node in
333 self._get_nodes()])
Yulia Portnova3556a062015-03-17 16:30:11 +0200334 self.network_roles = {}
335
336 def check_exists(self):
337 """Check if cluster exists"""
338 try:
339 self.get_status()
340 return True
341 except urllib2.HTTPError as err:
342 if err.code == 404:
343 return False
344 raise
345
Yulia Portnovad9767042015-04-10 17:32:06 +0300346 def get_openrc(self):
Yulia Portnova00025a52015-04-07 12:17:32 +0300347 access = self.get_attributes()['editable']['access']
348 creds = {}
349 creds['username'] = access['user']['value']
350 creds['password'] = access['password']['value']
351 creds['tenant_name'] = access['tenant']['value']
koder aka kdanilovfd2cfa52015-05-20 03:17:42 +0300352 creds['os_auth_url'] = "http://{0}:5000/v2.0".format(
353 self.get_networks()['public_vip'])
Yulia Portnova00025a52015-04-07 12:17:32 +0300354 return creds
355
Yulia Portnova3556a062015-03-17 16:30:11 +0200356 def get_nodes(self):
357 for node_descr in self._get_nodes():
358 yield Node(self.__connection__, **node_descr)
359
360 def add_node(self, node, roles, interfaces=None):
361 """Add node to cluster
362
363 :param node: Node object
364 :param roles: roles to assign
365 :param interfaces: mapping iface name to networks
366 """
367 data = {}
368 data['pending_roles'] = roles
369 data['cluster_id'] = self.id
370 data['id'] = node.id
371 data['pending_addition'] = True
372
koder aka kdanilovcee43342015-04-14 22:52:53 +0300373 logger.debug("Adding node %s to cluster..." % node.id)
Yulia Portnova3556a062015-03-17 16:30:11 +0200374
375 self.add_node_call([data])
376 self.nodes.append(node)
377
378 if interfaces is not None:
379 networks = {}
380 for iface_name, params in interfaces.items():
381 networks[iface_name] = params['networks']
382
383 node.set_network_assigment(networks)
384
385 def wait_operational(self, timeout):
386 """Wait until cluster status operational"""
387 def wo():
388 status = self.get_status()['status']
389 if status == "error":
390 raise Exception("Cluster deploy failed")
391 return self.get_status()['status'] == 'operational'
392 with_timeout(timeout, "deploy cluster")(wo)()
393
394 def deploy(self, timeout):
395 """Start deploy and wait until all tasks finished"""
396 logger.debug("Starting deploy...")
397 self.start_deploy()
398
399 self.wait_operational(timeout)
400
401 def all_tasks_finished_ok(obj):
402 ok = True
403 for task in obj.get_tasks_status():
404 if task['status'] == 'error':
405 raise Exception('Task execution error')
406 elif task['status'] != 'ready':
407 ok = False
408 return ok
409
410 wto = with_timeout(timeout, "wait deployment finished")
411 wto(all_tasks_finished_ok)(self)
412
413 def set_networks(self, net_descriptions):
414 """Update cluster networking parameters"""
415 configuration = self.get_networks()
416 current_networks = configuration['networks']
417 parameters = configuration['networking_parameters']
418
419 if net_descriptions.get('networks'):
420 net_mapping = net_descriptions['networks']
421
422 for net in current_networks:
423 net_desc = net_mapping.get(net['name'])
424 if net_desc:
425 net.update(net_desc)
426
427 if net_descriptions.get('networking_parameters'):
428 parameters.update(net_descriptions['networking_parameters'])
429
430 self.configure_networks(**configuration)
431
432
433def reflect_cluster(conn, cluster_id):
434 """Returns cluster object by id"""
435 c = Cluster(conn, id=cluster_id)
436 c.nodes = NodeList(list(c.get_nodes()))
437 return c
438
439
440def get_all_nodes(conn):
441 """Get all nodes from Fuel"""
442 for node_desc in conn.get('api/nodes'):
443 yield Node(conn, **node_desc)
444
445
446def get_all_clusters(conn):
447 """Get all clusters"""
448 for cluster_desc in conn.get('api/clusters'):
449 yield Cluster(conn, **cluster_desc)
450
451
koder aka kdanilovda45e882015-04-06 02:24:42 +0300452def get_cluster_id(conn, name):
Yulia Portnova3556a062015-03-17 16:30:11 +0200453 """Get cluster id by name"""
454 for cluster in get_all_clusters(conn):
455 if cluster.name == name:
Yulia Portnova3556a062015-03-17 16:30:11 +0200456 return cluster.id
457
koder aka kdanilovda45e882015-04-06 02:24:42 +0300458 raise ValueError("Cluster {0} not found".format(name))
459
Yulia Portnova3556a062015-03-17 16:30:11 +0200460
461sections = {
462 'sahara': 'additional_components',
463 'murano': 'additional_components',
464 'ceilometer': 'additional_components',
465 'volumes_ceph': 'storage',
466 'images_ceph': 'storage',
467 'ephemeral_ceph': 'storage',
468 'objects_ceph': 'storage',
469 'osd_pool_size': 'storage',
470 'volumes_lvm': 'storage',
471 'volumes_vmdk': 'storage',
472 'tenant': 'access',
473 'password': 'access',
474 'user': 'access',
475 'vc_password': 'vcenter',
476 'cluster': 'vcenter',
477 'host_ip': 'vcenter',
478 'vc_user': 'vcenter',
479 'use_vcenter': 'vcenter',
480}
481
482
483def create_empty_cluster(conn, cluster_desc, debug_mode=False):
484 """Create new cluster with configuration provided"""
485
486 data = {}
487 data['nodes'] = []
488 data['tasks'] = []
489 data['name'] = cluster_desc['name']
490 data['release'] = cluster_desc['release']
491 data['mode'] = cluster_desc.get('deployment_mode')
492 data['net_provider'] = cluster_desc.get('net_provider')
493
494 params = conn.post(path='/api/clusters', params=data)
495 cluster = Cluster(conn, **params)
496
497 attributes = cluster.get_attributes()
498
499 ed_attrs = attributes['editable']
500
501 ed_attrs['common']['libvirt_type']['value'] = \
502 cluster_desc.get('libvirt_type', 'kvm')
503
504 if 'nodes' in cluster_desc:
505 use_ceph = cluster_desc['nodes'].get('ceph_osd', None) is not None
506 else:
507 use_ceph = False
508
509 if 'storage_type' in cluster_desc:
510 st = cluster_desc['storage_type']
511 if st == 'ceph':
512 use_ceph = True
513 else:
514 use_ceph = False
515
516 if use_ceph:
517 opts = ['ephemeral_ceph', 'images_ceph', 'images_vcenter']
518 opts += ['iser', 'objects_ceph', 'volumes_ceph']
519 opts += ['volumes_lvm', 'volumes_vmdk']
520
521 for name in opts:
522 val = ed_attrs['storage'][name]
523 if val['type'] == 'checkbox':
524 is_ceph = ('images_ceph' == name)
525 is_ceph = is_ceph or ('volumes_ceph' == name)
526
527 if is_ceph:
528 val['value'] = True
529 else:
530 val['value'] = False
531 # else:
532 # raise NotImplementedError("Non-ceph storages are not implemented")
533
534 cluster.set_attributes(attributes)
535
536 return cluster