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, session=None):
47 Instantiates and returns a client for communications with OpenStack's Nova
49 :param os_creds: The connection credentials to the OpenStack API
50 :param session: the keystone session object (optional)
51 :return: the client object
53 logger.debug('Retrieving Nova Client')
55 session = keystone_utils.keystone_session(os_creds)
57 return Client(os_creds.compute_api_version,
59 region_name=os_creds.region_name)
62 def create_server(nova, keystone, neutron, glance, instance_config,
63 image_config, project_name, keypair_config=None):
66 :param nova: the nova client (required)
67 :param keystone: the keystone client for retrieving projects (required)
68 :param neutron: the neutron client for retrieving ports (required)
69 :param glance: the glance client (required)
70 :param instance_config: the VMInstConfig object (required)
71 :param image_config: the VM's ImageConfig object (required)
72 :param project_name: the associated project name (required)
73 :param keypair_config: the VM's KeypairConfig object (optional)
74 :return: a snaps.domain.VmInst object
79 for port_setting in instance_config.port_settings:
80 port = neutron_utils.get_port(
81 neutron, keystone, port_settings=port_setting,
82 project_name=project_name)
86 raise Exception('Cannot find port named - ' + port_setting.name)
90 kv['port-id'] = port.id
93 logger.info('Creating VM with name - ' + instance_config.name)
96 keypair_name = keypair_config.name
98 flavor = get_flavor_by_name(nova, instance_config.flavor)
101 'Flavor not found with name - %s', instance_config.flavor)
103 image = glance_utils.get_image(glance, image_settings=image_config)
106 if instance_config.userdata:
107 if isinstance(instance_config.userdata, str):
108 userdata = instance_config.userdata + '\n'
109 elif (isinstance(instance_config.userdata, dict) and
110 'script_file' in instance_config.userdata):
112 userdata = file_utils.read_file(
113 instance_config.userdata['script_file'])
114 except Exception as e:
115 logger.warn('error reading userdata file %s - %s',
116 instance_config.userdata, e)
117 args = {'name': instance_config.name,
121 'key_name': keypair_name,
123 instance_config.security_group_names,
124 'userdata': userdata}
126 if instance_config.availability_zone:
127 args['availability_zone'] = instance_config.availability_zone
129 server = nova.servers.create(**args)
131 return __map_os_server_obj_to_vm_inst(
132 neutron, keystone, server, project_name)
135 'Cannot create instance, image cannot be located with name %s',
139 def get_server(nova, neutron, keystone, vm_inst_settings=None,
140 server_name=None, project_id=None):
142 Returns a VmInst object for the first server instance found.
143 :param nova: the Nova client
144 :param neutron: the Neutron client
145 :param keystone: the Keystone client
146 :param vm_inst_settings: the VmInstanceConfig object from which to build
147 the query if not None
148 :param server_name: the server with this name to return if vm_inst_settings
150 :param project_id: the assocaited project ID
151 :return: a snaps.domain.VmInst object or None if not found
155 search_opts['name'] = vm_inst_settings.name
157 search_opts['name'] = server_name
159 servers = nova.servers.list(search_opts=search_opts)
160 for server in servers:
161 return __map_os_server_obj_to_vm_inst(
162 neutron, keystone, server, project_id)
165 def get_server_connection(nova, vm_inst_settings=None, server_name=None):
167 Returns a VmInst object for the first server instance found.
168 :param nova: the Nova client
169 :param vm_inst_settings: the VmInstanceConfig object from which to build
170 the query if not None
171 :param server_name: the server with this name to return if vm_inst_settings
173 :return: a snaps.domain.VmInst object or None if not found
177 search_opts['name'] = vm_inst_settings.name
179 search_opts['name'] = server_name
181 servers = nova.servers.list(search_opts=search_opts)
182 for server in servers:
183 return server.links[0]
186 def __map_os_server_obj_to_vm_inst(neutron, keystone, os_server,
189 Returns a VmInst object for an OpenStack Server object
190 :param neutron: the Neutron client
191 :param keystone: the Keystone client
192 :param os_server: the OpenStack server object
193 :param project_name: the associated project name
194 :return: an equivalent SNAPS-OO VmInst domain object
196 sec_grp_names = list()
197 # VM must be active for 'security_groups' attr to be initialized
198 if hasattr(os_server, 'security_groups'):
199 for sec_group in os_server.security_groups:
200 if sec_group.get('name'):
201 sec_grp_names.append(sec_group.get('name'))
204 if len(os_server.networks) > 0:
205 for net_name, ips in os_server.networks.items():
206 network = neutron_utils.get_network(
207 neutron, keystone, network_name=net_name,
208 project_name=project_name)
209 ports = neutron_utils.get_ports(neutron, network, ips)
211 out_ports.append(port)
214 if hasattr(os_server, 'os-extended-volumes:volumes_attached'):
215 volumes = getattr(os_server, 'os-extended-volumes:volumes_attached')
218 name=os_server.name, inst_id=os_server.id,
219 image_id=os_server.image['id'], flavor_id=os_server.flavor['id'],
220 ports=out_ports, keypair_name=os_server.key_name,
221 sec_grp_names=sec_grp_names, volume_ids=volumes,
222 compute_host=os_server._info.get('OS-EXT-SRV-ATTR:host'),
223 availability_zone=os_server._info.get('OS-EXT-AZ:availability_zone'))
226 def __get_latest_server_os_object(nova, server):
228 Returns a server with a given id
229 :param nova: the Nova client
230 :param server: the domain VmInst object
231 :return: the list of servers or None if not found
233 return __get_latest_server_os_object_by_id(nova, server.id)
236 def __get_latest_server_os_object_by_id(nova, server_id):
238 Returns a server with a given id
239 :param nova: the Nova client
240 :param server_id: the server's ID
241 :return: the list of servers or None if not found
243 return nova.servers.get(server_id)
246 def get_server_status(nova, server):
248 Returns the a VM instance's status from OpenStack
249 :param nova: the Nova client
250 :param server: the domain VmInst object
251 :return: the VM's string status or None if not founc
253 server = __get_latest_server_os_object(nova, server)
259 def get_server_console_output(nova, server):
261 Returns the console object for parsing VM activity
262 :param nova: the Nova client
263 :param server: the domain VmInst object
264 :return: the console output object or None if server object is not found
266 server = __get_latest_server_os_object(nova, server)
268 return server.get_console_output()
272 def get_latest_server_object(nova, neutron, keystone, server, project_name):
274 Returns a server with a given id
275 :param nova: the Nova client
276 :param neutron: the Neutron client
277 :param keystone: the Keystone client
278 :param server: the old server object
279 :param project_name: the associated project name
280 :return: the list of servers or None if not found
282 server = __get_latest_server_os_object(nova, server)
283 return __map_os_server_obj_to_vm_inst(
284 neutron, keystone, server, project_name)
287 def get_server_object_by_id(nova, neutron, keystone, server_id,
290 Returns a server with a given id
291 :param nova: the Nova client
292 :param neutron: the Neutron client
293 :param keystone: the Keystone client
294 :param server_id: the server's id
295 :param project_name: the associated project name
296 :return: an SNAPS-OO VmInst object or None if not found
298 server = __get_latest_server_os_object_by_id(nova, server_id)
299 return __map_os_server_obj_to_vm_inst(
300 neutron, keystone, server, project_name)
303 def get_server_security_group_names(nova, server):
305 Returns a server with a given id
306 :param nova: the Nova client
307 :param server: the old server object
308 :return: the list of security groups associated with a VM
311 os_vm_inst = __get_latest_server_os_object(nova, server)
312 if hasattr(os_vm_inst, 'security_groups'):
313 for sec_grp_dict in os_vm_inst.security_groups:
314 out.append(sec_grp_dict['name'])
318 def get_server_info(nova, server):
320 Returns a dictionary of a VMs info as returned by OpenStack
321 :param nova: the Nova client
322 :param server: the old server object
323 :return: a dict of the info if VM exists else None
325 vm = __get_latest_server_os_object(nova, server)
331 def reboot_server(nova, server, reboot_type=None):
333 Returns a dictionary of a VMs info as returned by OpenStack
334 :param nova: the Nova client
335 :param server: the old server object
336 :param reboot_type: Acceptable values 'SOFT', 'HARD'
337 (api uses SOFT as the default)
338 :return: a dict of the info if VM exists else None
340 vm = __get_latest_server_os_object(nova, server)
342 vm.reboot(reboot_type=reboot_type.value)
344 raise ServerNotFoundError('Cannot locate server')
347 def create_keys(key_size=2048):
349 Generates public and private keys
350 :param key_size: the number of bytes for the key size
351 :return: the cryptography keys
353 return rsa.generate_private_key(backend=default_backend(),
354 public_exponent=65537,
358 def public_key_openssh(keys):
360 Returns the public key for OpenSSH
361 :param keys: the keys generated by create_keys() from cryptography
362 :return: the OpenSSH public key
364 return keys.public_key().public_bytes(serialization.Encoding.OpenSSH,
365 serialization.PublicFormat.OpenSSH)
368 def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None):
370 Saves the generated RSA generated keys to the filesystem
371 :param keys: the keys to save generated by cryptography
372 :param pub_file_path: the path to the public keys
373 :param priv_file_path: the path to the private keys
378 pub_expand_file = os.path.expanduser(pub_file_path)
379 pub_dir = os.path.dirname(pub_expand_file)
381 if not os.path.isdir(pub_dir):
386 public_handle = open(pub_expand_file, 'wb')
387 public_bytes = keys.public_key().public_bytes(
388 serialization.Encoding.OpenSSH,
389 serialization.PublicFormat.OpenSSH)
390 public_handle.write(public_bytes)
393 public_handle.close()
395 os.chmod(pub_expand_file, 0o600)
396 logger.info("Saved public key to - " + pub_expand_file)
399 priv_expand_file = os.path.expanduser(priv_file_path)
400 priv_dir = os.path.dirname(priv_expand_file)
401 if not os.path.isdir(priv_dir):
404 private_handle = None
406 private_handle = open(priv_expand_file, 'wb')
407 private_handle.write(
409 encoding=serialization.Encoding.PEM,
410 format=serialization.PrivateFormat.TraditionalOpenSSL,
411 encryption_algorithm=serialization.NoEncryption()))
414 private_handle.close()
416 os.chmod(priv_expand_file, 0o600)
417 logger.info("Saved private key to - " + priv_expand_file)
420 def upload_keypair_file(nova, name, file_path):
422 Uploads a public key from a file
423 :param nova: the Nova client
424 :param name: the keypair name
425 :param file_path: the path to the public key file
426 :return: the keypair object
430 with open(os.path.expanduser(file_path), 'rb') as fpubkey:
431 logger.info('Saving keypair to - ' + file_path)
432 return upload_keypair(nova, name, fpubkey.read())
438 def upload_keypair(nova, name, key):
440 Uploads a public key from a file
441 :param nova: the Nova client
442 :param name: the keypair name
443 :param key: the public key object
444 :return: the keypair object
446 logger.info('Creating keypair with name - ' + name)
447 os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
448 return Keypair(name=os_kp.name, kp_id=os_kp.id,
449 public_key=os_kp.public_key, fingerprint=os_kp.fingerprint)
452 def keypair_exists(nova, keypair_obj):
454 Returns a copy of the keypair object if found
455 :param nova: the Nova client
456 :param keypair_obj: the keypair object
457 :return: the keypair object or None if not found
460 os_kp = nova.keypairs.get(keypair_obj)
461 return Keypair(name=os_kp.name, kp_id=os_kp.id,
462 public_key=os_kp.public_key)
467 def get_keypair_by_name(nova, name):
469 Returns a list of all available keypairs
470 :param nova: the Nova client
471 :param name: the name of the keypair to lookup
472 :return: the keypair object or None if not found
474 keypairs = nova.keypairs.list()
476 for keypair in keypairs:
477 if keypair.name == name:
478 return Keypair(name=keypair.name, kp_id=keypair.id,
479 public_key=keypair.public_key)
484 def get_keypair_by_id(nova, kp_id):
486 Returns a list of all available keypairs
487 :param nova: the Nova client
488 :param kp_id: the ID of the keypair to return
489 :return: the keypair object
491 keypair = nova.keypairs.get(kp_id)
492 return Keypair(name=keypair.name, kp_id=keypair.id,
493 public_key=keypair.public_key)
496 def delete_keypair(nova, key):
498 Deletes a keypair object from OpenStack
499 :param nova: the Nova client
500 :param key: the SNAPS-OO keypair domain object to delete
502 logger.debug('Deleting keypair - ' + key.name)
503 nova.keypairs.delete(key.id)
506 def get_availability_zone_hosts(nova, zone_name='nova'):
508 Returns the names of all nova active compute servers
509 :param nova: the Nova client
510 :param zone_name: the Nova client
511 :return: a list of compute server names
514 zones = nova.availability_zones.list()
516 if zone.zoneName == zone_name and zone.hosts:
517 for key, host in zone.hosts.items():
518 if host['nova-compute']['available']:
519 out.append(zone.zoneName + ':' + key)
524 def get_hypervisor_hosts(nova):
526 Returns the host names of all nova nodes with active hypervisors
527 :param nova: the Nova client
528 :return: a list of hypervisor host names
531 hypervisors = nova.hypervisors.list()
532 for hypervisor in hypervisors:
533 if hypervisor.state == "up":
534 out.append(hypervisor.hypervisor_hostname)
539 def delete_vm_instance(nova, vm_inst):
541 Deletes a VM instance
542 :param nova: the nova client
543 :param vm_inst: the snaps.domain.VmInst object
545 nova.servers.delete(vm_inst.id)
548 def __get_os_flavor(nova, flavor_id):
550 Returns to OpenStack flavor object by name
551 :param nova: the Nova client
552 :param flavor_id: the flavor's ID value
553 :return: the OpenStack Flavor object
556 return nova.flavors.get(flavor_id)
561 def get_flavor(nova, flavor):
563 Returns to OpenStack flavor object by name
564 :param nova: the Nova client
565 :param flavor: the SNAPS flavor domain object
566 :return: the SNAPS Flavor domain object
568 os_flavor = __get_os_flavor(nova, flavor.id)
571 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
572 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
573 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
574 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
576 return nova.flavors.get(flavor.id)
581 def get_flavor_by_id(nova, flavor_id):
583 Returns to OpenStack flavor object by name
584 :param nova: the Nova client
585 :param flavor_id: the flavor ID value
586 :return: the SNAPS Flavor domain object
588 os_flavor = __get_os_flavor(nova, flavor_id)
591 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
592 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
593 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
594 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
597 def __get_os_flavor_by_name(nova, name):
599 Returns to OpenStack flavor object by name
600 :param nova: the Nova client
601 :param name: the name of the flavor to query
602 :return: OpenStack flavor object
605 return nova.flavors.find(name=name)
610 def get_flavor_by_name(nova, name):
612 Returns a flavor by name
613 :param nova: the Nova client
614 :param name: the flavor name to return
615 :return: the SNAPS flavor domain object or None if not exists
617 os_flavor = __get_os_flavor_by_name(nova, name)
620 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
621 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
622 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
623 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
626 def create_flavor(nova, flavor_settings):
628 Creates and returns and OpenStack flavor object
629 :param nova: the Nova client
630 :param flavor_settings: the flavor settings
631 :return: the SNAPS flavor domain object
633 os_flavor = nova.flavors.create(
634 name=flavor_settings.name, flavorid=flavor_settings.flavor_id,
635 ram=flavor_settings.ram, vcpus=flavor_settings.vcpus,
636 disk=flavor_settings.disk, ephemeral=flavor_settings.ephemeral,
637 swap=flavor_settings.swap, rxtx_factor=flavor_settings.rxtx_factor,
638 is_public=flavor_settings.is_public)
640 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
641 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
642 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
643 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
646 def delete_flavor(nova, flavor):
649 :param nova: the Nova client
650 :param flavor: the SNAPS flavor domain object
652 nova.flavors.delete(flavor.id)
655 def set_flavor_keys(nova, flavor, metadata):
657 Sets metadata on the flavor
658 :param nova: the Nova client
659 :param flavor: the SNAPS flavor domain object
660 :param metadata: the metadata to set
662 os_flavor = __get_os_flavor(nova, flavor.id)
664 os_flavor.set_keys(metadata)
667 def get_flavor_keys(nova, flavor):
669 Sets metadata on the flavor
670 :param nova: the Nova client
671 :param flavor: the SNAPS flavor domain object
673 os_flavor = __get_os_flavor(nova, flavor.id)
675 return os_flavor.get_keys()
678 def add_security_group(nova, vm, security_group_name):
680 Adds a security group to an existing VM
681 :param nova: the nova client
682 :param vm: the OpenStack server object (VM) to alter
683 :param security_group_name: the name of the security group to add
686 nova.servers.add_security_group(str(vm.id), security_group_name)
687 except ClientException as e:
688 sec_grp_names = get_server_security_group_names(nova, vm)
689 if security_group_name in sec_grp_names:
690 logger.warn('Security group [%s] already added to VM [%s]',
691 security_group_name, vm.name)
694 logger.error('Unexpected error while adding security group [%s] - %s',
695 security_group_name, e)
699 def remove_security_group(nova, vm, security_group):
701 Removes a security group from an existing VM
702 :param nova: the nova client
703 :param vm: the OpenStack server object (VM) to alter
704 :param security_group: the SNAPS SecurityGroup domain object to add
706 nova.servers.remove_security_group(str(vm.id), security_group.name)
709 def get_compute_quotas(nova, project_id):
711 Returns a list of all available keypairs
712 :param nova: the Nova client
713 :param project_id: the project's ID of the quotas to lookup
714 :return: an object of type ComputeQuotas or None if not found
716 quotas = nova.quotas.get(tenant_id=project_id)
718 return ComputeQuotas(quotas)
721 def update_quotas(nova, project_id, compute_quotas):
723 Updates the compute quotas for a given project
724 :param nova: the Nova client
725 :param project_id: the project's ID that requires quota updates
726 :param compute_quotas: an object of type ComputeQuotas containing the
730 update_values = dict()
731 update_values['metadata_items'] = compute_quotas.metadata_items
732 update_values['cores'] = compute_quotas.cores
733 update_values['instances'] = compute_quotas.instances
734 update_values['injected_files'] = compute_quotas.injected_files
735 update_values['injected_file_content_bytes'] = (
736 compute_quotas.injected_file_content_bytes)
737 update_values['ram'] = compute_quotas.ram
738 update_values['fixed_ips'] = compute_quotas.fixed_ips
739 update_values['key_pairs'] = compute_quotas.key_pairs
741 return nova.quotas.update(project_id, **update_values)
744 def attach_volume(nova, neutron, keystone, server, volume, project_name,
747 Attaches a volume to a server. When the timeout parameter is used, a VmInst
748 object with the proper volume updates is returned unless it has not been
749 updated in the allotted amount of time then an Exception will be raised.
750 :param nova: the nova client
751 :param neutron: the neutron client
752 :param keystone: the neutron client
753 :param server: the VMInst domain object
754 :param volume: the Volume domain object
755 :param project_name: the associated project name
756 :param timeout: denotes the amount of time to block to determine if the
757 has been properly attached.
758 :return: updated VmInst object
760 nova.volumes.create_server_volume(server.id, volume.id)
762 start_time = time.time()
763 while time.time() < start_time + timeout:
764 vm = get_server_object_by_id(
765 nova, neutron, keystone, server.id, project_name)
766 for vol_dict in vm.volume_ids:
767 if volume.id == vol_dict['id']:
769 time.sleep(POLL_INTERVAL)
772 'Attach failed on volume - {} and server - {}'.format(
773 volume.id, server.id))
776 def detach_volume(nova, neutron, keystone, server, volume, project_name,
779 Detaches a volume to a server. When the timeout parameter is used, a VmInst
780 object with the proper volume updates is returned unless it has not been
781 updated in the allotted amount of time then an Exception will be raised.
782 :param nova: the nova client
783 :param neutron: the neutron client
784 :param keystone: the keystone client
785 :param server: the VMInst domain object
786 :param volume: the Volume domain object
787 :param project_name: the associated project name
788 :param timeout: denotes the amount of time to block to determine if the
789 has been properly detached.
790 :return: updated VmInst object
792 nova.volumes.delete_server_volume(server.id, volume.id)
794 start_time = time.time()
795 while time.time() < start_time + timeout:
796 vm = get_server_object_by_id(
797 nova, neutron, keystone, server.id, project_name)
798 if len(vm.volume_ids) == 0:
802 for vol_dict in vm.volume_ids:
803 ids.append(vol_dict['id'])
804 if volume.id not in ids:
806 time.sleep(POLL_INTERVAL)
809 'Detach failed on volume - {} server - {}'.format(
810 volume.id, server.id))
813 class RebootType(enum.Enum):
821 class NovaException(Exception):
823 Exception when calls to the Keystone client cannot be served properly
827 class ServerNotFoundError(Exception):
829 Exception when operations to a VM/Server is requested and the OpenStack
830 Server instance cannot be located