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, keystone, neutron, glance, instance_config,
59 image_config, project_name, keypair_config=None):
62 :param nova: the nova client (required)
63 :param keystone: the keystone client for retrieving projects (required)
64 :param neutron: the neutron client for retrieving ports (required)
65 :param glance: the glance client (required)
66 :param instance_config: the VMInstConfig object (required)
67 :param image_config: the VM's ImageConfig object (required)
68 :param project_name: the associated project name (required)
69 :param keypair_config: the VM's KeypairConfig object (optional)
70 :return: a snaps.domain.VmInst object
75 for port_setting in instance_config.port_settings:
76 port = neutron_utils.get_port(
77 neutron, keystone, port_settings=port_setting,
78 project_name=project_name)
82 raise Exception('Cannot find port named - ' + port_setting.name)
86 kv['port-id'] = port.id
89 logger.info('Creating VM with name - ' + instance_config.name)
92 keypair_name = keypair_config.name
94 flavor = get_flavor_by_name(nova, instance_config.flavor)
97 'Flavor not found with name - %s', instance_config.flavor)
99 image = glance_utils.get_image(glance, image_settings=image_config)
102 if instance_config.userdata:
103 if isinstance(instance_config.userdata, str):
104 userdata = instance_config.userdata + '\n'
105 elif (isinstance(instance_config.userdata, dict) and
106 'script_file' in instance_config.userdata):
108 userdata = file_utils.read_file(
109 instance_config.userdata['script_file'])
110 except Exception as e:
111 logger.warn('error reading userdata file %s - %s',
112 instance_config.userdata, e)
113 args = {'name': instance_config.name,
117 'key_name': keypair_name,
119 instance_config.security_group_names,
120 'userdata': userdata}
122 if instance_config.availability_zone:
123 args['availability_zone'] = instance_config.availability_zone
125 server = nova.servers.create(**args)
127 return __map_os_server_obj_to_vm_inst(
128 neutron, keystone, server, project_name)
131 'Cannot create instance, image cannot be located with name %s',
135 def get_server(nova, neutron, keystone, vm_inst_settings=None,
136 server_name=None, project_id=None):
138 Returns a VmInst object for the first server instance found.
139 :param nova: the Nova client
140 :param neutron: the Neutron client
141 :param keystone: the Keystone client
142 :param vm_inst_settings: the VmInstanceConfig object from which to build
143 the query if not None
144 :param server_name: the server with this name to return if vm_inst_settings
146 :param project_id: the assocaited project ID
147 :return: a snaps.domain.VmInst object or None if not found
151 search_opts['name'] = vm_inst_settings.name
153 search_opts['name'] = server_name
155 servers = nova.servers.list(search_opts=search_opts)
156 for server in servers:
157 return __map_os_server_obj_to_vm_inst(
158 neutron, keystone, server, project_id)
161 def get_server_connection(nova, vm_inst_settings=None, server_name=None):
163 Returns a VmInst object for the first server instance found.
164 :param nova: the Nova client
165 :param vm_inst_settings: the VmInstanceConfig object from which to build
166 the query if not None
167 :param server_name: the server with this name to return if vm_inst_settings
169 :return: a snaps.domain.VmInst object or None if not found
173 search_opts['name'] = vm_inst_settings.name
175 search_opts['name'] = server_name
177 servers = nova.servers.list(search_opts=search_opts)
178 for server in servers:
179 return server.links[0]
182 def __map_os_server_obj_to_vm_inst(neutron, keystone, os_server,
185 Returns a VmInst object for an OpenStack Server object
186 :param neutron: the Neutron client
187 :param keystone: the Keystone client
188 :param os_server: the OpenStack server object
189 :param project_name: the associated project name
190 :return: an equivalent SNAPS-OO VmInst domain object
192 sec_grp_names = list()
193 # VM must be active for 'security_groups' attr to be initialized
194 if hasattr(os_server, 'security_groups'):
195 for sec_group in os_server.security_groups:
196 if sec_group.get('name'):
197 sec_grp_names.append(sec_group.get('name'))
200 if len(os_server.networks) > 0:
201 for net_name, ips in os_server.networks.items():
202 network = neutron_utils.get_network(
203 neutron, keystone, network_name=net_name,
204 project_name=project_name)
205 ports = neutron_utils.get_ports(neutron, network, ips)
207 out_ports.append(port)
210 if hasattr(os_server, 'os-extended-volumes:volumes_attached'):
211 volumes = getattr(os_server, 'os-extended-volumes:volumes_attached')
214 name=os_server.name, inst_id=os_server.id,
215 image_id=os_server.image['id'], flavor_id=os_server.flavor['id'],
216 ports=out_ports, keypair_name=os_server.key_name,
217 sec_grp_names=sec_grp_names, volume_ids=volumes)
220 def __get_latest_server_os_object(nova, server):
222 Returns a server with a given id
223 :param nova: the Nova client
224 :param server: the domain VmInst object
225 :return: the list of servers or None if not found
227 return __get_latest_server_os_object_by_id(nova, server.id)
230 def __get_latest_server_os_object_by_id(nova, server_id):
232 Returns a server with a given id
233 :param nova: the Nova client
234 :param server_id: the server's ID
235 :return: the list of servers or None if not found
237 return nova.servers.get(server_id)
240 def get_server_status(nova, server):
242 Returns the a VM instance's status from OpenStack
243 :param nova: the Nova client
244 :param server: the domain VmInst object
245 :return: the VM's string status or None if not founc
247 server = __get_latest_server_os_object(nova, server)
253 def get_server_console_output(nova, server):
255 Returns the console object for parsing VM activity
256 :param nova: the Nova client
257 :param server: the domain VmInst object
258 :return: the console output object or None if server object is not found
260 server = __get_latest_server_os_object(nova, server)
262 return server.get_console_output()
266 def get_latest_server_object(nova, neutron, keystone, server, project_name):
268 Returns a server with a given id
269 :param nova: the Nova client
270 :param neutron: the Neutron client
271 :param keystone: the Keystone client
272 :param server: the old server object
273 :param project_name: the associated project name
274 :return: the list of servers or None if not found
276 server = __get_latest_server_os_object(nova, server)
277 return __map_os_server_obj_to_vm_inst(
278 neutron, keystone, server, project_name)
281 def get_server_object_by_id(nova, neutron, keystone, server_id,
284 Returns a server with a given id
285 :param nova: the Nova client
286 :param neutron: the Neutron client
287 :param keystone: the Keystone client
288 :param server_id: the server's id
289 :param project_name: the associated project name
290 :return: an SNAPS-OO VmInst object or None if not found
292 server = __get_latest_server_os_object_by_id(nova, server_id)
293 return __map_os_server_obj_to_vm_inst(
294 neutron, keystone, server, project_name)
297 def get_server_security_group_names(nova, server):
299 Returns a server with a given id
300 :param nova: the Nova client
301 :param server: the old server object
302 :return: the list of security groups associated with a VM
305 os_vm_inst = __get_latest_server_os_object(nova, server)
306 for sec_grp_dict in os_vm_inst.security_groups:
307 out.append(sec_grp_dict['name'])
311 def get_server_info(nova, server):
313 Returns a dictionary of a VMs info as returned by OpenStack
314 :param nova: the Nova client
315 :param server: the old server object
316 :return: a dict of the info if VM exists else None
318 vm = __get_latest_server_os_object(nova, server)
324 def reboot_server(nova, server, reboot_type=None):
326 Returns a dictionary of a VMs info as returned by OpenStack
327 :param nova: the Nova client
328 :param server: the old server object
329 :param reboot_type: Acceptable values 'SOFT', 'HARD'
330 (api uses SOFT as the default)
331 :return: a dict of the info if VM exists else None
333 vm = __get_latest_server_os_object(nova, server)
335 vm.reboot(reboot_type=reboot_type.value)
337 raise ServerNotFoundError('Cannot locate server')
340 def create_keys(key_size=2048):
342 Generates public and private keys
343 :param key_size: the number of bytes for the key size
344 :return: the cryptography keys
346 return rsa.generate_private_key(backend=default_backend(),
347 public_exponent=65537,
351 def public_key_openssh(keys):
353 Returns the public key for OpenSSH
354 :param keys: the keys generated by create_keys() from cryptography
355 :return: the OpenSSH public key
357 return keys.public_key().public_bytes(serialization.Encoding.OpenSSH,
358 serialization.PublicFormat.OpenSSH)
361 def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None):
363 Saves the generated RSA generated keys to the filesystem
364 :param keys: the keys to save generated by cryptography
365 :param pub_file_path: the path to the public keys
366 :param priv_file_path: the path to the private keys
371 pub_expand_file = os.path.expanduser(pub_file_path)
372 pub_dir = os.path.dirname(pub_expand_file)
374 if not os.path.isdir(pub_dir):
379 public_handle = open(pub_expand_file, 'wb')
380 public_bytes = keys.public_key().public_bytes(
381 serialization.Encoding.OpenSSH,
382 serialization.PublicFormat.OpenSSH)
383 public_handle.write(public_bytes)
386 public_handle.close()
388 os.chmod(pub_expand_file, 0o600)
389 logger.info("Saved public key to - " + pub_expand_file)
392 priv_expand_file = os.path.expanduser(priv_file_path)
393 priv_dir = os.path.dirname(priv_expand_file)
394 if not os.path.isdir(priv_dir):
397 private_handle = None
399 private_handle = open(priv_expand_file, 'wb')
400 private_handle.write(
402 encoding=serialization.Encoding.PEM,
403 format=serialization.PrivateFormat.TraditionalOpenSSL,
404 encryption_algorithm=serialization.NoEncryption()))
407 private_handle.close()
409 os.chmod(priv_expand_file, 0o600)
410 logger.info("Saved private key to - " + priv_expand_file)
413 def upload_keypair_file(nova, name, file_path):
415 Uploads a public key from a file
416 :param nova: the Nova client
417 :param name: the keypair name
418 :param file_path: the path to the public key file
419 :return: the keypair object
423 with open(os.path.expanduser(file_path), 'rb') as fpubkey:
424 logger.info('Saving keypair to - ' + file_path)
425 return upload_keypair(nova, name, fpubkey.read())
431 def upload_keypair(nova, name, key):
433 Uploads a public key from a file
434 :param nova: the Nova client
435 :param name: the keypair name
436 :param key: the public key object
437 :return: the keypair object
439 logger.info('Creating keypair with name - ' + name)
440 os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
441 return Keypair(name=os_kp.name, kp_id=os_kp.id,
442 public_key=os_kp.public_key, fingerprint=os_kp.fingerprint)
445 def keypair_exists(nova, keypair_obj):
447 Returns a copy of the keypair object if found
448 :param nova: the Nova client
449 :param keypair_obj: the keypair object
450 :return: the keypair object or None if not found
453 os_kp = nova.keypairs.get(keypair_obj)
454 return Keypair(name=os_kp.name, kp_id=os_kp.id,
455 public_key=os_kp.public_key)
460 def get_keypair_by_name(nova, name):
462 Returns a list of all available keypairs
463 :param nova: the Nova client
464 :param name: the name of the keypair to lookup
465 :return: the keypair object or None if not found
467 keypairs = nova.keypairs.list()
469 for keypair in keypairs:
470 if keypair.name == name:
471 return Keypair(name=keypair.name, kp_id=keypair.id,
472 public_key=keypair.public_key)
477 def get_keypair_by_id(nova, kp_id):
479 Returns a list of all available keypairs
480 :param nova: the Nova client
481 :param kp_id: the ID of the keypair to return
482 :return: the keypair object
484 keypair = nova.keypairs.get(kp_id)
485 return Keypair(name=keypair.name, kp_id=keypair.id,
486 public_key=keypair.public_key)
489 def delete_keypair(nova, key):
491 Deletes a keypair object from OpenStack
492 :param nova: the Nova client
493 :param key: the SNAPS-OO keypair domain object to delete
495 logger.debug('Deleting keypair - ' + key.name)
496 nova.keypairs.delete(key.id)
499 def get_availability_zone_hosts(nova, zone_name='nova'):
501 Returns the names of all nova active compute servers
502 :param nova: the Nova client
503 :param zone_name: the Nova client
504 :return: a list of compute server names
507 zones = nova.availability_zones.list()
509 if zone.zoneName == zone_name and zone.hosts:
510 for key, host in zone.hosts.items():
511 if host['nova-compute']['available']:
512 out.append(zone.zoneName + ':' + key)
517 def get_hypervisor_hosts(nova):
519 Returns the host names of all nova nodes with active hypervisors
520 :param nova: the Nova client
521 :return: a list of hypervisor host names
524 hypervisors = nova.hypervisors.list()
525 for hypervisor in hypervisors:
526 if hypervisor.state == "up":
527 out.append(hypervisor.hypervisor_hostname)
532 def delete_vm_instance(nova, vm_inst):
534 Deletes a VM instance
535 :param nova: the nova client
536 :param vm_inst: the snaps.domain.VmInst object
538 nova.servers.delete(vm_inst.id)
541 def __get_os_flavor(nova, flavor_id):
543 Returns to OpenStack flavor object by name
544 :param nova: the Nova client
545 :param flavor_id: the flavor's ID value
546 :return: the OpenStack Flavor object
549 return nova.flavors.get(flavor_id)
554 def get_flavor(nova, flavor):
556 Returns to OpenStack flavor object by name
557 :param nova: the Nova client
558 :param flavor: the SNAPS flavor domain object
559 :return: the SNAPS Flavor domain object
561 os_flavor = __get_os_flavor(nova, flavor.id)
564 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
565 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
566 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
567 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
569 return nova.flavors.get(flavor.id)
574 def get_flavor_by_id(nova, flavor_id):
576 Returns to OpenStack flavor object by name
577 :param nova: the Nova client
578 :param flavor_id: the flavor ID value
579 :return: the SNAPS Flavor domain object
581 os_flavor = __get_os_flavor(nova, flavor_id)
584 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
585 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
586 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
587 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
590 def __get_os_flavor_by_name(nova, name):
592 Returns to OpenStack flavor object by name
593 :param nova: the Nova client
594 :param name: the name of the flavor to query
595 :return: OpenStack flavor object
598 return nova.flavors.find(name=name)
603 def get_flavor_by_name(nova, name):
605 Returns a flavor by name
606 :param nova: the Nova client
607 :param name: the flavor name to return
608 :return: the SNAPS flavor domain object or None if not exists
610 os_flavor = __get_os_flavor_by_name(nova, name)
613 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
614 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
615 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
616 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
619 def create_flavor(nova, flavor_settings):
621 Creates and returns and OpenStack flavor object
622 :param nova: the Nova client
623 :param flavor_settings: the flavor settings
624 :return: the SNAPS flavor domain object
626 os_flavor = nova.flavors.create(
627 name=flavor_settings.name, flavorid=flavor_settings.flavor_id,
628 ram=flavor_settings.ram, vcpus=flavor_settings.vcpus,
629 disk=flavor_settings.disk, ephemeral=flavor_settings.ephemeral,
630 swap=flavor_settings.swap, rxtx_factor=flavor_settings.rxtx_factor,
631 is_public=flavor_settings.is_public)
633 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
634 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
635 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
636 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
639 def delete_flavor(nova, flavor):
642 :param nova: the Nova client
643 :param flavor: the SNAPS flavor domain object
645 nova.flavors.delete(flavor.id)
648 def set_flavor_keys(nova, flavor, metadata):
650 Sets metadata on the flavor
651 :param nova: the Nova client
652 :param flavor: the SNAPS flavor domain object
653 :param metadata: the metadata to set
655 os_flavor = __get_os_flavor(nova, flavor.id)
657 os_flavor.set_keys(metadata)
660 def get_flavor_keys(nova, flavor):
662 Sets metadata on the flavor
663 :param nova: the Nova client
664 :param flavor: the SNAPS flavor domain object
666 os_flavor = __get_os_flavor(nova, flavor.id)
668 return os_flavor.get_keys()
671 def add_security_group(nova, vm, security_group_name):
673 Adds a security group to an existing VM
674 :param nova: the nova client
675 :param vm: the OpenStack server object (VM) to alter
676 :param security_group_name: the name of the security group to add
679 nova.servers.add_security_group(str(vm.id), security_group_name)
680 except ClientException as e:
681 sec_grp_names = get_server_security_group_names(nova, vm)
682 if security_group_name in sec_grp_names:
683 logger.warn('Security group [%s] already added to VM [%s]',
684 security_group_name, vm.name)
687 logger.error('Unexpected error while adding security group [%s] - %s',
688 security_group_name, e)
692 def remove_security_group(nova, vm, security_group):
694 Removes a security group from an existing VM
695 :param nova: the nova client
696 :param vm: the OpenStack server object (VM) to alter
697 :param security_group: the SNAPS SecurityGroup domain object to add
699 nova.servers.remove_security_group(str(vm.id), security_group.name)
702 def get_compute_quotas(nova, project_id):
704 Returns a list of all available keypairs
705 :param nova: the Nova client
706 :param project_id: the project's ID of the quotas to lookup
707 :return: an object of type ComputeQuotas or None if not found
709 quotas = nova.quotas.get(tenant_id=project_id)
711 return ComputeQuotas(quotas)
714 def update_quotas(nova, project_id, compute_quotas):
716 Updates the compute quotas for a given project
717 :param nova: the Nova client
718 :param project_id: the project's ID that requires quota updates
719 :param compute_quotas: an object of type ComputeQuotas containing the
723 update_values = dict()
724 update_values['metadata_items'] = compute_quotas.metadata_items
725 update_values['cores'] = compute_quotas.cores
726 update_values['instances'] = compute_quotas.instances
727 update_values['injected_files'] = compute_quotas.injected_files
728 update_values['injected_file_content_bytes'] = (
729 compute_quotas.injected_file_content_bytes)
730 update_values['ram'] = compute_quotas.ram
731 update_values['fixed_ips'] = compute_quotas.fixed_ips
732 update_values['key_pairs'] = compute_quotas.key_pairs
734 return nova.quotas.update(project_id, **update_values)
737 def attach_volume(nova, neutron, keystone, server, volume, project_name,
740 Attaches a volume to a server. When the timeout parameter is used, a VmInst
741 object with the proper volume updates is returned unless it has not been
742 updated in the allotted amount of time then an Exception will be raised.
743 :param nova: the nova client
744 :param neutron: the neutron client
745 :param keystone: the neutron client
746 :param server: the VMInst domain object
747 :param volume: the Volume domain object
748 :param project_name: the associated project name
749 :param timeout: denotes the amount of time to block to determine if the
750 has been properly attached.
751 :return: updated VmInst object
753 nova.volumes.create_server_volume(server.id, volume.id)
755 start_time = time.time()
756 while time.time() < start_time + timeout:
757 vm = get_server_object_by_id(
758 nova, neutron, keystone, server.id, project_name)
759 for vol_dict in vm.volume_ids:
760 if volume.id == vol_dict['id']:
762 time.sleep(POLL_INTERVAL)
765 'Attach failed on volume - {} and server - {}'.format(
766 volume.id, server.id))
769 def detach_volume(nova, neutron, keystone, server, volume, project_name,
772 Detaches a volume to a server. When the timeout parameter is used, a VmInst
773 object with the proper volume updates is returned unless it has not been
774 updated in the allotted amount of time then an Exception will be raised.
775 :param nova: the nova client
776 :param neutron: the neutron client
777 :param keystone: the keystone client
778 :param server: the VMInst domain object
779 :param volume: the Volume domain object
780 :param project_name: the associated project name
781 :param timeout: denotes the amount of time to block to determine if the
782 has been properly detached.
783 :return: updated VmInst object
785 nova.volumes.delete_server_volume(server.id, volume.id)
787 start_time = time.time()
788 while time.time() < start_time + timeout:
789 vm = get_server_object_by_id(
790 nova, neutron, keystone, server.id, project_name)
791 if len(vm.volume_ids) == 0:
795 for vol_dict in vm.volume_ids:
796 ids.append(vol_dict['id'])
797 if volume.id not in ids:
799 time.sleep(POLL_INTERVAL)
802 'Detach failed on volume - {} server - {}'.format(
803 volume.id, server.id))
806 class RebootType(enum.Enum):
814 class NovaException(Exception):
816 Exception when calls to the Keystone client cannot be served properly
820 class ServerNotFoundError(Exception):
822 Exception when operations to a VM/Server is requested and the OpenStack
823 Server instance cannot be located