X-Git-Url: https://gerrit.opnfv.org/gerrit/gitweb?a=blobdiff_plain;f=snaps%2Fopenstack%2Futils%2Fnova_utils.py;h=38460c50dacf48ac2f6ebb66ac8e88cb681322e8;hb=390d31e3a6eaa0968474d19d71ec804d0c6c071a;hp=fe53211f0bf8944f0fdacb5e499d87e196bafe49;hpb=1342eb17df248ec75cc57e9c380a7753fc432194;p=snaps.git diff --git a/snaps/openstack/utils/nova_utils.py b/snaps/openstack/utils/nova_utils.py index fe53211..38460c5 100644 --- a/snaps/openstack/utils/nova_utils.py +++ b/snaps/openstack/utils/nova_utils.py @@ -15,13 +15,16 @@ import logging +import enum import os +import time from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa from novaclient.client import Client -from novaclient.exceptions import NotFound +from novaclient.exceptions import NotFound, ClientException +from snaps import file_utils from snaps.domain.flavor import Flavor from snaps.domain.keypair import Keypair from snaps.domain.project import ComputeQuotas @@ -32,89 +35,119 @@ __author__ = 'spisarski' logger = logging.getLogger('nova_utils') +POLL_INTERVAL = 3 + """ Utilities for basic OpenStack Nova API calls """ -def nova_client(os_creds): +def nova_client(os_creds, session=None): """ Instantiates and returns a client for communications with OpenStack's Nova server :param os_creds: The connection credentials to the OpenStack API + :param session: the keystone session object (optional) :return: the client object """ logger.debug('Retrieving Nova Client') + if not session: + session = keystone_utils.keystone_session(os_creds) + return Client(os_creds.compute_api_version, - session=keystone_utils.keystone_session(os_creds), + session=session, region_name=os_creds.region_name) -def create_server(nova, neutron, glance, instance_settings, image_settings, - keypair_settings=None): +def create_server(nova, keystone, neutron, glance, instance_config, + image_config, project_name, keypair_config=None): """ Creates a VM instance :param nova: the nova client (required) + :param keystone: the keystone client for retrieving projects (required) :param neutron: the neutron client for retrieving ports (required) :param glance: the glance client (required) - :param instance_settings: the VM instance settings object (required) - :param image_settings: the VM's image settings object (required) - :param keypair_settings: the VM's keypair settings object (optional) + :param instance_config: the VMInstConfig object (required) + :param image_config: the VM's ImageConfig object (required) + :param project_name: the associated project name (required) + :param keypair_config: the VM's KeypairConfig object (optional) :return: a snaps.domain.VmInst object """ ports = list() - for port_setting in instance_settings.port_settings: - ports.append(neutron_utils.get_port( - neutron, port_settings=port_setting)) + for port_setting in instance_config.port_settings: + port = neutron_utils.get_port( + neutron, keystone, port_settings=port_setting, + project_name=project_name) + if port: + ports.append(port) + else: + raise Exception('Cannot find port named - ' + port_setting.name) nics = [] for port in ports: kv = dict() kv['port-id'] = port.id nics.append(kv) - logger.info('Creating VM with name - ' + instance_settings.name) + logger.info('Creating VM with name - ' + instance_config.name) keypair_name = None - if keypair_settings: - keypair_name = keypair_settings.name + if keypair_config: + keypair_name = keypair_config.name - flavor = get_flavor_by_name(nova, instance_settings.flavor) + flavor = get_flavor_by_name(nova, instance_config.flavor) if not flavor: raise NovaException( - 'Flavor not found with name - %s', instance_settings.flavor) + 'Flavor not found with name - %s', instance_config.flavor) - image = glance_utils.get_image(glance, image_settings=image_settings) + image = glance_utils.get_image(glance, image_settings=image_config) if image: - args = {'name': instance_settings.name, + userdata = None + if instance_config.userdata: + if isinstance(instance_config.userdata, str): + userdata = instance_config.userdata + '\n' + elif (isinstance(instance_config.userdata, dict) and + 'script_file' in instance_config.userdata): + try: + userdata = file_utils.read_file( + instance_config.userdata['script_file']) + except Exception as e: + logger.warn('error reading userdata file %s - %s', + instance_config.userdata, e) + args = {'name': instance_config.name, 'flavor': flavor, 'image': image, 'nics': nics, 'key_name': keypair_name, 'security_groups': - instance_settings.security_group_names, - 'userdata': instance_settings.userdata} + instance_config.security_group_names, + 'userdata': userdata} - if instance_settings.availability_zone: - args['availability_zone'] = instance_settings.availability_zone + if instance_config.availability_zone: + args['availability_zone'] = instance_config.availability_zone server = nova.servers.create(**args) - return __map_os_server_obj_to_vm_inst(server) + return __map_os_server_obj_to_vm_inst( + neutron, keystone, server, project_name) else: raise NovaException( 'Cannot create instance, image cannot be located with name %s', - image_settings.name) + image_config.name) -def get_server(nova, vm_inst_settings=None, server_name=None): +def get_server(nova, neutron, keystone, vm_inst_settings=None, + server_name=None, project_id=None): """ Returns a VmInst object for the first server instance found. :param nova: the Nova client - :param vm_inst_settings: the VmInstanceSettings object from which to build + :param neutron: the Neutron client + :param keystone: the Keystone client + :param vm_inst_settings: the VmInstanceConfig object from which to build the query if not None :param server_name: the server with this name to return if vm_inst_settings is not None + :param project_id: the assocaited project ID :return: a snaps.domain.VmInst object or None if not found """ search_opts = dict() @@ -125,13 +158,39 @@ def get_server(nova, vm_inst_settings=None, server_name=None): servers = nova.servers.list(search_opts=search_opts) for server in servers: - return __map_os_server_obj_to_vm_inst(server) + return __map_os_server_obj_to_vm_inst( + neutron, keystone, server, project_id) -def __map_os_server_obj_to_vm_inst(os_server): +def get_server_connection(nova, vm_inst_settings=None, server_name=None): + """ + Returns a VmInst object for the first server instance found. + :param nova: the Nova client + :param vm_inst_settings: the VmInstanceConfig object from which to build + the query if not None + :param server_name: the server with this name to return if vm_inst_settings + is not None + :return: a snaps.domain.VmInst object or None if not found + """ + search_opts = dict() + if vm_inst_settings: + search_opts['name'] = vm_inst_settings.name + elif server_name: + search_opts['name'] = server_name + + servers = nova.servers.list(search_opts=search_opts) + for server in servers: + return server.links[0] + + +def __map_os_server_obj_to_vm_inst(neutron, keystone, os_server, + project_name=None): """ Returns a VmInst object for an OpenStack Server object + :param neutron: the Neutron client + :param keystone: the Keystone client :param os_server: the OpenStack server object + :param project_name: the associated project name :return: an equivalent SNAPS-OO VmInst domain object """ sec_grp_names = list() @@ -141,11 +200,27 @@ def __map_os_server_obj_to_vm_inst(os_server): if sec_group.get('name'): sec_grp_names.append(sec_group.get('name')) + out_ports = list() + if len(os_server.networks) > 0: + for net_name, ips in os_server.networks.items(): + network = neutron_utils.get_network( + neutron, keystone, network_name=net_name, + project_name=project_name) + ports = neutron_utils.get_ports(neutron, network, ips) + for port in ports: + out_ports.append(port) + + volumes = None + if hasattr(os_server, 'os-extended-volumes:volumes_attached'): + volumes = getattr(os_server, 'os-extended-volumes:volumes_attached') + return VmInst( name=os_server.name, inst_id=os_server.id, image_id=os_server.image['id'], flavor_id=os_server.flavor['id'], - networks=os_server.networks, keypair_name=os_server.key_name, - sec_grp_names=sec_grp_names) + ports=out_ports, keypair_name=os_server.key_name, + sec_grp_names=sec_grp_names, volume_ids=volumes, + compute_host=os_server._info.get('OS-EXT-SRV-ATTR:host'), + availability_zone=os_server._info.get('OS-EXT-AZ:availability_zone')) def __get_latest_server_os_object(nova, server): @@ -194,26 +269,35 @@ def get_server_console_output(nova, server): return None -def get_latest_server_object(nova, server): +def get_latest_server_object(nova, neutron, keystone, server, project_name): """ Returns a server with a given id :param nova: the Nova client + :param neutron: the Neutron client + :param keystone: the Keystone client :param server: the old server object + :param project_name: the associated project name :return: the list of servers or None if not found """ server = __get_latest_server_os_object(nova, server) - return __map_os_server_obj_to_vm_inst(server) + return __map_os_server_obj_to_vm_inst( + neutron, keystone, server, project_name) -def get_server_object_by_id(nova, server_id): +def get_server_object_by_id(nova, neutron, keystone, server_id, + project_name=None): """ Returns a server with a given id :param nova: the Nova client + :param neutron: the Neutron client + :param keystone: the Keystone client :param server_id: the server's id + :param project_name: the associated project name :return: an SNAPS-OO VmInst object or None if not found """ server = __get_latest_server_os_object_by_id(nova, server_id) - return __map_os_server_obj_to_vm_inst(server) + return __map_os_server_obj_to_vm_inst( + neutron, keystone, server, project_name) def get_server_security_group_names(nova, server): @@ -243,6 +327,22 @@ def get_server_info(nova, server): return None +def reboot_server(nova, server, reboot_type=None): + """ + Returns a dictionary of a VMs info as returned by OpenStack + :param nova: the Nova client + :param server: the old server object + :param reboot_type: Acceptable values 'SOFT', 'HARD' + (api uses SOFT as the default) + :return: a dict of the info if VM exists else None + """ + vm = __get_latest_server_os_object(nova, server) + if vm: + vm.reboot(reboot_type=reboot_type.value) + else: + raise ServerNotFoundError('Cannot locate server') + + def create_keys(key_size=2048): """ Generates public and private keys @@ -264,6 +364,58 @@ def public_key_openssh(keys): serialization.PublicFormat.OpenSSH) +def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None): + """ + Saves the generated RSA generated keys to the filesystem + :param keys: the keys to save generated by cryptography + :param pub_file_path: the path to the public keys + :param priv_file_path: the path to the private keys + """ + if keys: + if pub_file_path: + # To support '~' + pub_expand_file = os.path.expanduser(pub_file_path) + pub_dir = os.path.dirname(pub_expand_file) + + if not os.path.isdir(pub_dir): + os.mkdir(pub_dir) + + public_handle = None + try: + public_handle = open(pub_expand_file, 'wb') + public_bytes = keys.public_key().public_bytes( + serialization.Encoding.OpenSSH, + serialization.PublicFormat.OpenSSH) + public_handle.write(public_bytes) + finally: + if public_handle: + public_handle.close() + + os.chmod(pub_expand_file, 0o600) + logger.info("Saved public key to - " + pub_expand_file) + if priv_file_path: + # To support '~' + priv_expand_file = os.path.expanduser(priv_file_path) + priv_dir = os.path.dirname(priv_expand_file) + if not os.path.isdir(priv_dir): + os.mkdir(priv_dir) + + private_handle = None + try: + private_handle = open(priv_expand_file, 'wb') + private_handle.write( + keys.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption())) + finally: + if private_handle: + private_handle.close() + + os.chmod(priv_expand_file, 0o600) + logger.info("Saved private key to - " + priv_expand_file) + + def upload_keypair_file(nova, name, file_path): """ Uploads a public key from a file @@ -328,6 +480,18 @@ def get_keypair_by_name(nova, name): return None +def get_keypair_by_id(nova, kp_id): + """ + Returns a list of all available keypairs + :param nova: the Nova client + :param kp_id: the ID of the keypair to return + :return: the keypair object + """ + keypair = nova.keypairs.get(kp_id) + return Keypair(name=keypair.name, kp_id=keypair.id, + public_key=keypair.public_key) + + def delete_keypair(nova, key): """ Deletes a keypair object from OpenStack @@ -356,6 +520,21 @@ def get_availability_zone_hosts(nova, zone_name='nova'): return out +def get_hypervisor_hosts(nova): + """ + Returns the host names of all nova nodes with active hypervisors + :param nova: the Nova client + :return: a list of hypervisor host names + """ + out = list() + hypervisors = nova.hypervisors.list() + for hypervisor in hypervisors: + if hypervisor.state == "up": + out.append(hypervisor.hypervisor_hostname) + + return out + + def delete_vm_instance(nova, vm_inst): """ Deletes a VM instance @@ -502,7 +681,18 @@ def add_security_group(nova, vm, security_group_name): :param vm: the OpenStack server object (VM) to alter :param security_group_name: the name of the security group to add """ - nova.servers.add_security_group(str(vm.id), security_group_name) + try: + nova.servers.add_security_group(str(vm.id), security_group_name) + except ClientException as e: + sec_grp_names = get_server_security_group_names(nova, vm) + if security_group_name in sec_grp_names: + logger.warn('Security group [%s] already added to VM [%s]', + security_group_name, vm.name) + return + + logger.error('Unexpected error while adding security group [%s] - %s', + security_group_name, e) + raise def remove_security_group(nova, vm, security_group): @@ -515,18 +705,6 @@ def remove_security_group(nova, vm, security_group): nova.servers.remove_security_group(str(vm.id), security_group.name) -def add_floating_ip_to_server(nova, vm, floating_ip, ip_addr): - """ - Adds a floating IP to a server instance - :param nova: the nova client - :param vm: VmInst domain object - :param floating_ip: FloatingIp domain object - :param ip_addr: the IP to which to bind the floating IP to - """ - vm = __get_latest_server_os_object(nova, vm) - vm.add_floating_ip(floating_ip.ip, ip_addr) - - def get_compute_quotas(nova, project_id): """ Returns a list of all available keypairs @@ -553,7 +731,8 @@ def update_quotas(nova, project_id, compute_quotas): update_values['cores'] = compute_quotas.cores update_values['instances'] = compute_quotas.instances update_values['injected_files'] = compute_quotas.injected_files - update_values['injected_file_content_bytes'] = compute_quotas.injected_file_content_bytes + update_values['injected_file_content_bytes'] = ( + compute_quotas.injected_file_content_bytes) update_values['ram'] = compute_quotas.ram update_values['fixed_ips'] = compute_quotas.fixed_ips update_values['key_pairs'] = compute_quotas.key_pairs @@ -561,7 +740,91 @@ def update_quotas(nova, project_id, compute_quotas): return nova.quotas.update(project_id, **update_values) +def attach_volume(nova, neutron, keystone, server, volume, project_name, + timeout=120): + """ + Attaches a volume to a server. When the timeout parameter is used, a VmInst + object with the proper volume updates is returned unless it has not been + updated in the allotted amount of time then an Exception will be raised. + :param nova: the nova client + :param neutron: the neutron client + :param keystone: the neutron client + :param server: the VMInst domain object + :param volume: the Volume domain object + :param project_name: the associated project name + :param timeout: denotes the amount of time to block to determine if the + has been properly attached. + :return: updated VmInst object + """ + nova.volumes.create_server_volume(server.id, volume.id) + + start_time = time.time() + while time.time() < start_time + timeout: + vm = get_server_object_by_id( + nova, neutron, keystone, server.id, project_name) + for vol_dict in vm.volume_ids: + if volume.id == vol_dict['id']: + return vm + time.sleep(POLL_INTERVAL) + + raise NovaException( + 'Attach failed on volume - {} and server - {}'.format( + volume.id, server.id)) + + +def detach_volume(nova, neutron, keystone, server, volume, project_name, + timeout=120): + """ + Detaches a volume to a server. When the timeout parameter is used, a VmInst + object with the proper volume updates is returned unless it has not been + updated in the allotted amount of time then an Exception will be raised. + :param nova: the nova client + :param neutron: the neutron client + :param keystone: the keystone client + :param server: the VMInst domain object + :param volume: the Volume domain object + :param project_name: the associated project name + :param timeout: denotes the amount of time to block to determine if the + has been properly detached. + :return: updated VmInst object + """ + nova.volumes.delete_server_volume(server.id, volume.id) + + start_time = time.time() + while time.time() < start_time + timeout: + vm = get_server_object_by_id( + nova, neutron, keystone, server.id, project_name) + if len(vm.volume_ids) == 0: + return vm + else: + ids = list() + for vol_dict in vm.volume_ids: + ids.append(vol_dict['id']) + if volume.id not in ids: + return vm + time.sleep(POLL_INTERVAL) + + raise NovaException( + 'Detach failed on volume - {} server - {}'.format( + volume.id, server.id)) + + +class RebootType(enum.Enum): + """ + A rule's direction + """ + soft = 'SOFT' + hard = 'HARD' + + class NovaException(Exception): """ Exception when calls to the Keystone client cannot be served properly """ + + +class ServerNotFoundError(Exception): + """ + Exception when operations to a VM/Server is requested and the OpenStack + Server instance cannot be located + """