blob: 728ac946f9de0f5ad564bf1ebc3ec9a5c9121411 [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']
Michael Semenov8d6c0572015-08-25 12:59:05 +0300352
353 version = FuelInfo(self.__connection__).get_version()
koder aka kdanilov5ea9df02015-12-04 21:46:06 +0200354 # only HTTPS since 7.0
355 if version >= [7, 0]:
Michael Semenov8d6c0572015-08-25 12:59:05 +0300356 creds['insecure'] = "True"
357 creds['os_auth_url'] = "https://{0}:5000/v2.0".format(
358 self.get_networks()['public_vip'])
359 else:
360 creds['os_auth_url'] = "http://{0}:5000/v2.0".format(
361 self.get_networks()['public_vip'])
Yulia Portnova00025a52015-04-07 12:17:32 +0300362 return creds
363
Yulia Portnova3556a062015-03-17 16:30:11 +0200364 def get_nodes(self):
365 for node_descr in self._get_nodes():
366 yield Node(self.__connection__, **node_descr)
367
368 def add_node(self, node, roles, interfaces=None):
369 """Add node to cluster
370
371 :param node: Node object
372 :param roles: roles to assign
373 :param interfaces: mapping iface name to networks
374 """
375 data = {}
376 data['pending_roles'] = roles
377 data['cluster_id'] = self.id
378 data['id'] = node.id
379 data['pending_addition'] = True
380
koder aka kdanilovcee43342015-04-14 22:52:53 +0300381 logger.debug("Adding node %s to cluster..." % node.id)
Yulia Portnova3556a062015-03-17 16:30:11 +0200382
383 self.add_node_call([data])
384 self.nodes.append(node)
385
386 if interfaces is not None:
387 networks = {}
388 for iface_name, params in interfaces.items():
389 networks[iface_name] = params['networks']
390
391 node.set_network_assigment(networks)
392
393 def wait_operational(self, timeout):
394 """Wait until cluster status operational"""
395 def wo():
396 status = self.get_status()['status']
397 if status == "error":
398 raise Exception("Cluster deploy failed")
399 return self.get_status()['status'] == 'operational'
400 with_timeout(timeout, "deploy cluster")(wo)()
401
402 def deploy(self, timeout):
403 """Start deploy and wait until all tasks finished"""
404 logger.debug("Starting deploy...")
405 self.start_deploy()
406
407 self.wait_operational(timeout)
408
409 def all_tasks_finished_ok(obj):
410 ok = True
411 for task in obj.get_tasks_status():
412 if task['status'] == 'error':
413 raise Exception('Task execution error')
414 elif task['status'] != 'ready':
415 ok = False
416 return ok
417
418 wto = with_timeout(timeout, "wait deployment finished")
419 wto(all_tasks_finished_ok)(self)
420
421 def set_networks(self, net_descriptions):
422 """Update cluster networking parameters"""
423 configuration = self.get_networks()
424 current_networks = configuration['networks']
425 parameters = configuration['networking_parameters']
426
427 if net_descriptions.get('networks'):
428 net_mapping = net_descriptions['networks']
429
430 for net in current_networks:
431 net_desc = net_mapping.get(net['name'])
432 if net_desc:
433 net.update(net_desc)
434
435 if net_descriptions.get('networking_parameters'):
436 parameters.update(net_descriptions['networking_parameters'])
437
438 self.configure_networks(**configuration)
439
440
441def reflect_cluster(conn, cluster_id):
442 """Returns cluster object by id"""
443 c = Cluster(conn, id=cluster_id)
444 c.nodes = NodeList(list(c.get_nodes()))
445 return c
446
447
448def get_all_nodes(conn):
449 """Get all nodes from Fuel"""
450 for node_desc in conn.get('api/nodes'):
451 yield Node(conn, **node_desc)
452
453
454def get_all_clusters(conn):
455 """Get all clusters"""
456 for cluster_desc in conn.get('api/clusters'):
457 yield Cluster(conn, **cluster_desc)
458
459
koder aka kdanilovda45e882015-04-06 02:24:42 +0300460def get_cluster_id(conn, name):
Yulia Portnova3556a062015-03-17 16:30:11 +0200461 """Get cluster id by name"""
462 for cluster in get_all_clusters(conn):
463 if cluster.name == name:
Yulia Portnova3556a062015-03-17 16:30:11 +0200464 return cluster.id
465
koder aka kdanilovda45e882015-04-06 02:24:42 +0300466 raise ValueError("Cluster {0} not found".format(name))
467
Yulia Portnova3556a062015-03-17 16:30:11 +0200468
469sections = {
470 'sahara': 'additional_components',
471 'murano': 'additional_components',
472 'ceilometer': 'additional_components',
473 'volumes_ceph': 'storage',
474 'images_ceph': 'storage',
475 'ephemeral_ceph': 'storage',
476 'objects_ceph': 'storage',
477 'osd_pool_size': 'storage',
478 'volumes_lvm': 'storage',
479 'volumes_vmdk': 'storage',
480 'tenant': 'access',
481 'password': 'access',
482 'user': 'access',
483 'vc_password': 'vcenter',
484 'cluster': 'vcenter',
485 'host_ip': 'vcenter',
486 'vc_user': 'vcenter',
487 'use_vcenter': 'vcenter',
488}
489
490
491def create_empty_cluster(conn, cluster_desc, debug_mode=False):
492 """Create new cluster with configuration provided"""
493
494 data = {}
495 data['nodes'] = []
496 data['tasks'] = []
497 data['name'] = cluster_desc['name']
498 data['release'] = cluster_desc['release']
499 data['mode'] = cluster_desc.get('deployment_mode')
500 data['net_provider'] = cluster_desc.get('net_provider')
501
502 params = conn.post(path='/api/clusters', params=data)
503 cluster = Cluster(conn, **params)
504
505 attributes = cluster.get_attributes()
506
507 ed_attrs = attributes['editable']
508
509 ed_attrs['common']['libvirt_type']['value'] = \
510 cluster_desc.get('libvirt_type', 'kvm')
511
512 if 'nodes' in cluster_desc:
513 use_ceph = cluster_desc['nodes'].get('ceph_osd', None) is not None
514 else:
515 use_ceph = False
516
517 if 'storage_type' in cluster_desc:
518 st = cluster_desc['storage_type']
519 if st == 'ceph':
520 use_ceph = True
521 else:
522 use_ceph = False
523
524 if use_ceph:
525 opts = ['ephemeral_ceph', 'images_ceph', 'images_vcenter']
526 opts += ['iser', 'objects_ceph', 'volumes_ceph']
527 opts += ['volumes_lvm', 'volumes_vmdk']
528
529 for name in opts:
530 val = ed_attrs['storage'][name]
531 if val['type'] == 'checkbox':
532 is_ceph = ('images_ceph' == name)
533 is_ceph = is_ceph or ('volumes_ceph' == name)
534
535 if is_ceph:
536 val['value'] = True
537 else:
538 val['value'] = False
539 # else:
540 # raise NotImplementedError("Non-ceph storages are not implemented")
541
542 cluster.set_attributes(attributes)
543
544 return cluster