blob: 23f72895f530378b159720ce5041227f70620b16 [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
6
7from functools import partial, wraps
8import urlparse
9
10import netaddr
11
12from keystoneclient.v2_0 import Client as keystoneclient
13from keystoneclient import exceptions
14
15
koder aka kdanilovcee43342015-04-14 22:52:53 +030016logger = logging.getLogger("io-perf-tool.fuel_api")
Yulia Portnova3556a062015-03-17 16:30:11 +020017
18
19class Urllib2HTTP(object):
20 """
21 class for making HTTP requests
22 """
23
24 allowed_methods = ('get', 'put', 'post', 'delete', 'patch', 'head')
25
koder aka kdanilovcee43342015-04-14 22:52:53 +030026 def __init__(self, root_url, headers=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
36 def host(self):
37 return self.root_url.split('/')[2]
38
39 def do(self, method, path, params=None):
40 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 kdanilovcee43342015-04-14 22:52:53 +030051 logger.debug("HTTP: {} {}".format(method.upper(), url))
Yulia Portnova3556a062015-03-17 16:30:11 +020052
53 request = urllib2.Request(url,
54 data=data_json,
55 headers=self.headers)
56 if data_json is not None:
57 request.add_header('Content-Type', 'application/json')
58
59 request.get_method = lambda: method.upper()
60 response = urllib2.urlopen(request)
61
koder aka kdanilovcee43342015-04-14 22:52:53 +030062 logger.debug("HTTP Responce: {}".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
74 def __getattr__(self, name):
75 if name in self.allowed_methods:
76 return partial(self.do, name)
77 raise AttributeError(name)
78
79
80class KeystoneAuth(Urllib2HTTP):
koder aka kdanilovcee43342015-04-14 22:52:53 +030081 def __init__(self, root_url, creds, headers=None):
82 super(KeystoneAuth, self).__init__(root_url, headers)
Yulia Portnova3556a062015-03-17 16:30:11 +020083 admin_node_ip = urlparse.urlparse(root_url).hostname
84 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
89 def refresh_token(self):
90 """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
99 def do(self, method, path, params=None):
100 """Do request. If gets 401 refresh token"""
101 try:
102 return super(KeystoneAuth, self).do(method, path, params)
103 except urllib2.HTTPError as e:
104 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
113def get_inline_param_list(url):
114 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
119class RestObj(object):
120 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):
128 res = ["{}({}):".format(self.__class__.__name__, self.name)]
129 for k, v in sorted(self.__dict__.items()):
130 if k.startswith('__') or k.endswith('__'):
131 continue
132 if k != 'name':
133 res.append(" {}={!r}".format(k, v))
134 return "\n".join(res)
135
136 def __getitem__(self, item):
137 return getattr(self, item)
138
139
140def make_call(method, url):
141 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}')
198
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
217
218class Node(RestObj):
219 """Represents node in Fuel"""
220
221 get_info = GET('/api/nodes/{id}')
222 get_interfaces = GET('/api/nodes/{id}/interfaces')
223 update_interfaces = PUT('/api/nodes/{id}/interfaces')
224
225 def set_network_assigment(self, mapping):
226 """Assings networks to interfaces
227 :param mapping: list (dict) interfaces info
228 """
229
230 curr_interfaces = self.get_interfaces()
231
232 network_ids = {}
233 for interface in curr_interfaces:
234 for net in interface['assigned_networks']:
235 network_ids[net['name']] = net['id']
236
237 # transform mappings
238 new_assigned_networks = {}
239
240 for dev_name, networks in mapping.items():
241 new_assigned_networks[dev_name] = []
242 for net_name in networks:
243 nnet = {'name': net_name, 'id': network_ids[net_name]}
244 new_assigned_networks[dev_name].append(nnet)
245
246 # update by ref
247 for dev_descr in curr_interfaces:
248 if dev_descr['name'] in new_assigned_networks:
249 nass = new_assigned_networks[dev_descr['name']]
250 dev_descr['assigned_networks'] = nass
251
252 self.update_interfaces(curr_interfaces, id=self.id)
253
254 def set_node_name(self, name):
255 """Update node name"""
256 self.__connection__.put('api/nodes', [{'id': self.id, 'name': name}])
257
258 def get_network_data(self):
259 """Returns node network data"""
260 node_info = self.get_info()
261 return node_info.get('network_data')
262
Yulia Portnova0e64ea22015-03-20 17:27:22 +0200263 def get_roles(self, pending=False):
Yulia Portnova3556a062015-03-17 16:30:11 +0200264 """Get node roles
265
266 Returns: (roles, pending_roles)
267 """
268 node_info = self.get_info()
Yulia Portnova0e64ea22015-03-20 17:27:22 +0200269 if pending:
270 return node_info.get('roles'), node_info.get('pending_roles')
271 else:
272 return node_info.get('roles')
Yulia Portnova3556a062015-03-17 16:30:11 +0200273
274 def get_ip(self, network='public'):
275 """Get node ip
276
277 :param network: network to pick
278 """
279 nets = self.get_network_data()
280 for net in nets:
281 if net['name'] == network:
282 iface_name = net['dev']
283 for iface in self.get_info()['meta']['interfaces']:
284 if iface['name'] == iface_name:
285 try:
286 return iface['ip']
287 except KeyError:
288 return netaddr.IPNetwork(net['ip']).ip
289 raise Exception('Network %s not found' % network)
290
291
292class NodeList(list):
293 """Class for filtering nodes through attributes"""
294 allowed_roles = ['controller', 'compute', 'cinder', 'ceph-osd', 'mongo',
295 'zabbix-server']
296
297 def __getattr__(self, name):
298 if name in self.allowed_roles:
299 return [node for node in self if name in node.roles]
300
301
302class Cluster(RestObj):
303 """Class represents Cluster in Fuel"""
304
305 add_node_call = PUT('api/nodes')
306 start_deploy = PUT('api/clusters/{id}/changes')
307 get_status = GET('api/clusters/{id}')
308 delete = DELETE('api/clusters/{id}')
309 get_tasks_status = GET("api/tasks?cluster_id={id}")
310 get_networks = GET(
311 'api/clusters/{id}/network_configuration/{net_provider}')
312
313 get_attributes = GET(
314 'api/clusters/{id}/attributes')
315
316 set_attributes = PUT(
317 'api/clusters/{id}/attributes')
318
319 configure_networks = PUT(
320 'api/clusters/{id}/network_configuration/{net_provider}')
321
322 _get_nodes = GET('api/nodes?cluster_id={id}')
323
324 def __init__(self, *dt, **mp):
325 super(Cluster, self).__init__(*dt, **mp)
Yulia Portnovad9767042015-04-10 17:32:06 +0300326 self.nodes = NodeList([Node(self.__connection__, **node) for node in
327 self._get_nodes()])
Yulia Portnova3556a062015-03-17 16:30:11 +0200328 self.network_roles = {}
329
330 def check_exists(self):
331 """Check if cluster exists"""
332 try:
333 self.get_status()
334 return True
335 except urllib2.HTTPError as err:
336 if err.code == 404:
337 return False
338 raise
339
Yulia Portnovad9767042015-04-10 17:32:06 +0300340 def get_openrc(self):
Yulia Portnova00025a52015-04-07 12:17:32 +0300341 access = self.get_attributes()['editable']['access']
342 creds = {}
343 creds['username'] = access['user']['value']
344 creds['password'] = access['password']['value']
345 creds['tenant_name'] = access['tenant']['value']
Yulia Portnovad9767042015-04-10 17:32:06 +0300346 if self.nodes.controller:
347 contr = self.nodes.controller[0]
348 creds['os_auth_url'] = "http://%s:5000/v2.0" \
349 % contr.get_ip(network="public")
350 else:
351 creds['os_auth_url'] = ""
Yulia Portnova00025a52015-04-07 12:17:32 +0300352 return creds
353
Yulia Portnova3556a062015-03-17 16:30:11 +0200354 def get_nodes(self):
355 for node_descr in self._get_nodes():
356 yield Node(self.__connection__, **node_descr)
357
358 def add_node(self, node, roles, interfaces=None):
359 """Add node to cluster
360
361 :param node: Node object
362 :param roles: roles to assign
363 :param interfaces: mapping iface name to networks
364 """
365 data = {}
366 data['pending_roles'] = roles
367 data['cluster_id'] = self.id
368 data['id'] = node.id
369 data['pending_addition'] = True
370
koder aka kdanilovcee43342015-04-14 22:52:53 +0300371 logger.debug("Adding node %s to cluster..." % node.id)
Yulia Portnova3556a062015-03-17 16:30:11 +0200372
373 self.add_node_call([data])
374 self.nodes.append(node)
375
376 if interfaces is not None:
377 networks = {}
378 for iface_name, params in interfaces.items():
379 networks[iface_name] = params['networks']
380
381 node.set_network_assigment(networks)
382
383 def wait_operational(self, timeout):
384 """Wait until cluster status operational"""
385 def wo():
386 status = self.get_status()['status']
387 if status == "error":
388 raise Exception("Cluster deploy failed")
389 return self.get_status()['status'] == 'operational'
390 with_timeout(timeout, "deploy cluster")(wo)()
391
392 def deploy(self, timeout):
393 """Start deploy and wait until all tasks finished"""
394 logger.debug("Starting deploy...")
395 self.start_deploy()
396
397 self.wait_operational(timeout)
398
399 def all_tasks_finished_ok(obj):
400 ok = True
401 for task in obj.get_tasks_status():
402 if task['status'] == 'error':
403 raise Exception('Task execution error')
404 elif task['status'] != 'ready':
405 ok = False
406 return ok
407
408 wto = with_timeout(timeout, "wait deployment finished")
409 wto(all_tasks_finished_ok)(self)
410
411 def set_networks(self, net_descriptions):
412 """Update cluster networking parameters"""
413 configuration = self.get_networks()
414 current_networks = configuration['networks']
415 parameters = configuration['networking_parameters']
416
417 if net_descriptions.get('networks'):
418 net_mapping = net_descriptions['networks']
419
420 for net in current_networks:
421 net_desc = net_mapping.get(net['name'])
422 if net_desc:
423 net.update(net_desc)
424
425 if net_descriptions.get('networking_parameters'):
426 parameters.update(net_descriptions['networking_parameters'])
427
428 self.configure_networks(**configuration)
429
430
431def reflect_cluster(conn, cluster_id):
432 """Returns cluster object by id"""
433 c = Cluster(conn, id=cluster_id)
434 c.nodes = NodeList(list(c.get_nodes()))
435 return c
436
437
438def get_all_nodes(conn):
439 """Get all nodes from Fuel"""
440 for node_desc in conn.get('api/nodes'):
441 yield Node(conn, **node_desc)
442
443
444def get_all_clusters(conn):
445 """Get all clusters"""
446 for cluster_desc in conn.get('api/clusters'):
447 yield Cluster(conn, **cluster_desc)
448
449
koder aka kdanilovda45e882015-04-06 02:24:42 +0300450def get_cluster_id(conn, name):
Yulia Portnova3556a062015-03-17 16:30:11 +0200451 """Get cluster id by name"""
452 for cluster in get_all_clusters(conn):
453 if cluster.name == name:
Yulia Portnova3556a062015-03-17 16:30:11 +0200454 return cluster.id
455
koder aka kdanilovda45e882015-04-06 02:24:42 +0300456 raise ValueError("Cluster {0} not found".format(name))
457
Yulia Portnova3556a062015-03-17 16:30:11 +0200458
459sections = {
460 'sahara': 'additional_components',
461 'murano': 'additional_components',
462 'ceilometer': 'additional_components',
463 'volumes_ceph': 'storage',
464 'images_ceph': 'storage',
465 'ephemeral_ceph': 'storage',
466 'objects_ceph': 'storage',
467 'osd_pool_size': 'storage',
468 'volumes_lvm': 'storage',
469 'volumes_vmdk': 'storage',
470 'tenant': 'access',
471 'password': 'access',
472 'user': 'access',
473 'vc_password': 'vcenter',
474 'cluster': 'vcenter',
475 'host_ip': 'vcenter',
476 'vc_user': 'vcenter',
477 'use_vcenter': 'vcenter',
478}
479
480
481def create_empty_cluster(conn, cluster_desc, debug_mode=False):
482 """Create new cluster with configuration provided"""
483
484 data = {}
485 data['nodes'] = []
486 data['tasks'] = []
487 data['name'] = cluster_desc['name']
488 data['release'] = cluster_desc['release']
489 data['mode'] = cluster_desc.get('deployment_mode')
490 data['net_provider'] = cluster_desc.get('net_provider')
491
492 params = conn.post(path='/api/clusters', params=data)
493 cluster = Cluster(conn, **params)
494
495 attributes = cluster.get_attributes()
496
497 ed_attrs = attributes['editable']
498
499 ed_attrs['common']['libvirt_type']['value'] = \
500 cluster_desc.get('libvirt_type', 'kvm')
501
502 if 'nodes' in cluster_desc:
503 use_ceph = cluster_desc['nodes'].get('ceph_osd', None) is not None
504 else:
505 use_ceph = False
506
507 if 'storage_type' in cluster_desc:
508 st = cluster_desc['storage_type']
509 if st == 'ceph':
510 use_ceph = True
511 else:
512 use_ceph = False
513
514 if use_ceph:
515 opts = ['ephemeral_ceph', 'images_ceph', 'images_vcenter']
516 opts += ['iser', 'objects_ceph', 'volumes_ceph']
517 opts += ['volumes_lvm', 'volumes_vmdk']
518
519 for name in opts:
520 val = ed_attrs['storage'][name]
521 if val['type'] == 'checkbox':
522 is_ceph = ('images_ceph' == name)
523 is_ceph = is_ceph or ('volumes_ceph' == name)
524
525 if is_ceph:
526 val['value'] = True
527 else:
528 val['value'] = False
529 # else:
530 # raise NotImplementedError("Non-ceph storages are not implemented")
531
532 cluster.set_attributes(attributes)
533
534 return cluster