e886bb2d5a8d867091f5af8d2ef83fbad9e56f89
[osm/SO.git] / rwcal / plugins / vala / rwcal_openstack / rift / rwcal / openstack / utils / compute.py
1 #!/usr/bin/python
2
3 #
4 # Copyright 2017 RIFT.IO Inc
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 # http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17 #
18 import uuid
19 import gi
20 gi.require_version('RwcalYang', '1.0')
21 from gi.repository import RwcalYang
22
23
24 class ImageValidateError(Exception):
25 pass
26
27 class VolumeValidateError(Exception):
28 pass
29
30 class AffinityGroupError(Exception):
31 pass
32
33
34 class ComputeUtils(object):
35 """
36 Utility class for compute operations
37 """
38 epa_types = ['vm_flavor',
39 'guest_epa',
40 'host_epa',
41 'host_aggregate',
42 'hypervisor_epa',
43 'vswitch_epa']
44 def __init__(self, driver):
45 """
46 Constructor for class
47 Arguments:
48 driver: object of OpenstackDriver()
49 """
50 self._driver = driver
51 self.log = driver.log
52
53 @property
54 def driver(self):
55 return self._driver
56
57 def search_vdu_flavor(self, vdu_params):
58 """
59 Function to search a matching flavor for VDU instantiation
60 from already existing flavors
61
62 Arguments:
63 vdu_params: Protobuf GI object RwcalYang.VDUInitParams()
64
65 Returns:
66 flavor_id(string): Flavor id for VDU instantiation
67 None if no flavor could be found
68 """
69 kwargs = { 'vcpus': vdu_params.vm_flavor.vcpu_count,
70 'ram' : vdu_params.vm_flavor.memory_mb,
71 'disk' : vdu_params.vm_flavor.storage_gb,}
72
73 flavors = self.driver.nova_flavor_find(**kwargs)
74 flavor_list = list()
75 for flv in flavors:
76 flavor_list.append(self.driver.utils.flavor.parse_flavor_info(flv))
77
78 flavor_id = self.driver.utils.flavor.match_resource_flavor(vdu_params, flavor_list)
79 return flavor_id
80
81 def select_vdu_flavor(self, vdu_params):
82 """
83 This function attempts to find a pre-existing flavor matching required
84 parameters for VDU instantiation. If no such flavor is found, a new one
85 is created.
86
87 Arguments:
88 vdu_params: Protobuf GI object RwcalYang.VDUInitParams()
89
90 Returns:
91 flavor_id(string): Flavor id for VDU instantiation
92 """
93 flavor_id = self.search_vdu_flavor(vdu_params)
94 if flavor_id is not None:
95 self.log.info("Found flavor with id: %s matching requirements for VDU: %s",
96 flavor_id, vdu_params.name)
97 return flavor_id
98
99 flavor = RwcalYang.FlavorInfoItem()
100 flavor.name = str(uuid.uuid4())
101
102 epa_dict = { k: v for k, v in vdu_params.as_dict().items()
103 if k in ComputeUtils.epa_types }
104
105 flavor.from_dict(epa_dict)
106
107 flavor_id = self.driver.nova_flavor_create(name = flavor.name,
108 ram = flavor.vm_flavor.memory_mb,
109 vcpus = flavor.vm_flavor.vcpu_count,
110 disk = flavor.vm_flavor.storage_gb,
111 epa_specs = self.driver.utils.flavor.get_extra_specs(flavor))
112 return flavor_id
113
114 def make_vdu_flavor_args(self, vdu_params):
115 """
116 Creates flavor related arguments for VDU operation
117 Arguments:
118 vdu_params: Protobuf GI object RwcalYang.VDUInitParams()
119
120 Returns:
121 A dictionary {'flavor_id': <flavor-id>}
122 """
123 return {'flavor_id': self.select_vdu_flavor(vdu_params)}
124
125
126 def make_vdu_image_args(self, vdu_params):
127 """
128 Creates image related arguments for VDU operation
129 Arguments:
130 vdu_params: Protobuf GI object RwcalYang.VDUInitParams()
131
132 Returns:
133 A dictionary {'image_id': <image-id>}
134
135 """
136 kwargs = dict()
137 if vdu_params.has_field('image_name'):
138 kwargs['image_id'] = self.resolve_image_n_validate(vdu_params.image_name,
139 vdu_params.image_checksum)
140 elif vdu_params.has_field('image_id'):
141 kwargs['image_id'] = vdu_params.image_id
142
143 return kwargs
144
145 def resolve_image_n_validate(self, image_name, checksum = None):
146 """
147 Resolve the image_name to image-object by matching image_name and checksum
148
149 Arguments:
150 image_name (string): Name of image
151 checksums (string): Checksum associated with image
152
153 Raises ImageValidateError in case of Errors
154 """
155 image_info = [ i for i in self.driver._glance_image_list if i['name'] == image_name]
156
157 if not image_info:
158 self.log.error("No image with name: %s found", image_name)
159 raise ImageValidateError("No image with name %s found" %(image_name))
160
161 for image in image_info:
162 if 'status' not in image or image['status'] != 'active':
163 self.log.error("Image %s not in active state. Current state: %s",
164 image_name, image['status'])
165 raise ImageValidateError("Image with name %s found in incorrect (%s) state"
166 %(image_name, image['status']))
167 if not checksum or checksum == image['checksum']:
168 break
169 else:
170 self.log.info("No image found with matching name: %s and checksum: %s",
171 image_name, checksum)
172 raise ImageValidateError("No image found with matching name: %s and checksum: %s"
173 %(image_name, checksum))
174 return image['id']
175
176 def resolve_volume_n_validate(self, volume_ref):
177 """
178 Resolve the volume reference
179
180 Arguments:
181 volume_ref (string): Name of volume reference
182
183 Raises VolumeValidateError in case of Errors
184 """
185
186 for vol in self.driver._cinder_volume_list:
187 voldict = vol.to_dict()
188 if 'display_name' in voldict and voldict['display_name'] == volume_ref:
189 if 'status' in voldict:
190 if voldict['status'] == 'available':
191 return voldict['id']
192 else:
193 self.log.error("Volume %s not in available state. Current state: %s",
194 volume_ref, voldict['status'])
195 raise VolumeValidateError("Volume with name %s found in incorrect (%s) state"
196 %(volume_ref, voldict['status']))
197
198 self.log.info("No volume found with matching name: %s ", volume_ref)
199 raise VolumeValidateError("No volume found with matching name: %s " %(volume_ref))
200
201 def make_vdu_volume_args(self, volume, vdu_params):
202 """
203 Arguments:
204 volume: Protobuf GI object RwcalYang.VDUInitParams_Volumes()
205 vdu_params: Protobuf GI object RwcalYang.VDUInitParams()
206
207 Returns:
208 A dictionary required to create volume for VDU
209
210 Raises VolumeValidateError in case of Errors
211 """
212 kwargs = dict()
213
214 if 'boot_priority' in volume:
215 # Rift-only field
216 kwargs['boot_index'] = volume.boot_priority
217 if volume.has_field("image"):
218 # Support image->volume
219 kwargs['source_type'] = "image"
220 kwargs['uuid'] = self.resolve_image_n_validate(volume.image, volume.image_checksum)
221 kwargs['delete_on_termination'] = True
222 elif "volume_ref" in volume:
223 # Support volume-ref->volume (only ref)
224 # Rift-only field
225 kwargs['source_type'] = "volume"
226 kwargs['uuid'] = self.resolve_volume_n_validate(volume.volume_ref)
227 kwargs['delete_on_termination'] = False
228 else:
229 # Support blank->volume
230 kwargs['source_type'] = "blank"
231 kwargs['delete_on_termination'] = True
232 kwargs['device_name'] = volume.name
233 kwargs['destination_type'] = "volume"
234 kwargs['volume_size'] = volume.size
235
236 if volume.has_field('device_type'):
237 if volume.device_type in ['cdrom', 'disk']:
238 kwargs['device_type'] = volume.device_type
239 else:
240 self.log.error("Unsupported device_type <%s> found for volume: %s",
241 volume.device_type, volume.name)
242 raise VolumeValidateError("Unsupported device_type <%s> found for volume: %s"
243 %(volume.device_type, volume.name))
244
245 if volume.has_field('device_bus'):
246 if volume.device_bus in ['ide', 'virtio', 'scsi']:
247 kwargs['disk_bus'] = volume.device_bus
248 else:
249 self.log.error("Unsupported device_type <%s> found for volume: %s",
250 volume.device_type, volume.name)
251 raise VolumeValidateError("Unsupported device_type <%s> found for volume: %s"
252 %(volume.device_type, volume.name))
253
254 return kwargs
255
256 def make_vdu_storage_args(self, vdu_params):
257 """
258 Creates volume related arguments for VDU operation
259
260 Arguments:
261 vdu_params: Protobuf GI object RwcalYang.VDUInitParams()
262
263 Returns:
264 A dictionary required for volumes creation for VDU instantiation
265 """
266 kwargs = dict()
267 if vdu_params.has_field('volumes'):
268 kwargs['block_device_mapping_v2'] = list()
269 bootvol_list = list()
270 othervol_list = list()
271 # Ignore top-level image
272 kwargs['image_id'] = ""
273 for volume in vdu_params.volumes:
274 if 'boot_priority' in volume:
275 bootvol_list.append(self.make_vdu_volume_args(volume, vdu_params))
276 else:
277 othervol_list.append(self.make_vdu_volume_args(volume, vdu_params))
278 # Sort block_device_mapping_v2 list by boot index, Openstack does not seem to respecting order by boot index
279 kwargs['block_device_mapping_v2'] = sorted(bootvol_list, key=lambda k: k['boot_index']) + othervol_list
280 return kwargs
281
282 def make_vdu_network_args(self, vdu_params):
283 """
284 Creates VDU network related arguments for VDU operation
285 Arguments:
286 vdu_params: Protobuf GI object RwcalYang.VDUInitParams()
287
288 Returns:
289 A dictionary {'port_list' : [ports], 'network_list': [networks]}
290
291 """
292 kwargs = dict()
293 kwargs['port_list'], kwargs['network_list'] = self.driver.utils.network.setup_vdu_networking(vdu_params)
294 return kwargs
295
296
297 def make_vdu_boot_config_args(self, vdu_params):
298 """
299 Creates VDU boot config related arguments for VDU operation
300 Arguments:
301 vdu_params: Protobuf GI object RwcalYang.VDUInitParams()
302
303 Returns:
304 A dictionary {
305 'userdata' : <cloud-init> ,
306 'config_drive': True/False,
307 'files' : [ file name ],
308 'metadata' : <metadata string>
309 }
310 """
311 kwargs = dict()
312 metadata = dict()
313
314 if vdu_params.has_field('node_id'):
315 metadata['rift_node_id'] = vdu_params.node_id
316 kwargs['metadata'] = metadata
317
318 if vdu_params.has_field('vdu_init') and vdu_params.vdu_init.has_field('userdata'):
319 kwargs['userdata'] = vdu_params.vdu_init.userdata
320 else:
321 kwargs['userdata'] = ''
322
323 if not vdu_params.has_field('supplemental_boot_data'):
324 return kwargs
325
326 if vdu_params.supplemental_boot_data.has_field('config_file'):
327 files = dict()
328 for cf in vdu_params.supplemental_boot_data.config_file:
329 files[cf.dest] = cf.source
330 kwargs['files'] = files
331
332 if vdu_params.supplemental_boot_data.has_field('boot_data_drive'):
333 kwargs['config_drive'] = vdu_params.supplemental_boot_data.boot_data_drive
334 else:
335 kwargs['config_drive'] = False
336
337 try:
338 # Rift model only
339 if vdu_params.supplemental_boot_data.has_field('custom_meta_data'):
340 for cm in vdu_params.supplemental_boot_data.custom_meta_data:
341 metadata[cm.name] = cm.value
342 kwargs['metadata'] = metadata
343 except Exception as e:
344 pass
345
346 return kwargs
347
348 def _select_affinity_group(self, group_name):
349 """
350 Selects the affinity group based on name and return its id
351 Arguments:
352 group_name (string): Name of the Affinity/Anti-Affinity group
353 Returns:
354 Id of the matching group
355
356 Raises exception AffinityGroupError if no matching group is found
357 """
358 groups = [g['id'] for g in self.driver._nova_affinity_group if g['name'] == group_name]
359 if not groups:
360 self.log.error("No affinity/anti-affinity group with name: %s found", group_name)
361 raise AffinityGroupError("No affinity/anti-affinity group with name: %s found" %(group_name))
362 return groups[0]
363
364
365 def make_vdu_server_placement_args(self, vdu_params):
366 """
367 Function to create kwargs required for nova server placement
368
369 Arguments:
370 vdu_params: Protobuf GI object RwcalYang.VDUInitParams()
371
372 Returns:
373 A dictionary { 'availability_zone' : < Zone >, 'scheduler_hints': <group-id> }
374
375 """
376 kwargs = dict()
377
378 if vdu_params.has_field('availability_zone') \
379 and vdu_params.availability_zone.has_field('name'):
380 kwargs['availability_zone'] = vdu_params.availability_zone
381
382 if vdu_params.has_field('server_group'):
383 kwargs['scheduler_hints'] = {
384 'group': self._select_affinity_group(vdu_params.server_group)
385 }
386 return kwargs
387
388 def make_vdu_server_security_args(self, vdu_params, account):
389 """
390 Function to create kwargs required for nova security group
391
392 Arguments:
393 vdu_params: Protobuf GI object RwcalYang.VDUInitParams()
394 account: Protobuf GI object RwcalYang.CloudAccount()
395
396 Returns:
397 A dictionary {'security_groups' : < group > }
398 """
399 kwargs = dict()
400 if account.openstack.security_groups:
401 kwargs['security_groups'] = account.openstack.security_groups
402 return kwargs
403
404
405 def make_vdu_create_args(self, vdu_params, account):
406 """
407 Function to create kwargs required for nova_server_create API
408
409 Arguments:
410 vdu_params: Protobuf GI object RwcalYang.VDUInitParams()
411 account: Protobuf GI object RwcalYang.CloudAccount()
412
413 Returns:
414 A kwargs dictionary for VDU create operation
415 """
416 kwargs = dict()
417
418 kwargs['name'] = vdu_params.name
419
420 kwargs.update(self.make_vdu_flavor_args(vdu_params))
421 kwargs.update(self.make_vdu_storage_args(vdu_params))
422 kwargs.update(self.make_vdu_image_args(vdu_params))
423 kwargs.update(self.make_vdu_network_args(vdu_params))
424 kwargs.update(self.make_vdu_boot_config_args(vdu_params))
425 kwargs.update(self.make_vdu_server_placement_args(vdu_params))
426 kwargs.update(self.make_vdu_server_security_args(vdu_params, account))
427 return kwargs
428
429
430 def _parse_vdu_mgmt_address_info(self, vm_info):
431 """
432 Get management_ip and public_ip for VDU
433
434 Arguments:
435 vm_info : A dictionary object return by novaclient library listing VM attributes
436
437 Returns:
438 A tuple of mgmt_ip (string) and public_ip (string)
439 """
440 mgmt_ip = None
441 public_ip = None
442 if 'addresses' in vm_info:
443 for network_name, network_info in vm_info['addresses'].items():
444 if network_info and network_name == self.driver.mgmt_network:
445 for interface in network_info:
446 if 'OS-EXT-IPS:type' in interface:
447 if interface['OS-EXT-IPS:type'] == 'fixed':
448 mgmt_ip = interface['addr']
449 elif interface['OS-EXT-IPS:type'] == 'floating':
450 public_ip = interface['addr']
451 return (mgmt_ip, public_ip)
452
453 def get_vdu_epa_info(self, vm_info):
454 """
455 Get flavor information (including EPA) for VDU
456
457 Arguments:
458 vm_info : A dictionary returned by novaclient library listing VM attributes
459 Returns:
460 flavor_info: A dictionary object returned by novaclient library listing flavor attributes
461 """
462 if 'flavor' in vm_info and 'id' in vm_info['flavor']:
463 try:
464 flavor_info = self.driver.nova_flavor_get(vm_info['flavor']['id'])
465 return flavor_info
466 except Exception as e:
467 self.log.exception("Exception %s occured during get-flavor", str(e))
468 return dict()
469
470 def _parse_vdu_cp_info(self, vdu_id):
471 """
472 Get connection point information for VDU identified by vdu_id
473 Arguments:
474 vdu_id (string) : VDU Id (vm_info['id'])
475 Returns:
476 A List of object RwcalYang.VDUInfoParams_ConnectionPoints()
477
478 """
479 cp_list = []
480 # Fill the port information
481 port_list = self.driver.neutron_port_list(**{'device_id': vdu_id})
482 for port in port_list:
483 cp_info = self.driver.utils.network._parse_cp(port)
484 cp = RwcalYang.VDUInfoParams_ConnectionPoints()
485 cp.from_dict(cp_info.as_dict())
486 cp_list.append(cp)
487 return cp_list
488
489 def _parse_vdu_state_info(self, vm_info):
490 """
491 Get VDU state information
492
493 Arguments:
494 vm_info : A dictionary returned by novaclient library listing VM attributes
495
496 Returns:
497 state (string): State of the VDU
498 """
499 if 'status' in vm_info:
500 if vm_info['status'] == 'ACTIVE':
501 vdu_state = 'active'
502 elif vm_info['status'] == 'ERROR':
503 vdu_state = 'failed'
504 else:
505 vdu_state = 'inactive'
506 else:
507 vdu_state = 'unknown'
508 return vdu_state
509
510 def _parse_vdu_server_group_info(self, vm_info):
511 """
512 Get VDU server group information
513 Arguments:
514 vm_info : A dictionary returned by novaclient library listing VM attributes
515
516 Returns:
517 server_group_name (string): Name of the server group to which VM belongs, else empty string
518
519 """
520 server_group = [ v['name']
521 for v in self.driver.nova_server_group_list()
522 if vm_info['id'] in v['members'] ]
523 if server_group:
524 return server_group[0]
525 else:
526 return str()
527
528 def _parse_vdu_boot_config_data(self, vm_info):
529 """
530 Parses VDU supplemental boot data
531 Arguments:
532 vm_info : A dictionary returned by novaclient library listing VM attributes
533
534 Returns:
535 List of RwcalYang.VDUInfoParams_SupplementalBootData()
536 """
537 supplemental_boot_data = None
538 node_id = None
539 if 'config_drive' in vm_info:
540 supplemental_boot_data = RwcalYang.VDUInfoParams_SupplementalBootData()
541 supplemental_boot_data.boot_data_drive = vm_info['config_drive']
542 # Look for any metadata
543 if 'metadata' not in vm_info:
544 return node_id, supplemental_boot_data
545 if supplemental_boot_data is None:
546 supplemental_boot_data = RwcalYang.VDUInfoParams_SupplementalBootData()
547 for key, value in vm_info['metadata'].items():
548 if key == 'rift_node_id':
549 node_id = value
550 else:
551 try:
552 # rift only
553 cm = supplemental_boot_data.custom_meta_data.add()
554 cm.name = key
555 cm.value = str(value)
556 except Exception as e:
557 pass
558 return node_id, supplemental_boot_data
559
560 def _parse_vdu_volume_info(self, vm_info):
561 """
562 Get VDU server group information
563 Arguments:
564 vm_info : A dictionary returned by novaclient library listing VM attributes
565
566 Returns:
567 List of RwcalYang.VDUInfoParams_Volumes()
568 """
569 volumes = list()
570
571 try:
572 volume_list = self.driver.nova_volume_list(vm_info['id'])
573 except Exception as e:
574 self.log.exception("Exception %s occured during nova-volume-list", str(e))
575 return volumes
576
577 for v in volume_list:
578 volume = RwcalYang.VDUInfoParams_Volumes()
579 try:
580 volume.name = (v['device']).split('/')[2]
581 volume.volume_id = v['volumeId']
582 details = self.driver.cinder_volume_get(volume.volume_id)
583 try:
584 # Rift only
585 for k, v in details.metadata.items():
586 vd = volume.custom_meta_data.add()
587 vd.name = k
588 vd.value = v
589 except Exception as e:
590 pass
591 except Exception as e:
592 self.log.exception("Exception %s occured during volume list parsing", str(e))
593 continue
594 else:
595 volumes.append(volume)
596 return volumes
597
598 def _parse_vdu_console_url(self, vm_info):
599 """
600 Get VDU console URL
601 Arguments:
602 vm_info : A dictionary returned by novaclient library listing VM attributes
603
604 Returns:
605 console_url(string): Console URL for VM
606 """
607 console_url = None
608 if self._parse_vdu_state_info(vm_info) == 'active':
609 try:
610 serv_console_url = self.driver.nova_server_console(vm_info['id'])
611 if 'console' in serv_console_url:
612 console_url = serv_console_url['console']['url']
613 else:
614 self.log.error("Error fetching console url. This could be an Openstack issue. Console : " + str(serv_console_url))
615
616
617 except Exception as e:
618 self.log.exception("Exception %s occured during volume list parsing", str(e))
619 return console_url
620
621 def parse_cloud_vdu_info(self, vm_info):
622 """
623 Parse vm_info dictionary (return by python-client) and put values in GI object for VDU
624
625 Arguments:
626 vm_info : A dictionary object return by novaclient library listing VM attributes
627
628 Returns:
629 Protobuf GI Object of type RwcalYang.VDUInfoParams()
630 """
631 vdu = RwcalYang.VDUInfoParams()
632 vdu.name = vm_info['name']
633 vdu.vdu_id = vm_info['id']
634 vdu.cloud_type = 'openstack'
635
636 if 'image' in vm_info and 'id' in vm_info['image']:
637 vdu.image_id = vm_info['image']['id']
638
639 if 'availability_zone' in vm_info:
640 vdu.availability_zone = vm_info['availability_zone']
641
642 vdu.state = self._parse_vdu_state_info(vm_info)
643 management_ip,public_ip = self._parse_vdu_mgmt_address_info(vm_info)
644
645 if management_ip:
646 vdu.management_ip = management_ip
647
648 if public_ip:
649 vdu.public_ip = public_ip
650
651 if 'flavor' in vm_info and 'id' in vm_info['flavor']:
652 vdu.flavor_id = vm_info['flavor']['id']
653 flavor_info = self.get_vdu_epa_info(vm_info)
654 vm_flavor = self.driver.utils.flavor.parse_vm_flavor_epa_info(flavor_info)
655 guest_epa = self.driver.utils.flavor.parse_guest_epa_info(flavor_info)
656 host_epa = self.driver.utils.flavor.parse_host_epa_info(flavor_info)
657 host_aggregates = self.driver.utils.flavor.parse_host_aggregate_epa_info(flavor_info)
658
659 vdu.vm_flavor.from_dict(vm_flavor.as_dict())
660 vdu.guest_epa.from_dict(guest_epa.as_dict())
661 vdu.host_epa.from_dict(host_epa.as_dict())
662 for aggr in host_aggregates:
663 ha = vdu.host_aggregate.add()
664 ha.from_dict(aggr.as_dict())
665
666 node_id, boot_data = self._parse_vdu_boot_config_data(vm_info)
667 if node_id:
668 vdu.node_id = node_id
669 if boot_data:
670 vdu.supplemental_boot_data = boot_data
671
672 cp_list = self._parse_vdu_cp_info(vdu.vdu_id)
673 for cp in cp_list:
674 vdu.connection_points.append(cp)
675
676 vdu.server_group.name = self._parse_vdu_server_group_info(vm_info)
677
678 for v in self._parse_vdu_volume_info(vm_info):
679 vdu.volumes.append(v)
680
681 vdu.console_url = self._parse_vdu_console_url(vm_info)
682 return vdu
683
684
685 def perform_vdu_network_cleanup(self, vdu_id):
686 """
687 This function cleans up networking resources related to VDU
688 Arguments:
689 vdu_id(string): VDU id
690 Returns:
691 None
692 """
693 ### Get list of floating_ips associated with this instance and delete them
694 floating_ips = [ f for f in self.driver.nova_floating_ip_list() if f.instance_id == vdu_id ]
695 for f in floating_ips:
696 self.driver.nova_floating_ip_delete(f)
697
698 ### Get list of port on VM and delete them.
699 port_list = self.driver.neutron_port_list(**{'device_id': vdu_id})
700
701 for port in port_list:
702 if ((port['device_owner'] == 'compute:None') or (port['device_owner'] == '')):
703 self.driver.neutron_port_delete(port['id'])
704