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')
39 Utilities for basic OpenStack Nova API calls
43 def nova_client(os_creds):
45 Instantiates and returns a client for communications with OpenStack's Nova
47 :param os_creds: The connection credentials to the OpenStack API
48 :return: the client object
50 logger.debug('Retrieving Nova Client')
51 return Client(os_creds.compute_api_version,
52 session=keystone_utils.keystone_session(os_creds),
53 region_name=os_creds.region_name)
56 def create_server(nova, neutron, glance, instance_config, image_config,
60 :param nova: the nova client (required)
61 :param neutron: the neutron client for retrieving ports (required)
62 :param glance: the glance client (required)
63 :param instance_config: the VMInstConfig object (required)
64 :param image_config: the VM's ImageConfig object (required)
65 :param keypair_config: the VM's KeypairConfig object (optional)
66 :return: a snaps.domain.VmInst object
71 for port_setting in instance_config.port_settings:
72 port = neutron_utils.get_port(neutron, port_settings=port_setting)
76 raise Exception('Cannot find port named - ' + port_setting.name)
80 kv['port-id'] = port.id
83 logger.info('Creating VM with name - ' + instance_config.name)
86 keypair_name = keypair_config.name
88 flavor = get_flavor_by_name(nova, instance_config.flavor)
91 'Flavor not found with name - %s', instance_config.flavor)
93 image = glance_utils.get_image(glance, image_settings=image_config)
96 if instance_config.userdata:
97 if isinstance(instance_config.userdata, str):
98 userdata = instance_config.userdata + '\n'
99 elif (isinstance(instance_config.userdata, dict) and
100 'script_file' in instance_config.userdata):
102 userdata = file_utils.read_file(
103 instance_config.userdata['script_file'])
104 except Exception as e:
105 logger.warn('error reading userdata file %s - %s',
106 instance_config.userdata, e)
107 args = {'name': instance_config.name,
111 'key_name': keypair_name,
113 instance_config.security_group_names,
114 'userdata': userdata}
116 if instance_config.availability_zone:
117 args['availability_zone'] = instance_config.availability_zone
119 server = nova.servers.create(**args)
121 return __map_os_server_obj_to_vm_inst(neutron, server)
124 'Cannot create instance, image cannot be located with name %s',
128 def get_server(nova, neutron, vm_inst_settings=None, server_name=None):
130 Returns a VmInst object for the first server instance found.
131 :param nova: the Nova client
132 :param neutron: the Neutron client
133 :param vm_inst_settings: the VmInstanceConfig object from which to build
134 the query if not None
135 :param server_name: the server with this name to return if vm_inst_settings
137 :return: a snaps.domain.VmInst object or None if not found
141 search_opts['name'] = vm_inst_settings.name
143 search_opts['name'] = server_name
145 servers = nova.servers.list(search_opts=search_opts)
146 for server in servers:
147 return __map_os_server_obj_to_vm_inst(neutron, server)
150 def get_server_connection(nova, vm_inst_settings=None, server_name=None):
152 Returns a VmInst object for the first server instance found.
153 :param nova: the Nova client
154 :param vm_inst_settings: the VmInstanceConfig object from which to build
155 the query if not None
156 :param server_name: the server with this name to return if vm_inst_settings
158 :return: a snaps.domain.VmInst object or None if not found
162 search_opts['name'] = vm_inst_settings.name
164 search_opts['name'] = server_name
166 servers = nova.servers.list(search_opts=search_opts)
167 for server in servers:
168 return server.links[0]
171 def __map_os_server_obj_to_vm_inst(neutron, os_server):
173 Returns a VmInst object for an OpenStack Server object
174 :param neutron: the Neutron client (when None, ports will be empty)
175 :param os_server: the OpenStack server object
176 :return: an equivalent SNAPS-OO VmInst domain object
178 sec_grp_names = list()
179 # VM must be active for 'security_groups' attr to be initialized
180 if hasattr(os_server, 'security_groups'):
181 for sec_group in os_server.security_groups:
182 if sec_group.get('name'):
183 sec_grp_names.append(sec_group.get('name'))
186 if len(os_server.networks) > 0:
187 for net_name, ips in os_server.networks.items():
188 network = neutron_utils.get_network(neutron, network_name=net_name)
189 ports = neutron_utils.get_ports(neutron, network, ips)
191 out_ports.append(port)
194 if hasattr(os_server, 'os-extended-volumes:volumes_attached'):
195 volumes = getattr(os_server, 'os-extended-volumes:volumes_attached')
198 name=os_server.name, inst_id=os_server.id,
199 image_id=os_server.image['id'], flavor_id=os_server.flavor['id'],
200 ports=out_ports, keypair_name=os_server.key_name,
201 sec_grp_names=sec_grp_names, volume_ids=volumes)
204 def __get_latest_server_os_object(nova, server):
206 Returns a server with a given id
207 :param nova: the Nova client
208 :param server: the domain VmInst object
209 :return: the list of servers or None if not found
211 return __get_latest_server_os_object_by_id(nova, server.id)
214 def __get_latest_server_os_object_by_id(nova, server_id):
216 Returns a server with a given id
217 :param nova: the Nova client
218 :param server_id: the server's ID
219 :return: the list of servers or None if not found
221 return nova.servers.get(server_id)
224 def get_server_status(nova, server):
226 Returns the a VM instance's status from OpenStack
227 :param nova: the Nova client
228 :param server: the domain VmInst object
229 :return: the VM's string status or None if not founc
231 server = __get_latest_server_os_object(nova, server)
237 def get_server_console_output(nova, server):
239 Returns the console object for parsing VM activity
240 :param nova: the Nova client
241 :param server: the domain VmInst object
242 :return: the console output object or None if server object is not found
244 server = __get_latest_server_os_object(nova, server)
246 return server.get_console_output()
250 def get_latest_server_object(nova, neutron, server):
252 Returns a server with a given id
253 :param nova: the Nova client
254 :param neutron: the Neutron client
255 :param server: the old server object
256 :return: the list of servers or None if not found
258 server = __get_latest_server_os_object(nova, server)
259 return __map_os_server_obj_to_vm_inst(neutron, server)
262 def get_server_object_by_id(nova, neutron, server_id):
264 Returns a server with a given id
265 :param nova: the Nova client
266 :param neutron: the Neutron client
267 :param server_id: the server's id
268 :return: an SNAPS-OO VmInst object or None if not found
270 server = __get_latest_server_os_object_by_id(nova, server_id)
271 return __map_os_server_obj_to_vm_inst(neutron, server)
274 def get_server_security_group_names(nova, server):
276 Returns a server with a given id
277 :param nova: the Nova client
278 :param server: the old server object
279 :return: the list of security groups associated with a VM
282 os_vm_inst = __get_latest_server_os_object(nova, server)
283 for sec_grp_dict in os_vm_inst.security_groups:
284 out.append(sec_grp_dict['name'])
288 def get_server_info(nova, server):
290 Returns a dictionary of a VMs info as returned by OpenStack
291 :param nova: the Nova client
292 :param server: the old server object
293 :return: a dict of the info if VM exists else None
295 vm = __get_latest_server_os_object(nova, server)
301 def reboot_server(nova, server, reboot_type=None):
303 Returns a dictionary of a VMs info as returned by OpenStack
304 :param nova: the Nova client
305 :param server: the old server object
306 :param reboot_type: Acceptable values 'SOFT', 'HARD'
307 (api uses SOFT as the default)
308 :return: a dict of the info if VM exists else None
310 vm = __get_latest_server_os_object(nova, server)
312 vm.reboot(reboot_type=reboot_type.value)
314 raise ServerNotFoundError('Cannot locate server')
317 def create_keys(key_size=2048):
319 Generates public and private keys
320 :param key_size: the number of bytes for the key size
321 :return: the cryptography keys
323 return rsa.generate_private_key(backend=default_backend(),
324 public_exponent=65537,
328 def public_key_openssh(keys):
330 Returns the public key for OpenSSH
331 :param keys: the keys generated by create_keys() from cryptography
332 :return: the OpenSSH public key
334 return keys.public_key().public_bytes(serialization.Encoding.OpenSSH,
335 serialization.PublicFormat.OpenSSH)
338 def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None):
340 Saves the generated RSA generated keys to the filesystem
341 :param keys: the keys to save generated by cryptography
342 :param pub_file_path: the path to the public keys
343 :param priv_file_path: the path to the private keys
348 pub_expand_file = os.path.expanduser(pub_file_path)
349 pub_dir = os.path.dirname(pub_expand_file)
351 if not os.path.isdir(pub_dir):
356 public_handle = open(pub_expand_file, 'wb')
357 public_bytes = keys.public_key().public_bytes(
358 serialization.Encoding.OpenSSH,
359 serialization.PublicFormat.OpenSSH)
360 public_handle.write(public_bytes)
363 public_handle.close()
365 os.chmod(pub_expand_file, 0o600)
366 logger.info("Saved public key to - " + pub_expand_file)
369 priv_expand_file = os.path.expanduser(priv_file_path)
370 priv_dir = os.path.dirname(priv_expand_file)
371 if not os.path.isdir(priv_dir):
374 private_handle = None
376 private_handle = open(priv_expand_file, 'wb')
377 private_handle.write(
379 encoding=serialization.Encoding.PEM,
380 format=serialization.PrivateFormat.TraditionalOpenSSL,
381 encryption_algorithm=serialization.NoEncryption()))
384 private_handle.close()
386 os.chmod(priv_expand_file, 0o600)
387 logger.info("Saved private key to - " + priv_expand_file)
390 def upload_keypair_file(nova, name, file_path):
392 Uploads a public key from a file
393 :param nova: the Nova client
394 :param name: the keypair name
395 :param file_path: the path to the public key file
396 :return: the keypair object
400 with open(os.path.expanduser(file_path), 'rb') as fpubkey:
401 logger.info('Saving keypair to - ' + file_path)
402 return upload_keypair(nova, name, fpubkey.read())
408 def upload_keypair(nova, name, key):
410 Uploads a public key from a file
411 :param nova: the Nova client
412 :param name: the keypair name
413 :param key: the public key object
414 :return: the keypair object
416 logger.info('Creating keypair with name - ' + name)
417 os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
418 return Keypair(name=os_kp.name, kp_id=os_kp.id,
419 public_key=os_kp.public_key, fingerprint=os_kp.fingerprint)
422 def keypair_exists(nova, keypair_obj):
424 Returns a copy of the keypair object if found
425 :param nova: the Nova client
426 :param keypair_obj: the keypair object
427 :return: the keypair object or None if not found
430 os_kp = nova.keypairs.get(keypair_obj)
431 return Keypair(name=os_kp.name, kp_id=os_kp.id,
432 public_key=os_kp.public_key)
437 def get_keypair_by_name(nova, name):
439 Returns a list of all available keypairs
440 :param nova: the Nova client
441 :param name: the name of the keypair to lookup
442 :return: the keypair object or None if not found
444 keypairs = nova.keypairs.list()
446 for keypair in keypairs:
447 if keypair.name == name:
448 return Keypair(name=keypair.name, kp_id=keypair.id,
449 public_key=keypair.public_key)
454 def get_keypair_by_id(nova, kp_id):
456 Returns a list of all available keypairs
457 :param nova: the Nova client
458 :param kp_id: the ID of the keypair to return
459 :return: the keypair object
461 keypair = nova.keypairs.get(kp_id)
462 return Keypair(name=keypair.name, kp_id=keypair.id,
463 public_key=keypair.public_key)
466 def delete_keypair(nova, key):
468 Deletes a keypair object from OpenStack
469 :param nova: the Nova client
470 :param key: the SNAPS-OO keypair domain object to delete
472 logger.debug('Deleting keypair - ' + key.name)
473 nova.keypairs.delete(key.id)
476 def get_availability_zone_hosts(nova, zone_name='nova'):
478 Returns the names of all nova active compute servers
479 :param nova: the Nova client
480 :param zone_name: the Nova client
481 :return: a list of compute server names
484 zones = nova.availability_zones.list()
486 if zone.zoneName == zone_name and zone.hosts:
487 for key, host in zone.hosts.items():
488 if host['nova-compute']['available']:
489 out.append(zone.zoneName + ':' + key)
494 def get_hypervisor_hosts(nova):
496 Returns the host names of all nova nodes with active hypervisors
497 :param nova: the Nova client
498 :return: a list of hypervisor host names
501 hypervisors = nova.hypervisors.list()
502 for hypervisor in hypervisors:
503 if hypervisor.state == "up":
504 out.append(hypervisor.hypervisor_hostname)
509 def delete_vm_instance(nova, vm_inst):
511 Deletes a VM instance
512 :param nova: the nova client
513 :param vm_inst: the snaps.domain.VmInst object
515 nova.servers.delete(vm_inst.id)
518 def __get_os_flavor(nova, flavor_id):
520 Returns to OpenStack flavor object by name
521 :param nova: the Nova client
522 :param flavor_id: the flavor's ID value
523 :return: the OpenStack Flavor object
526 return nova.flavors.get(flavor_id)
531 def get_flavor(nova, flavor):
533 Returns to OpenStack flavor object by name
534 :param nova: the Nova client
535 :param flavor: the SNAPS flavor domain object
536 :return: the SNAPS Flavor domain object
538 os_flavor = __get_os_flavor(nova, flavor.id)
541 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
542 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
543 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
544 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
546 return nova.flavors.get(flavor.id)
551 def get_flavor_by_id(nova, flavor_id):
553 Returns to OpenStack flavor object by name
554 :param nova: the Nova client
555 :param flavor_id: the flavor ID value
556 :return: the SNAPS Flavor domain object
558 os_flavor = __get_os_flavor(nova, flavor_id)
561 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
562 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
563 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
564 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
567 def __get_os_flavor_by_name(nova, name):
569 Returns to OpenStack flavor object by name
570 :param nova: the Nova client
571 :param name: the name of the flavor to query
572 :return: OpenStack flavor object
575 return nova.flavors.find(name=name)
580 def get_flavor_by_name(nova, name):
582 Returns a flavor by name
583 :param nova: the Nova client
584 :param name: the flavor name to return
585 :return: the SNAPS flavor domain object or None if not exists
587 os_flavor = __get_os_flavor_by_name(nova, name)
590 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
591 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
592 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
593 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
596 def create_flavor(nova, flavor_settings):
598 Creates and returns and OpenStack flavor object
599 :param nova: the Nova client
600 :param flavor_settings: the flavor settings
601 :return: the SNAPS flavor domain object
603 os_flavor = nova.flavors.create(
604 name=flavor_settings.name, flavorid=flavor_settings.flavor_id,
605 ram=flavor_settings.ram, vcpus=flavor_settings.vcpus,
606 disk=flavor_settings.disk, ephemeral=flavor_settings.ephemeral,
607 swap=flavor_settings.swap, rxtx_factor=flavor_settings.rxtx_factor,
608 is_public=flavor_settings.is_public)
610 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
611 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
612 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
613 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
616 def delete_flavor(nova, flavor):
619 :param nova: the Nova client
620 :param flavor: the SNAPS flavor domain object
622 nova.flavors.delete(flavor.id)
625 def set_flavor_keys(nova, flavor, metadata):
627 Sets metadata on the flavor
628 :param nova: the Nova client
629 :param flavor: the SNAPS flavor domain object
630 :param metadata: the metadata to set
632 os_flavor = __get_os_flavor(nova, flavor.id)
634 os_flavor.set_keys(metadata)
637 def get_flavor_keys(nova, flavor):
639 Sets metadata on the flavor
640 :param nova: the Nova client
641 :param flavor: the SNAPS flavor domain object
643 os_flavor = __get_os_flavor(nova, flavor.id)
645 return os_flavor.get_keys()
648 def add_security_group(nova, vm, security_group_name):
650 Adds a security group to an existing VM
651 :param nova: the nova client
652 :param vm: the OpenStack server object (VM) to alter
653 :param security_group_name: the name of the security group to add
656 nova.servers.add_security_group(str(vm.id), security_group_name)
657 except ClientException as e:
658 sec_grp_names = get_server_security_group_names(nova, vm)
659 if security_group_name in sec_grp_names:
660 logger.warn('Security group [%s] already added to VM [%s]',
661 security_group_name, vm.name)
664 logger.error('Unexpected error while adding security group [%s] - %s',
665 security_group_name, e)
669 def remove_security_group(nova, vm, security_group):
671 Removes a security group from an existing VM
672 :param nova: the nova client
673 :param vm: the OpenStack server object (VM) to alter
674 :param security_group: the SNAPS SecurityGroup domain object to add
676 nova.servers.remove_security_group(str(vm.id), security_group.name)
679 def get_compute_quotas(nova, project_id):
681 Returns a list of all available keypairs
682 :param nova: the Nova client
683 :param project_id: the project's ID of the quotas to lookup
684 :return: an object of type ComputeQuotas or None if not found
686 quotas = nova.quotas.get(tenant_id=project_id)
688 return ComputeQuotas(quotas)
691 def update_quotas(nova, project_id, compute_quotas):
693 Updates the compute quotas for a given project
694 :param nova: the Nova client
695 :param project_id: the project's ID that requires quota updates
696 :param compute_quotas: an object of type ComputeQuotas containing the
700 update_values = dict()
701 update_values['metadata_items'] = compute_quotas.metadata_items
702 update_values['cores'] = compute_quotas.cores
703 update_values['instances'] = compute_quotas.instances
704 update_values['injected_files'] = compute_quotas.injected_files
705 update_values['injected_file_content_bytes'] = (
706 compute_quotas.injected_file_content_bytes)
707 update_values['ram'] = compute_quotas.ram
708 update_values['fixed_ips'] = compute_quotas.fixed_ips
709 update_values['key_pairs'] = compute_quotas.key_pairs
711 return nova.quotas.update(project_id, **update_values)
714 def attach_volume(nova, neutron, server, volume, timeout=None):
716 Attaches a volume to a server
717 :param nova: the nova client
718 :param neutron: the neutron client
719 :param server: the VMInst domain object
720 :param volume: the Volume domain object
721 :param timeout: denotes the amount of time to block to determine if the
722 has been properly attached. When None, do not wait.
723 :return: the value from the nova call
725 nova.volumes.create_server_volume(server.id, volume.id)
728 start_time = time.time()
729 while time.time() < start_time + timeout:
730 vm = get_server_object_by_id(nova, neutron, server.id)
731 for vol_dict in vm.volume_ids:
732 if volume.id == vol_dict['id']:
737 return get_server_object_by_id(nova, neutron, server.id)
740 def detach_volume(nova, neutron, server, volume, timeout=None):
742 Attaches a volume to a server
743 :param nova: the nova client
744 :param neutron: the neutron client
745 :param server: the VMInst domain object
746 :param volume: the Volume domain object
747 :param timeout: denotes the amount of time to block to determine if the
748 has been properly detached. When None, do not wait.
749 :return: the value from the nova call
751 nova.volumes.delete_server_volume(server.id, volume.id)
754 start_time = time.time()
755 while time.time() < start_time + timeout:
756 vm = get_server_object_by_id(nova, neutron, server.id)
758 for vol_dict in vm.volume_ids:
759 if volume.id == vol_dict['id']:
767 return get_server_object_by_id(nova, neutron, server.id)
770 class RebootType(enum.Enum):
778 class NovaException(Exception):
780 Exception when calls to the Keystone client cannot be served properly
784 class ServerNotFoundError(Exception):
786 Exception when operations to a VM/Server is requested and the OpenStack
787 Server instance cannot be located