1 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
2 # and others. All rights reserved.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
21 from cryptography.hazmat.backends import default_backend
22 from cryptography.hazmat.primitives import serialization
23 from cryptography.hazmat.primitives.asymmetric import rsa
24 from novaclient.client import Client
25 from novaclient.exceptions import NotFound, ClientException
27 from snaps import file_utils
28 from snaps.domain.flavor import Flavor
29 from snaps.domain.keypair import Keypair
30 from snaps.domain.project import ComputeQuotas
31 from snaps.domain.vm_inst import VmInst
32 from snaps.openstack.utils import keystone_utils, glance_utils, neutron_utils
34 __author__ = 'spisarski'
36 logger = logging.getLogger('nova_utils')
41 Utilities for basic OpenStack Nova API calls
45 def nova_client(os_creds):
47 Instantiates and returns a client for communications with OpenStack's Nova
49 :param os_creds: The connection credentials to the OpenStack API
50 :return: the client object
52 logger.debug('Retrieving Nova Client')
53 return Client(os_creds.compute_api_version,
54 session=keystone_utils.keystone_session(os_creds),
55 region_name=os_creds.region_name)
58 def create_server(nova, neutron, glance, instance_config, image_config,
62 :param nova: the nova client (required)
63 :param neutron: the neutron client for retrieving ports (required)
64 :param glance: the glance client (required)
65 :param instance_config: the VMInstConfig object (required)
66 :param image_config: the VM's ImageConfig object (required)
67 :param keypair_config: the VM's KeypairConfig object (optional)
68 :return: a snaps.domain.VmInst object
73 for port_setting in instance_config.port_settings:
74 port = neutron_utils.get_port(neutron, port_settings=port_setting)
78 raise Exception('Cannot find port named - ' + port_setting.name)
82 kv['port-id'] = port.id
85 logger.info('Creating VM with name - ' + instance_config.name)
88 keypair_name = keypair_config.name
90 flavor = get_flavor_by_name(nova, instance_config.flavor)
93 'Flavor not found with name - %s', instance_config.flavor)
95 image = glance_utils.get_image(glance, image_settings=image_config)
98 if instance_config.userdata:
99 if isinstance(instance_config.userdata, str):
100 userdata = instance_config.userdata + '\n'
101 elif (isinstance(instance_config.userdata, dict) and
102 'script_file' in instance_config.userdata):
104 userdata = file_utils.read_file(
105 instance_config.userdata['script_file'])
106 except Exception as e:
107 logger.warn('error reading userdata file %s - %s',
108 instance_config.userdata, e)
109 args = {'name': instance_config.name,
113 'key_name': keypair_name,
115 instance_config.security_group_names,
116 'userdata': userdata}
118 if instance_config.availability_zone:
119 args['availability_zone'] = instance_config.availability_zone
121 server = nova.servers.create(**args)
123 return __map_os_server_obj_to_vm_inst(neutron, server)
126 'Cannot create instance, image cannot be located with name %s',
130 def get_server(nova, neutron, vm_inst_settings=None, server_name=None):
132 Returns a VmInst object for the first server instance found.
133 :param nova: the Nova client
134 :param neutron: the Neutron client
135 :param vm_inst_settings: the VmInstanceConfig object from which to build
136 the query if not None
137 :param server_name: the server with this name to return if vm_inst_settings
139 :return: a snaps.domain.VmInst object or None if not found
143 search_opts['name'] = vm_inst_settings.name
145 search_opts['name'] = server_name
147 servers = nova.servers.list(search_opts=search_opts)
148 for server in servers:
149 return __map_os_server_obj_to_vm_inst(neutron, server)
152 def get_server_connection(nova, vm_inst_settings=None, server_name=None):
154 Returns a VmInst object for the first server instance found.
155 :param nova: the Nova client
156 :param vm_inst_settings: the VmInstanceConfig object from which to build
157 the query if not None
158 :param server_name: the server with this name to return if vm_inst_settings
160 :return: a snaps.domain.VmInst object or None if not found
164 search_opts['name'] = vm_inst_settings.name
166 search_opts['name'] = server_name
168 servers = nova.servers.list(search_opts=search_opts)
169 for server in servers:
170 return server.links[0]
173 def __map_os_server_obj_to_vm_inst(neutron, os_server):
175 Returns a VmInst object for an OpenStack Server object
176 :param neutron: the Neutron client (when None, ports will be empty)
177 :param os_server: the OpenStack server object
178 :return: an equivalent SNAPS-OO VmInst domain object
180 sec_grp_names = list()
181 # VM must be active for 'security_groups' attr to be initialized
182 if hasattr(os_server, 'security_groups'):
183 for sec_group in os_server.security_groups:
184 if sec_group.get('name'):
185 sec_grp_names.append(sec_group.get('name'))
188 if len(os_server.networks) > 0:
189 for net_name, ips in os_server.networks.items():
190 network = neutron_utils.get_network(neutron, network_name=net_name)
191 ports = neutron_utils.get_ports(neutron, network, ips)
193 out_ports.append(port)
196 if hasattr(os_server, 'os-extended-volumes:volumes_attached'):
197 volumes = getattr(os_server, 'os-extended-volumes:volumes_attached')
200 name=os_server.name, inst_id=os_server.id,
201 image_id=os_server.image['id'], flavor_id=os_server.flavor['id'],
202 ports=out_ports, keypair_name=os_server.key_name,
203 sec_grp_names=sec_grp_names, volume_ids=volumes)
206 def __get_latest_server_os_object(nova, server):
208 Returns a server with a given id
209 :param nova: the Nova client
210 :param server: the domain VmInst object
211 :return: the list of servers or None if not found
213 return __get_latest_server_os_object_by_id(nova, server.id)
216 def __get_latest_server_os_object_by_id(nova, server_id):
218 Returns a server with a given id
219 :param nova: the Nova client
220 :param server_id: the server's ID
221 :return: the list of servers or None if not found
223 return nova.servers.get(server_id)
226 def get_server_status(nova, server):
228 Returns the a VM instance's status from OpenStack
229 :param nova: the Nova client
230 :param server: the domain VmInst object
231 :return: the VM's string status or None if not founc
233 server = __get_latest_server_os_object(nova, server)
239 def get_server_console_output(nova, server):
241 Returns the console object for parsing VM activity
242 :param nova: the Nova client
243 :param server: the domain VmInst object
244 :return: the console output object or None if server object is not found
246 server = __get_latest_server_os_object(nova, server)
248 return server.get_console_output()
252 def get_latest_server_object(nova, neutron, server):
254 Returns a server with a given id
255 :param nova: the Nova client
256 :param neutron: the Neutron client
257 :param server: the old server object
258 :return: the list of servers or None if not found
260 server = __get_latest_server_os_object(nova, server)
261 return __map_os_server_obj_to_vm_inst(neutron, server)
264 def get_server_object_by_id(nova, neutron, server_id):
266 Returns a server with a given id
267 :param nova: the Nova client
268 :param neutron: the Neutron client
269 :param server_id: the server's id
270 :return: an SNAPS-OO VmInst object or None if not found
272 server = __get_latest_server_os_object_by_id(nova, server_id)
273 return __map_os_server_obj_to_vm_inst(neutron, server)
276 def get_server_security_group_names(nova, server):
278 Returns a server with a given id
279 :param nova: the Nova client
280 :param server: the old server object
281 :return: the list of security groups associated with a VM
284 os_vm_inst = __get_latest_server_os_object(nova, server)
285 for sec_grp_dict in os_vm_inst.security_groups:
286 out.append(sec_grp_dict['name'])
290 def get_server_info(nova, server):
292 Returns a dictionary of a VMs info as returned by OpenStack
293 :param nova: the Nova client
294 :param server: the old server object
295 :return: a dict of the info if VM exists else None
297 vm = __get_latest_server_os_object(nova, server)
303 def reboot_server(nova, server, reboot_type=None):
305 Returns a dictionary of a VMs info as returned by OpenStack
306 :param nova: the Nova client
307 :param server: the old server object
308 :param reboot_type: Acceptable values 'SOFT', 'HARD'
309 (api uses SOFT as the default)
310 :return: a dict of the info if VM exists else None
312 vm = __get_latest_server_os_object(nova, server)
314 vm.reboot(reboot_type=reboot_type.value)
316 raise ServerNotFoundError('Cannot locate server')
319 def create_keys(key_size=2048):
321 Generates public and private keys
322 :param key_size: the number of bytes for the key size
323 :return: the cryptography keys
325 return rsa.generate_private_key(backend=default_backend(),
326 public_exponent=65537,
330 def public_key_openssh(keys):
332 Returns the public key for OpenSSH
333 :param keys: the keys generated by create_keys() from cryptography
334 :return: the OpenSSH public key
336 return keys.public_key().public_bytes(serialization.Encoding.OpenSSH,
337 serialization.PublicFormat.OpenSSH)
340 def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None):
342 Saves the generated RSA generated keys to the filesystem
343 :param keys: the keys to save generated by cryptography
344 :param pub_file_path: the path to the public keys
345 :param priv_file_path: the path to the private keys
350 pub_expand_file = os.path.expanduser(pub_file_path)
351 pub_dir = os.path.dirname(pub_expand_file)
353 if not os.path.isdir(pub_dir):
358 public_handle = open(pub_expand_file, 'wb')
359 public_bytes = keys.public_key().public_bytes(
360 serialization.Encoding.OpenSSH,
361 serialization.PublicFormat.OpenSSH)
362 public_handle.write(public_bytes)
365 public_handle.close()
367 os.chmod(pub_expand_file, 0o600)
368 logger.info("Saved public key to - " + pub_expand_file)
371 priv_expand_file = os.path.expanduser(priv_file_path)
372 priv_dir = os.path.dirname(priv_expand_file)
373 if not os.path.isdir(priv_dir):
376 private_handle = None
378 private_handle = open(priv_expand_file, 'wb')
379 private_handle.write(
381 encoding=serialization.Encoding.PEM,
382 format=serialization.PrivateFormat.TraditionalOpenSSL,
383 encryption_algorithm=serialization.NoEncryption()))
386 private_handle.close()
388 os.chmod(priv_expand_file, 0o600)
389 logger.info("Saved private key to - " + priv_expand_file)
392 def upload_keypair_file(nova, name, file_path):
394 Uploads a public key from a file
395 :param nova: the Nova client
396 :param name: the keypair name
397 :param file_path: the path to the public key file
398 :return: the keypair object
402 with open(os.path.expanduser(file_path), 'rb') as fpubkey:
403 logger.info('Saving keypair to - ' + file_path)
404 return upload_keypair(nova, name, fpubkey.read())
410 def upload_keypair(nova, name, key):
412 Uploads a public key from a file
413 :param nova: the Nova client
414 :param name: the keypair name
415 :param key: the public key object
416 :return: the keypair object
418 logger.info('Creating keypair with name - ' + name)
419 os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
420 return Keypair(name=os_kp.name, kp_id=os_kp.id,
421 public_key=os_kp.public_key, fingerprint=os_kp.fingerprint)
424 def keypair_exists(nova, keypair_obj):
426 Returns a copy of the keypair object if found
427 :param nova: the Nova client
428 :param keypair_obj: the keypair object
429 :return: the keypair object or None if not found
432 os_kp = nova.keypairs.get(keypair_obj)
433 return Keypair(name=os_kp.name, kp_id=os_kp.id,
434 public_key=os_kp.public_key)
439 def get_keypair_by_name(nova, name):
441 Returns a list of all available keypairs
442 :param nova: the Nova client
443 :param name: the name of the keypair to lookup
444 :return: the keypair object or None if not found
446 keypairs = nova.keypairs.list()
448 for keypair in keypairs:
449 if keypair.name == name:
450 return Keypair(name=keypair.name, kp_id=keypair.id,
451 public_key=keypair.public_key)
456 def get_keypair_by_id(nova, kp_id):
458 Returns a list of all available keypairs
459 :param nova: the Nova client
460 :param kp_id: the ID of the keypair to return
461 :return: the keypair object
463 keypair = nova.keypairs.get(kp_id)
464 return Keypair(name=keypair.name, kp_id=keypair.id,
465 public_key=keypair.public_key)
468 def delete_keypair(nova, key):
470 Deletes a keypair object from OpenStack
471 :param nova: the Nova client
472 :param key: the SNAPS-OO keypair domain object to delete
474 logger.debug('Deleting keypair - ' + key.name)
475 nova.keypairs.delete(key.id)
478 def get_availability_zone_hosts(nova, zone_name='nova'):
480 Returns the names of all nova active compute servers
481 :param nova: the Nova client
482 :param zone_name: the Nova client
483 :return: a list of compute server names
486 zones = nova.availability_zones.list()
488 if zone.zoneName == zone_name and zone.hosts:
489 for key, host in zone.hosts.items():
490 if host['nova-compute']['available']:
491 out.append(zone.zoneName + ':' + key)
496 def get_hypervisor_hosts(nova):
498 Returns the host names of all nova nodes with active hypervisors
499 :param nova: the Nova client
500 :return: a list of hypervisor host names
503 hypervisors = nova.hypervisors.list()
504 for hypervisor in hypervisors:
505 if hypervisor.state == "up":
506 out.append(hypervisor.hypervisor_hostname)
511 def delete_vm_instance(nova, vm_inst):
513 Deletes a VM instance
514 :param nova: the nova client
515 :param vm_inst: the snaps.domain.VmInst object
517 nova.servers.delete(vm_inst.id)
520 def __get_os_flavor(nova, flavor_id):
522 Returns to OpenStack flavor object by name
523 :param nova: the Nova client
524 :param flavor_id: the flavor's ID value
525 :return: the OpenStack Flavor object
528 return nova.flavors.get(flavor_id)
533 def get_flavor(nova, flavor):
535 Returns to OpenStack flavor object by name
536 :param nova: the Nova client
537 :param flavor: the SNAPS flavor domain object
538 :return: the SNAPS Flavor domain object
540 os_flavor = __get_os_flavor(nova, flavor.id)
543 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
544 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
545 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
546 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
548 return nova.flavors.get(flavor.id)
553 def get_flavor_by_id(nova, flavor_id):
555 Returns to OpenStack flavor object by name
556 :param nova: the Nova client
557 :param flavor_id: the flavor ID value
558 :return: the SNAPS Flavor domain object
560 os_flavor = __get_os_flavor(nova, flavor_id)
563 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
564 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
565 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
566 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
569 def __get_os_flavor_by_name(nova, name):
571 Returns to OpenStack flavor object by name
572 :param nova: the Nova client
573 :param name: the name of the flavor to query
574 :return: OpenStack flavor object
577 return nova.flavors.find(name=name)
582 def get_flavor_by_name(nova, name):
584 Returns a flavor by name
585 :param nova: the Nova client
586 :param name: the flavor name to return
587 :return: the SNAPS flavor domain object or None if not exists
589 os_flavor = __get_os_flavor_by_name(nova, name)
592 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
593 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
594 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
595 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
598 def create_flavor(nova, flavor_settings):
600 Creates and returns and OpenStack flavor object
601 :param nova: the Nova client
602 :param flavor_settings: the flavor settings
603 :return: the SNAPS flavor domain object
605 os_flavor = nova.flavors.create(
606 name=flavor_settings.name, flavorid=flavor_settings.flavor_id,
607 ram=flavor_settings.ram, vcpus=flavor_settings.vcpus,
608 disk=flavor_settings.disk, ephemeral=flavor_settings.ephemeral,
609 swap=flavor_settings.swap, rxtx_factor=flavor_settings.rxtx_factor,
610 is_public=flavor_settings.is_public)
612 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
613 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
614 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
615 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
618 def delete_flavor(nova, flavor):
621 :param nova: the Nova client
622 :param flavor: the SNAPS flavor domain object
624 nova.flavors.delete(flavor.id)
627 def set_flavor_keys(nova, flavor, metadata):
629 Sets metadata on the flavor
630 :param nova: the Nova client
631 :param flavor: the SNAPS flavor domain object
632 :param metadata: the metadata to set
634 os_flavor = __get_os_flavor(nova, flavor.id)
636 os_flavor.set_keys(metadata)
639 def get_flavor_keys(nova, flavor):
641 Sets metadata on the flavor
642 :param nova: the Nova client
643 :param flavor: the SNAPS flavor domain object
645 os_flavor = __get_os_flavor(nova, flavor.id)
647 return os_flavor.get_keys()
650 def add_security_group(nova, vm, security_group_name):
652 Adds a security group to an existing VM
653 :param nova: the nova client
654 :param vm: the OpenStack server object (VM) to alter
655 :param security_group_name: the name of the security group to add
658 nova.servers.add_security_group(str(vm.id), security_group_name)
659 except ClientException as e:
660 sec_grp_names = get_server_security_group_names(nova, vm)
661 if security_group_name in sec_grp_names:
662 logger.warn('Security group [%s] already added to VM [%s]',
663 security_group_name, vm.name)
666 logger.error('Unexpected error while adding security group [%s] - %s',
667 security_group_name, e)
671 def remove_security_group(nova, vm, security_group):
673 Removes a security group from an existing VM
674 :param nova: the nova client
675 :param vm: the OpenStack server object (VM) to alter
676 :param security_group: the SNAPS SecurityGroup domain object to add
678 nova.servers.remove_security_group(str(vm.id), security_group.name)
681 def get_compute_quotas(nova, project_id):
683 Returns a list of all available keypairs
684 :param nova: the Nova client
685 :param project_id: the project's ID of the quotas to lookup
686 :return: an object of type ComputeQuotas or None if not found
688 quotas = nova.quotas.get(tenant_id=project_id)
690 return ComputeQuotas(quotas)
693 def update_quotas(nova, project_id, compute_quotas):
695 Updates the compute quotas for a given project
696 :param nova: the Nova client
697 :param project_id: the project's ID that requires quota updates
698 :param compute_quotas: an object of type ComputeQuotas containing the
702 update_values = dict()
703 update_values['metadata_items'] = compute_quotas.metadata_items
704 update_values['cores'] = compute_quotas.cores
705 update_values['instances'] = compute_quotas.instances
706 update_values['injected_files'] = compute_quotas.injected_files
707 update_values['injected_file_content_bytes'] = (
708 compute_quotas.injected_file_content_bytes)
709 update_values['ram'] = compute_quotas.ram
710 update_values['fixed_ips'] = compute_quotas.fixed_ips
711 update_values['key_pairs'] = compute_quotas.key_pairs
713 return nova.quotas.update(project_id, **update_values)
716 def attach_volume(nova, neutron, server, volume, timeout=120):
718 Attaches a volume to a server. When the timeout parameter is used, a VmInst
719 object with the proper volume updates is returned unless it has not been
720 updated in the allotted amount of time then an Exception will be raised.
721 :param nova: the nova client
722 :param neutron: the neutron client
723 :param server: the VMInst domain object
724 :param volume: the Volume domain object
725 :param timeout: denotes the amount of time to block to determine if the
726 has been properly attached.
727 :return: updated VmInst object
729 nova.volumes.create_server_volume(server.id, volume.id)
731 start_time = time.time()
732 while time.time() < start_time + timeout:
733 vm = get_server_object_by_id(nova, neutron, server.id)
734 for vol_dict in vm.volume_ids:
735 if volume.id == vol_dict['id']:
737 time.sleep(POLL_INTERVAL)
740 'Attach failed on volume - {} and server - {}'.format(
741 volume.id, server.id))
744 def detach_volume(nova, neutron, server, volume, timeout=120):
746 Detaches a volume to a server. When the timeout parameter is used, a VmInst
747 object with the proper volume updates is returned unless it has not been
748 updated in the allotted amount of time then an Exception will be raised.
749 :param nova: the nova client
750 :param neutron: the neutron client
751 :param server: the VMInst domain object
752 :param volume: the Volume domain object
753 :param timeout: denotes the amount of time to block to determine if the
754 has been properly detached.
755 :return: updated VmInst object
757 nova.volumes.delete_server_volume(server.id, volume.id)
759 start_time = time.time()
760 while time.time() < start_time + timeout:
761 vm = get_server_object_by_id(nova, neutron, server.id)
762 if len(vm.volume_ids) == 0:
766 for vol_dict in vm.volume_ids:
767 ids.append(vol_dict['id'])
768 if volume.id not in ids:
770 time.sleep(POLL_INTERVAL)
773 'Detach failed on volume - {} server - {}'.format(
774 volume.id, server.id))
777 class RebootType(enum.Enum):
785 class NovaException(Exception):
787 Exception when calls to the Keystone client cannot be served properly
791 class ServerNotFoundError(Exception):
793 Exception when operations to a VM/Server is requested and the OpenStack
794 Server instance cannot be located