Adds Stack Update
[snaps.git] / snaps / openstack / utils / nova_utils.py
index 42b7356..005b56f 100644 (file)
 
 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
@@ -34,101 +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:
         userdata = None
-        if instance_settings.userdata:
-            if isinstance(instance_settings.userdata, str):
-                userdata = instance_settings.userdata + '\n'
-            elif (isinstance(instance_settings.userdata, dict) and
-                  'script_file' in instance_settings.userdata):
+        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_settings.userdata['script_file'])
+                        instance_config.userdata['script_file'])
                 except Exception as e:
                     logger.warn('error reading userdata file %s - %s',
-                                instance_settings.userdata, e)
-        args = {'name': instance_settings.name,
+                                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,
+                    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()
@@ -139,14 +158,15 @@ 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 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 VmInstanceSettings object from which to build
+    :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
@@ -163,10 +183,14 @@ def get_server_connection(nova, vm_inst_settings=None, server_name=None):
         return server.links[0]
 
 
-def __map_os_server_obj_to_vm_inst(os_server):
+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()
@@ -176,6 +200,21 @@ 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)
+            if network:
+                ports = neutron_utils.get_ports(neutron, network, ips)
+                for port in ports:
+                    out_ports.append(port)
+            else:
+                raise NovaException(
+                    'Unable to locate network in project {} with '
+                    'name {}'.format(project_name, net_name))
+
     volumes = None
     if hasattr(os_server, 'os-extended-volumes:volumes_attached'):
         volumes = getattr(os_server, 'os-extended-volumes:volumes_attached')
@@ -183,8 +222,10 @@ def __map_os_server_obj_to_vm_inst(os_server):
     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, volume_ids=volumes)
+        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):
@@ -233,26 +274,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):
@@ -264,8 +314,9 @@ def get_server_security_group_names(nova, server):
     """
     out = list()
     os_vm_inst = __get_latest_server_os_object(nova, server)
-    for sec_grp_dict in os_vm_inst.security_groups:
-        out.append(sec_grp_dict['name'])
+    if hasattr(os_vm_inst, 'security_groups'):
+        for sec_grp_dict in os_vm_inst.security_groups:
+            out.append(sec_grp_dict['name'])
     return out
 
 
@@ -282,6 +333,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
@@ -419,6 +486,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
@@ -447,6 +526,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
@@ -593,7 +687,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):
@@ -606,18 +711,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
@@ -653,61 +746,91 @@ def update_quotas(nova, project_id, compute_quotas):
     return nova.quotas.update(project_id, **update_values)
 
 
-def attach_volume(nova, server, volume, timeout=None):
+def attach_volume(nova, neutron, keystone, server, volume, project_name,
+                  timeout=120):
     """
-    Attaches a volume to a server
+    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. When None, do not wait.
-    :return: the value from the nova call
+                    has been properly attached.
+    :return: updated VmInst object
     """
     nova.volumes.create_server_volume(server.id, volume.id)
 
-    if timeout:
-        start_time = time.time()
-        while time.time() < start_time + timeout:
-            vm = get_server_object_by_id(nova, server.id)
-            for vol_dict in vm.volume_ids:
-                if volume.id == vol_dict['id']:
-                    return vm
+    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)
 
-        return None
-    else:
-        return get_server_object_by_id(nova, server.id)
+    raise NovaException(
+        'Attach failed on volume - {} and server - {}'.format(
+            volume.id, server.id))
 
 
-def detach_volume(nova, server, volume, timeout=None):
+def detach_volume(nova, neutron, keystone, server, volume, project_name,
+                  timeout=120):
     """
-    Attaches a volume to a server
+    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. When None, do not wait.
-    :return: the value from the nova call
+                    has been properly detached.
+    :return: updated VmInst object
     """
     nova.volumes.delete_server_volume(server.id, volume.id)
 
-    if timeout:
-        start_time = time.time()
-        while time.time() < start_time + timeout:
-            vm = get_server_object_by_id(nova, server.id)
-            found = False
+    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:
-                if volume.id == vol_dict['id']:
-                    found = True
-
-            if not found:
+                ids.append(vol_dict['id'])
+            if volume.id not in ids:
                 return vm
+        time.sleep(POLL_INTERVAL)
 
-        return None
-    else:
-        return get_server_object_by_id(nova, server.id)
+    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
+    """