blob: 0567e71615f458dd6998f1413dc41d05ddf06b18 [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']:
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300290 print iface, net['ip']
Yulia Portnova3556a062015-03-17 16:30:11 +0200291 if iface['name'] == iface_name:
292 try:
293 return iface['ip']
294 except KeyError:
295 return netaddr.IPNetwork(net['ip']).ip
296 raise Exception('Network %s not found' % network)
297
298
299class NodeList(list):
300 """Class for filtering nodes through attributes"""
301 allowed_roles = ['controller', 'compute', 'cinder', 'ceph-osd', 'mongo',
302 'zabbix-server']
303
304 def __getattr__(self, name):
305 if name in self.allowed_roles:
306 return [node for node in self if name in node.roles]
307
308
309class Cluster(RestObj):
310 """Class represents Cluster in Fuel"""
311
312 add_node_call = PUT('api/nodes')
313 start_deploy = PUT('api/clusters/{id}/changes')
314 get_status = GET('api/clusters/{id}')
315 delete = DELETE('api/clusters/{id}')
316 get_tasks_status = GET("api/tasks?cluster_id={id}")
317 get_networks = GET(
318 'api/clusters/{id}/network_configuration/{net_provider}')
319
320 get_attributes = GET(
321 'api/clusters/{id}/attributes')
322
323 set_attributes = PUT(
324 'api/clusters/{id}/attributes')
325
326 configure_networks = PUT(
327 'api/clusters/{id}/network_configuration/{net_provider}')
328
329 _get_nodes = GET('api/nodes?cluster_id={id}')
330
331 def __init__(self, *dt, **mp):
332 super(Cluster, self).__init__(*dt, **mp)
Yulia Portnovad9767042015-04-10 17:32:06 +0300333 self.nodes = NodeList([Node(self.__connection__, **node) for node in
334 self._get_nodes()])
Yulia Portnova3556a062015-03-17 16:30:11 +0200335 self.network_roles = {}
336
337 def check_exists(self):
338 """Check if cluster exists"""
339 try:
340 self.get_status()
341 return True
342 except urllib2.HTTPError as err:
343 if err.code == 404:
344 return False
345 raise
346
Yulia Portnovad9767042015-04-10 17:32:06 +0300347 def get_openrc(self):
Yulia Portnova00025a52015-04-07 12:17:32 +0300348 access = self.get_attributes()['editable']['access']
349 creds = {}
350 creds['username'] = access['user']['value']
351 creds['password'] = access['password']['value']
352 creds['tenant_name'] = access['tenant']['value']
Yulia Portnovad9767042015-04-10 17:32:06 +0300353 if self.nodes.controller:
354 contr = self.nodes.controller[0]
355 creds['os_auth_url'] = "http://%s:5000/v2.0" \
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300356 % contr.get_ip(network="public")
Yulia Portnovad9767042015-04-10 17:32:06 +0300357 else:
358 creds['os_auth_url'] = ""
Yulia Portnova00025a52015-04-07 12:17:32 +0300359 return creds
360
Yulia Portnova3556a062015-03-17 16:30:11 +0200361 def get_nodes(self):
362 for node_descr in self._get_nodes():
363 yield Node(self.__connection__, **node_descr)
364
365 def add_node(self, node, roles, interfaces=None):
366 """Add node to cluster
367
368 :param node: Node object
369 :param roles: roles to assign
370 :param interfaces: mapping iface name to networks
371 """
372 data = {}
373 data['pending_roles'] = roles
374 data['cluster_id'] = self.id
375 data['id'] = node.id
376 data['pending_addition'] = True
377
koder aka kdanilovcee43342015-04-14 22:52:53 +0300378 logger.debug("Adding node %s to cluster..." % node.id)
Yulia Portnova3556a062015-03-17 16:30:11 +0200379
380 self.add_node_call([data])
381 self.nodes.append(node)
382
383 if interfaces is not None:
384 networks = {}
385 for iface_name, params in interfaces.items():
386 networks[iface_name] = params['networks']
387
388 node.set_network_assigment(networks)
389
390 def wait_operational(self, timeout):
391 """Wait until cluster status operational"""
392 def wo():
393 status = self.get_status()['status']
394 if status == "error":
395 raise Exception("Cluster deploy failed")
396 return self.get_status()['status'] == 'operational'
397 with_timeout(timeout, "deploy cluster")(wo)()
398
399 def deploy(self, timeout):
400 """Start deploy and wait until all tasks finished"""
401 logger.debug("Starting deploy...")
402 self.start_deploy()
403
404 self.wait_operational(timeout)
405
406 def all_tasks_finished_ok(obj):
407 ok = True
408 for task in obj.get_tasks_status():
409 if task['status'] == 'error':
410 raise Exception('Task execution error')
411 elif task['status'] != 'ready':
412 ok = False
413 return ok
414
415 wto = with_timeout(timeout, "wait deployment finished")
416 wto(all_tasks_finished_ok)(self)
417
418 def set_networks(self, net_descriptions):
419 """Update cluster networking parameters"""
420 configuration = self.get_networks()
421 current_networks = configuration['networks']
422 parameters = configuration['networking_parameters']
423
424 if net_descriptions.get('networks'):
425 net_mapping = net_descriptions['networks']
426
427 for net in current_networks:
428 net_desc = net_mapping.get(net['name'])
429 if net_desc:
430 net.update(net_desc)
431
432 if net_descriptions.get('networking_parameters'):
433 parameters.update(net_descriptions['networking_parameters'])
434
435 self.configure_networks(**configuration)
436
437
438def reflect_cluster(conn, cluster_id):
439 """Returns cluster object by id"""
440 c = Cluster(conn, id=cluster_id)
441 c.nodes = NodeList(list(c.get_nodes()))
442 return c
443
444
445def get_all_nodes(conn):
446 """Get all nodes from Fuel"""
447 for node_desc in conn.get('api/nodes'):
448 yield Node(conn, **node_desc)
449
450
451def get_all_clusters(conn):
452 """Get all clusters"""
453 for cluster_desc in conn.get('api/clusters'):
454 yield Cluster(conn, **cluster_desc)
455
456
koder aka kdanilovda45e882015-04-06 02:24:42 +0300457def get_cluster_id(conn, name):
Yulia Portnova3556a062015-03-17 16:30:11 +0200458 """Get cluster id by name"""
459 for cluster in get_all_clusters(conn):
460 if cluster.name == name:
Yulia Portnova3556a062015-03-17 16:30:11 +0200461 return cluster.id
462
koder aka kdanilovda45e882015-04-06 02:24:42 +0300463 raise ValueError("Cluster {0} not found".format(name))
464
Yulia Portnova3556a062015-03-17 16:30:11 +0200465
466sections = {
467 'sahara': 'additional_components',
468 'murano': 'additional_components',
469 'ceilometer': 'additional_components',
470 'volumes_ceph': 'storage',
471 'images_ceph': 'storage',
472 'ephemeral_ceph': 'storage',
473 'objects_ceph': 'storage',
474 'osd_pool_size': 'storage',
475 'volumes_lvm': 'storage',
476 'volumes_vmdk': 'storage',
477 'tenant': 'access',
478 'password': 'access',
479 'user': 'access',
480 'vc_password': 'vcenter',
481 'cluster': 'vcenter',
482 'host_ip': 'vcenter',
483 'vc_user': 'vcenter',
484 'use_vcenter': 'vcenter',
485}
486
487
488def create_empty_cluster(conn, cluster_desc, debug_mode=False):
489 """Create new cluster with configuration provided"""
490
491 data = {}
492 data['nodes'] = []
493 data['tasks'] = []
494 data['name'] = cluster_desc['name']
495 data['release'] = cluster_desc['release']
496 data['mode'] = cluster_desc.get('deployment_mode')
497 data['net_provider'] = cluster_desc.get('net_provider')
498
499 params = conn.post(path='/api/clusters', params=data)
500 cluster = Cluster(conn, **params)
501
502 attributes = cluster.get_attributes()
503
504 ed_attrs = attributes['editable']
505
506 ed_attrs['common']['libvirt_type']['value'] = \
507 cluster_desc.get('libvirt_type', 'kvm')
508
509 if 'nodes' in cluster_desc:
510 use_ceph = cluster_desc['nodes'].get('ceph_osd', None) is not None
511 else:
512 use_ceph = False
513
514 if 'storage_type' in cluster_desc:
515 st = cluster_desc['storage_type']
516 if st == 'ceph':
517 use_ceph = True
518 else:
519 use_ceph = False
520
521 if use_ceph:
522 opts = ['ephemeral_ceph', 'images_ceph', 'images_vcenter']
523 opts += ['iser', 'objects_ceph', 'volumes_ceph']
524 opts += ['volumes_lvm', 'volumes_vmdk']
525
526 for name in opts:
527 val = ed_attrs['storage'][name]
528 if val['type'] == 'checkbox':
529 is_ceph = ('images_ceph' == name)
530 is_ceph = is_ceph or ('volumes_ceph' == name)
531
532 if is_ceph:
533 val['value'] = True
534 else:
535 val['value'] = False
536 # else:
537 # raise NotImplementedError("Non-ceph storages are not implemented")
538
539 cluster.set_attributes(attributes)
540
541 return cluster