Added method to OpenStackHeatStack to return OpenStackKeypair objects.
[snaps.git] / snaps / openstack / utils / nova_utils.py
index bee526c..0820289 100644 (file)
 import logging
 
 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 snaps import file_utils
 from snaps.domain.flavor import Flavor
 from snaps.domain.keypair import Keypair
+from snaps.domain.project import ComputeQuotas
 from snaps.domain.vm_inst import VmInst
 from snaps.openstack.utils import keystone_utils, glance_utils, neutron_utils
 
@@ -45,7 +48,8 @@ def nova_client(os_creds):
     """
     logger.debug('Retrieving Nova Client')
     return Client(os_creds.compute_api_version,
-                  session=keystone_utils.keystone_session(os_creds))
+                  session=keystone_utils.keystone_session(os_creds),
+                  region_name=os_creds.region_name)
 
 
 def create_server(nova, neutron, glance, instance_settings, image_settings,
@@ -64,8 +68,8 @@ def create_server(nova, neutron, glance, instance_settings, image_settings,
     ports = list()
 
     for port_setting in instance_settings.port_settings:
-        ports.append(neutron_utils.get_port_by_name(
-            neutron, port_setting.name))
+        ports.append(neutron_utils.get_port(
+            neutron, port_settings=port_setting))
     nics = []
     for port in ports:
         kv = dict()
@@ -79,12 +83,23 @@ def create_server(nova, neutron, glance, instance_settings, image_settings,
 
     flavor = get_flavor_by_name(nova, instance_settings.flavor)
     if not flavor:
-        raise Exception(
-            'Flavor not found with name - %s',
-            instance_settings.flavor)
+        raise NovaException(
+            'Flavor not found with name - %s', instance_settings.flavor)
 
-    image = glance_utils.get_image(glance, image_settings.name)
+    image = glance_utils.get_image(glance, image_settings=image_settings)
     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):
+                try:
+                    userdata = file_utils.read_file(
+                        instance_settings.userdata['script_file'])
+                except Exception as e:
+                    logger.warn('error reading userdata file %s - %s',
+                                instance_settings.userdata, e)
         args = {'name': instance_settings.name,
                 'flavor': flavor,
                 'image': image,
@@ -92,31 +107,84 @@ def create_server(nova, neutron, glance, instance_settings, image_settings,
                 'key_name': keypair_name,
                 'security_groups':
                     instance_settings.security_group_names,
-                'userdata': instance_settings.userdata,
-                'availability_zone':
-                    instance_settings.availability_zone}
+                'userdata': userdata}
+
+        if instance_settings.availability_zone:
+            args['availability_zone'] = instance_settings.availability_zone
+
         server = nova.servers.create(**args)
-        return VmInst(name=server.name, inst_id=server.id,
-                      networks=server.networks)
+
+        return __map_os_server_obj_to_vm_inst(server)
     else:
-        raise Exception(
+        raise NovaException(
             'Cannot create instance, image cannot be located with name %s',
             image_settings.name)
 
 
-def get_servers_by_name(nova, name):
+def get_server(nova, vm_inst_settings=None, server_name=None):
     """
-    Returns a list of servers with a given name
+    Returns a VmInst object for the first server instance found.
     :param nova: the Nova client
-    :param name: the server name
-    :return: the list of snaps.domain.VmInst objects
+    :param vm_inst_settings: the VmInstanceSettings 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 __map_os_server_obj_to_vm_inst(server)
+
+
+def get_server_connection(nova, vm_inst_settings=None, server_name=None):
     """
-    out = list()
-    servers = nova.servers.list(search_opts={'name': name})
+    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
+                             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:
-        out.append(VmInst(name=server.name, inst_id=server.id,
-                          networks=server.networks))
-    return out
+        return server.links[0]
+
+
+def __map_os_server_obj_to_vm_inst(os_server):
+    """
+    Returns a VmInst object for an OpenStack Server object
+    :param os_server: the OpenStack server object
+    :return: an equivalent SNAPS-OO VmInst domain object
+    """
+    sec_grp_names = list()
+    # VM must be active for 'security_groups' attr to be initialized
+    if hasattr(os_server, 'security_groups'):
+        for sec_group in os_server.security_groups:
+            if sec_group.get('name'):
+                sec_grp_names.append(sec_group.get('name'))
+
+    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, volume_ids=volumes)
 
 
 def __get_latest_server_os_object(nova, server):
@@ -126,7 +194,17 @@ def __get_latest_server_os_object(nova, server):
     :param server: the domain VmInst object
     :return: the list of servers or None if not found
     """
-    return nova.servers.get(server.id)
+    return __get_latest_server_os_object_by_id(nova, server.id)
+
+
+def __get_latest_server_os_object_by_id(nova, server_id):
+    """
+    Returns a server with a given id
+    :param nova: the Nova client
+    :param server_id: the server's ID
+    :return: the list of servers or None if not found
+    """
+    return nova.servers.get(server_id)
 
 
 def get_server_status(nova, server):
@@ -163,8 +241,18 @@ def get_latest_server_object(nova, server):
     :return: the list of servers or None if not found
     """
     server = __get_latest_server_os_object(nova, server)
-    return VmInst(name=server.name, inst_id=server.id,
-                  networks=server.networks)
+    return __map_os_server_obj_to_vm_inst(server)
+
+
+def get_server_object_by_id(nova, server_id):
+    """
+    Returns a server with a given id
+    :param nova: the Nova client
+    :param server_id: the server's id
+    :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)
 
 
 def get_server_security_group_names(nova, server):
@@ -230,13 +318,19 @@ def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None):
 
             if not os.path.isdir(pub_dir):
                 os.mkdir(pub_dir)
-            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)
-            public_handle.close()
-            os.chmod(pub_expand_file, 0o400)
+
+            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 '~'
@@ -244,14 +338,20 @@ def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None):
             priv_dir = os.path.dirname(priv_expand_file)
             if not os.path.isdir(priv_dir):
                 os.mkdir(priv_dir)
-            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()))
-            private_handle.close()
-            os.chmod(priv_expand_file, 0o400)
+
+            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)
 
 
@@ -263,9 +363,14 @@ def upload_keypair_file(nova, name, file_path):
     :param file_path: the path to the public key file
     :return: the keypair object
     """
-    with open(os.path.expanduser(file_path), 'rb') as fpubkey:
-        logger.info('Saving keypair to - ' + file_path)
-        return upload_keypair(nova, name, fpubkey.read())
+    fpubkey = None
+    try:
+        with open(os.path.expanduser(file_path), 'rb') as fpubkey:
+            logger.info('Saving keypair to - ' + file_path)
+            return upload_keypair(nova, name, fpubkey.read())
+    finally:
+        if fpubkey:
+            fpubkey.close()
 
 
 def upload_keypair(nova, name, key):
@@ -278,7 +383,8 @@ def upload_keypair(nova, name, key):
     """
     logger.info('Creating keypair with name - ' + name)
     os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
-    return Keypair(name=os_kp.name, id=os_kp.id, public_key=os_kp.public_key)
+    return Keypair(name=os_kp.name, kp_id=os_kp.id,
+                   public_key=os_kp.public_key, fingerprint=os_kp.fingerprint)
 
 
 def keypair_exists(nova, keypair_obj):
@@ -290,7 +396,7 @@ def keypair_exists(nova, keypair_obj):
     """
     try:
         os_kp = nova.keypairs.get(keypair_obj)
-        return Keypair(name=os_kp.name, id=os_kp.id,
+        return Keypair(name=os_kp.name, kp_id=os_kp.id,
                        public_key=os_kp.public_key)
     except:
         return None
@@ -307,12 +413,24 @@ def get_keypair_by_name(nova, name):
 
     for keypair in keypairs:
         if keypair.name == name:
-            return Keypair(name=keypair.name, id=keypair.id,
+            return Keypair(name=keypair.name, kp_id=keypair.id,
                            public_key=keypair.public_key)
 
     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
@@ -323,16 +441,17 @@ def delete_keypair(nova, key):
     nova.keypairs.delete(key.id)
 
 
-def get_nova_availability_zones(nova):
+def get_availability_zone_hosts(nova, zone_name='nova'):
     """
     Returns the names of all nova active compute servers
     :param nova: the Nova client
+    :param zone_name: the Nova client
     :return: a list of compute server names
     """
     out = list()
     zones = nova.availability_zones.list()
     for zone in zones:
-        if zone.zoneName == 'nova':
+        if zone.zoneName == zone_name and zone.hosts:
             for key, host in zone.hosts.items():
                 if host['nova-compute']['available']:
                     out.append(zone.zoneName + ':' + key)
@@ -349,15 +468,15 @@ def delete_vm_instance(nova, vm_inst):
     nova.servers.delete(vm_inst.id)
 
 
-def __get_os_flavor(nova, flavor):
+def __get_os_flavor(nova, flavor_id):
     """
     Returns to OpenStack flavor object by name
     :param nova: the Nova client
-    :param flavor: the SNAPS flavor domain object
+    :param flavor_id: the flavor's ID value
     :return: the OpenStack Flavor object
     """
     try:
-        return nova.flavors.get(flavor.id)
+        return nova.flavors.get(flavor_id)
     except NotFound:
         return None
 
@@ -369,7 +488,7 @@ def get_flavor(nova, flavor):
     :param flavor: the SNAPS flavor domain object
     :return: the SNAPS Flavor domain object
     """
-    os_flavor = __get_os_flavor(nova, flavor)
+    os_flavor = __get_os_flavor(nova, flavor.id)
     if os_flavor:
         return Flavor(
             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
@@ -382,6 +501,22 @@ def get_flavor(nova, flavor):
         return None
 
 
+def get_flavor_by_id(nova, flavor_id):
+    """
+    Returns to OpenStack flavor object by name
+    :param nova: the Nova client
+    :param flavor_id: the flavor ID value
+    :return: the SNAPS Flavor domain object
+    """
+    os_flavor = __get_os_flavor(nova, flavor_id)
+    if os_flavor:
+        return Flavor(
+            name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
+            disk=os_flavor.disk, vcpus=os_flavor.vcpus,
+            ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
+            rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
+
+
 def __get_os_flavor_by_name(nova, name):
     """
     Returns to OpenStack flavor object by name
@@ -447,11 +582,22 @@ def set_flavor_keys(nova, flavor, metadata):
     :param flavor: the SNAPS flavor domain object
     :param metadata: the metadata to set
     """
-    os_flavor = __get_os_flavor(nova, flavor)
+    os_flavor = __get_os_flavor(nova, flavor.id)
     if os_flavor:
         os_flavor.set_keys(metadata)
 
 
+def get_flavor_keys(nova, flavor):
+    """
+    Sets metadata on the flavor
+    :param nova: the Nova client
+    :param flavor: the SNAPS flavor domain object
+    """
+    os_flavor = __get_os_flavor(nova, flavor.id)
+    if os_flavor:
+        return os_flavor.get_keys()
+
+
 def add_security_group(nova, vm, security_group_name):
     """
     Adds a security group to an existing VM
@@ -482,3 +628,98 @@ def add_floating_ip_to_server(nova, vm, floating_ip, ip_addr):
     """
     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
+    :param nova: the Nova client
+    :param project_id: the project's ID of the quotas to lookup
+    :return: an object of type ComputeQuotas or None if not found
+    """
+    quotas = nova.quotas.get(tenant_id=project_id)
+    if quotas:
+        return ComputeQuotas(quotas)
+
+
+def update_quotas(nova, project_id, compute_quotas):
+    """
+    Updates the compute quotas for a given project
+    :param nova: the Nova client
+    :param project_id: the project's ID that requires quota updates
+    :param compute_quotas: an object of type ComputeQuotas containing the
+                           values to update
+    :return:
+    """
+    update_values = dict()
+    update_values['metadata_items'] = compute_quotas.metadata_items
+    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['ram'] = compute_quotas.ram
+    update_values['fixed_ips'] = compute_quotas.fixed_ips
+    update_values['key_pairs'] = compute_quotas.key_pairs
+
+    return nova.quotas.update(project_id, **update_values)
+
+
+def attach_volume(nova, server, volume, timeout=None):
+    """
+    Attaches a volume to a server
+    :param nova: the nova client
+    :param server: the VMInst domain object
+    :param volume: the Volume domain object
+    :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
+    """
+    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
+
+        return None
+    else:
+        return get_server_object_by_id(nova, server.id)
+
+
+def detach_volume(nova, server, volume, timeout=None):
+    """
+    Attaches a volume to a server
+    :param nova: the nova client
+    :param server: the VMInst domain object
+    :param volume: the Volume domain object
+    :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
+    """
+    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
+            for vol_dict in vm.volume_ids:
+                if volume.id == vol_dict['id']:
+                    found = True
+
+            if not found:
+                return vm
+
+        return None
+    else:
+        return get_server_object_by_id(nova, server.id)
+
+
+class NovaException(Exception):
+    """
+    Exception when calls to the Keystone client cannot be served properly
+    """