Refactoring of VolumeSettings to extend VolumeConfig
[snaps.git] / snaps / openstack / create_stack.py
index 1492829..cd65a27 100644 (file)
@@ -17,66 +17,99 @@ import logging
 import time
 
 from heatclient.exc import HTTPNotFound
 import time
 
 from heatclient.exc import HTTPNotFound
-from snaps.openstack.utils import heat_utils
+
+import snaps
+from snaps.config.stack import StackConfig
+from snaps.openstack.create_flavor import OpenStackFlavor
+from snaps.openstack.create_instance import OpenStackVmInstance
+from snaps.openstack.create_keypairs import OpenStackKeypair
+from snaps.openstack.create_security_group import OpenStackSecurityGroup
+from snaps.openstack.create_router import OpenStackRouter
+from snaps.openstack.create_volume import OpenStackVolume
+from snaps.openstack.create_volume_type import OpenStackVolumeType
+from snaps.openstack.openstack_creator import OpenStackCloudObject
+from snaps.openstack.utils import (
+    nova_utils, settings_utils, glance_utils, cinder_utils)
+
+from snaps.openstack.create_network import OpenStackNetwork
+from snaps.openstack.utils import heat_utils, neutron_utils
 
 __author__ = 'spisarski'
 
 logger = logging.getLogger('create_stack')
 
 
 __author__ = 'spisarski'
 
 logger = logging.getLogger('create_stack')
 
-STACK_COMPLETE_TIMEOUT = 1200
-POLL_INTERVAL = 3
-STATUS_CREATE_COMPLETE = 'CREATE_COMPLETE'
-STATUS_DELETE_COMPLETE = 'DELETE_COMPLETE'
-
 
 
-class OpenStackHeatStack:
+class OpenStackHeatStack(OpenStackCloudObject, object):
     """
     """
-    Class responsible for creating an heat stack in OpenStack
+    Class responsible for managing a heat stack in OpenStack
     """
 
     """
 
-    def __init__(self, os_creds, stack_settings):
+    def __init__(self, os_creds, stack_settings, image_settings=None,
+                 keypair_settings=None):
         """
         Constructor
         :param os_creds: The OpenStack connection credentials
         :param stack_settings: The stack settings
         """
         Constructor
         :param os_creds: The OpenStack connection credentials
         :param stack_settings: The stack settings
+        :param image_settings: A list of ImageConfig objects that were used
+                               for spawning this stack
+        :param keypair_settings: A list of KeypairConfig objects that were
+                                 used for spawning this stack
         :return:
         """
         :return:
         """
-        self.__os_creds = os_creds
+        super(self.__class__, self).__init__(os_creds)
+
         self.stack_settings = stack_settings
         self.stack_settings = stack_settings
+
+        if image_settings:
+            self.image_settings = image_settings
+        else:
+            self.image_settings = None
+
+        if image_settings:
+            self.keypair_settings = keypair_settings
+        else:
+            self.keypair_settings = None
+
         self.__stack = None
         self.__heat_cli = None
 
         self.__stack = None
         self.__heat_cli = None
 
-    def create(self, cleanup=False):
+    def initialize(self):
+        """
+        Loads the existing heat stack
+        :return: The Stack domain object or None
+        """
+        self.__heat_cli = heat_utils.heat_client(self._os_creds)
+        self.__stack = heat_utils.get_stack(
+            self.__heat_cli, stack_settings=self.stack_settings)
+        if self.__stack:
+            logger.info('Found stack with name - ' + self.stack_settings.name)
+            return self.__stack
+
+    def create(self):
         """
         Creates the heat stack in OpenStack if it does not already exist and
         returns the domain Stack object
         """
         Creates the heat stack in OpenStack if it does not already exist and
         returns the domain Stack object
-        :param cleanup: Denotes whether or not this is being called for cleanup
-        :return: The OpenStack Stack object
+        :return: The Stack domain object or None
         """
         """
-        self.__heat_cli = heat_utils.heat_client(self.__os_creds)
-        self.__stack = heat_utils.get_stack_by_name(self.__heat_cli,
-                                                    self.stack_settings.name)
+        self.initialize()
+
         if self.__stack:
         if self.__stack:
-            logger.info('Found stack with name - ' + self.stack_settings.name)
+            logger.info('Found stack with name - %s', self.stack_settings.name)
             return self.__stack
             return self.__stack
-        elif not cleanup:
+        else:
             self.__stack = heat_utils.create_stack(self.__heat_cli,
                                                    self.stack_settings)
             logger.info(
             self.__stack = heat_utils.create_stack(self.__heat_cli,
                                                    self.stack_settings)
             logger.info(
-                'Created stack with name - ' + self.stack_settings.name)
+                'Created stack with name - %s', self.stack_settings.name)
             if self.__stack and self.stack_complete(block=True):
             if self.__stack and self.stack_complete(block=True):
-                logger.info(
-                    'Stack is now active with name - ' +
-                    self.stack_settings.name)
+                logger.info('Stack is now active with name - %s',
+                            self.stack_settings.name)
                 return self.__stack
             else:
                 return self.__stack
             else:
-                raise StackCreationError(
-                    'Stack was not created or activated in the alloted amount '
-                    'of time')
-        else:
-            logger.info('Did not create stack due to cleanup mode')
-
-        return self.__stack
+                status = heat_utils.get_stack_status_reason(self.__heat_cli,
+                                                            self.__stack.id)
+                logger.error('ERROR: STACK CREATION FAILED: %s', status)
+                raise StackCreationError('Failure while creating stack')
 
     def clean(self):
         """
 
     def clean(self):
         """
@@ -85,11 +118,39 @@ class OpenStackHeatStack:
         """
         if self.__stack:
             try:
         """
         if self.__stack:
             try:
+                logger.info('Deleting stack - %s', self.__stack.name)
                 heat_utils.delete_stack(self.__heat_cli, self.__stack)
                 heat_utils.delete_stack(self.__heat_cli, self.__stack)
+
+                try:
+                    self.stack_deleted(block=True)
+                except StackError as e:
+                    # Stack deletion seems to fail quite a bit
+                    logger.warn('Stack did not delete properly - %s', e)
+
+                    # Delete VMs first
+                    for vm_inst_creator in self.get_vm_inst_creators():
+                        try:
+                            vm_inst_creator.clean()
+                            if not vm_inst_creator.vm_deleted(block=True):
+                                logger.warn('Unable to deleted VM - %s',
+                                            vm_inst_creator.get_vm_inst().name)
+                        except:
+                            logger.warn('Unexpected error deleting VM - %s ',
+                                        vm_inst_creator.get_vm_inst().name)
+
+                logger.info('Attempting to delete again stack - %s',
+                            self.__stack.name)
+
+                # Delete Stack again
+                heat_utils.delete_stack(self.__heat_cli, self.__stack)
+                deleted = self.stack_deleted(block=True)
+                if not deleted:
+                    raise StackError(
+                        'Stack could not be deleted ' + self.__stack.name)
             except HTTPNotFound:
                 pass
 
             except HTTPNotFound:
                 pass
 
-        self.__stack = None
+            self.__stack = None
 
     def get_stack(self):
         """
 
     def get_stack(self):
         """
@@ -105,7 +166,7 @@ class OpenStackHeatStack:
         object
         :return:
         """
         object
         :return:
         """
-        return heat_utils.get_stack_outputs(self.__heat_cli, self.__stack.id)
+        return heat_utils.get_outputs(self.__heat_cli, self.__stack)
 
     def get_status(self):
         """
 
     def get_status(self):
         """
@@ -116,7 +177,7 @@ class OpenStackHeatStack:
         return heat_utils.get_stack_status(self.__heat_cli, self.__stack.id)
 
     def stack_complete(self, block=False, timeout=None,
         return heat_utils.get_stack_status(self.__heat_cli, self.__stack.id)
 
     def stack_complete(self, block=False, timeout=None,
-                       poll_interval=POLL_INTERVAL):
+                       poll_interval=snaps.config.stack.POLL_INTERVAL):
         """
         Returns true when the stack status returns the value of
         expected_status_code
         """
         Returns true when the stack status returns the value of
         expected_status_code
@@ -128,11 +189,234 @@ class OpenStackHeatStack:
         """
         if not timeout:
             timeout = self.stack_settings.stack_create_timeout
         """
         if not timeout:
             timeout = self.stack_settings.stack_create_timeout
-        return self._stack_status_check(STATUS_CREATE_COMPLETE, block, timeout,
-                                        poll_interval)
+        return self._stack_status_check(
+            snaps.config.stack.STATUS_CREATE_COMPLETE, block, timeout,
+            poll_interval, snaps.config.stack.STATUS_CREATE_FAILED)
+
+    def stack_deleted(self, block=False,
+                      timeout=snaps.config.stack.STACK_DELETE_TIMEOUT,
+                      poll_interval=snaps.config.stack.POLL_INTERVAL):
+        """
+        Returns true when the stack status returns the value of
+        expected_status_code
+        :param block: When true, thread will block until active or timeout
+                      value in seconds has been exceeded (False)
+        :param timeout: The timeout value
+        :param poll_interval: The polling interval in seconds
+        :return: T/F
+        """
+        return self._stack_status_check(
+            snaps.config.stack.STATUS_DELETE_COMPLETE, block, timeout,
+            poll_interval, snaps.config.stack.STATUS_DELETE_FAILED)
+
+    def get_network_creators(self):
+        """
+        Returns a list of network creator objects as configured by the heat
+        template
+        :return: list() of OpenStackNetwork objects
+        """
+
+        neutron = neutron_utils.neutron_client(self._os_creds)
+
+        out = list()
+        stack_networks = heat_utils.get_stack_networks(
+            self.__heat_cli, neutron, self.__stack)
+
+        for stack_network in stack_networks:
+            net_settings = settings_utils.create_network_settings(
+                neutron, stack_network)
+            net_creator = OpenStackNetwork(self._os_creds, net_settings)
+            out.append(net_creator)
+            net_creator.initialize()
+
+        return out
+
+    def get_security_group_creators(self):
+        """
+        Returns a list of security group creator objects as configured by the
+        heat template
+        :return: list() of OpenStackNetwork objects
+        """
+
+        neutron = neutron_utils.neutron_client(self._os_creds)
+
+        out = list()
+        stack_security_groups = heat_utils.get_stack_security_groups(
+            self.__heat_cli, neutron, self.__stack)
+
+        for stack_security_group in stack_security_groups:
+            settings = settings_utils.create_security_group_settings(
+                neutron, stack_security_group)
+            creator = OpenStackSecurityGroup(self._os_creds, settings)
+            out.append(creator)
+            creator.initialize()
+
+        return out
+
+    def get_router_creators(self):
+        """
+        Returns a list of router creator objects as configured by the heat
+        template
+        :return: list() of OpenStackRouter objects
+        """
+
+        neutron = neutron_utils.neutron_client(self._os_creds)
+
+        out = list()
+        stack_routers = heat_utils.get_stack_routers(
+            self.__heat_cli, neutron, self.__stack)
+
+        for routers in stack_routers:
+            settings = settings_utils.create_router_settings(
+                neutron, routers)
+            creator = OpenStackRouter(self._os_creds, settings)
+            out.append(creator)
+            creator.initialize()
+
+        return out
+
+    def get_vm_inst_creators(self, heat_keypair_option=None):
+        """
+        Returns a list of VM Instance creator objects as configured by the heat
+        template
+        :return: list() of OpenStackVmInstance objects
+        """
+
+        out = list()
+        nova = nova_utils.nova_client(self._os_creds)
+
+        stack_servers = heat_utils.get_stack_servers(
+            self.__heat_cli, nova, self.__stack)
+
+        neutron = neutron_utils.neutron_client(self._os_creds)
+        glance = glance_utils.glance_client(self._os_creds)
+
+        for stack_server in stack_servers:
+            vm_inst_settings = settings_utils.create_vm_inst_settings(
+                nova, neutron, stack_server)
+            image_settings = settings_utils.determine_image_config(
+                glance, stack_server, self.image_settings)
+            keypair_settings = settings_utils.determine_keypair_config(
+                self.__heat_cli, self.__stack, stack_server,
+                keypair_settings=self.keypair_settings,
+                priv_key_key=heat_keypair_option)
+            vm_inst_creator = OpenStackVmInstance(
+                self._os_creds, vm_inst_settings, image_settings,
+                keypair_settings)
+            out.append(vm_inst_creator)
+            vm_inst_creator.initialize()
+
+        return out
+
+    def get_volume_creators(self):
+        """
+        Returns a list of Volume creator objects as configured by the heat
+        template
+        :return: list() of OpenStackVolume objects
+        """
+
+        out = list()
+        cinder = cinder_utils.cinder_client(self._os_creds)
+
+        volumes = heat_utils.get_stack_volumes(
+            self.__heat_cli, cinder, self.__stack)
+
+        for volume in volumes:
+            settings = settings_utils.create_volume_config(volume)
+            creator = OpenStackVolume(self._os_creds, settings)
+            out.append(creator)
+
+            try:
+                creator.initialize()
+            except Exception as e:
+                logger.error(
+                    'Unexpected error initializing volume creator - %s', e)
+
+        return out
+
+    def get_volume_type_creators(self):
+        """
+        Returns a list of VolumeType creator objects as configured by the heat
+        template
+        :return: list() of OpenStackVolumeType objects
+        """
+
+        out = list()
+        cinder = cinder_utils.cinder_client(self._os_creds)
+
+        vol_types = heat_utils.get_stack_volume_types(
+            self.__heat_cli, cinder, self.__stack)
+
+        for volume in vol_types:
+            settings = settings_utils.create_volume_type_config(volume)
+            creator = OpenStackVolumeType(self._os_creds, settings)
+            out.append(creator)
+
+            try:
+                creator.initialize()
+            except Exception as e:
+                logger.error(
+                    'Unexpected error initializing volume type creator - %s',
+                    e)
+
+        return out
+
+    def get_keypair_creators(self, outputs_pk_key=None):
+        """
+        Returns a list of keypair creator objects as configured by the heat
+        template
+        :return: list() of OpenStackKeypair objects
+        """
+
+        out = list()
+        nova = nova_utils.nova_client(self._os_creds)
+
+        keypairs = heat_utils.get_stack_keypairs(
+            self.__heat_cli, nova, self.__stack)
+
+        for keypair in keypairs:
+            settings = settings_utils.create_keypair_settings(
+                self.__heat_cli, self.__stack, keypair, outputs_pk_key)
+            creator = OpenStackKeypair(self._os_creds, settings)
+            out.append(creator)
+
+            try:
+                creator.initialize()
+            except Exception as e:
+                logger.error(
+                    'Unexpected error initializing volume type creator - %s',
+                    e)
+
+        return out
+
+    def get_flavor_creators(self):
+        """
+        Returns a list of Flavor creator objects as configured by the heat
+        template
+        :return: list() of OpenStackFlavor objects
+        """
+
+        out = list()
+        nova = nova_utils.nova_client(self._os_creds)
+
+        flavors = heat_utils.get_stack_flavors(
+            self.__heat_cli, nova, self.__stack)
+
+        for flavor in flavors:
+            settings = settings_utils.create_flavor_config(flavor)
+            creator = OpenStackFlavor(self._os_creds, settings)
+            out.append(creator)
+
+            try:
+                creator.initialize()
+            except Exception as e:
+                logger.error(
+                    'Unexpected error initializing volume creator - %s', e)
+
+        return out
 
     def _stack_status_check(self, expected_status_code, block, timeout,
 
     def _stack_status_check(self, expected_status_code, block, timeout,
-                            poll_interval):
+                            poll_interval, fail_status):
         """
         Returns true when the stack status returns the value of
         expected_status_code
         """
         Returns true when the stack status returns the value of
         expected_status_code
@@ -142,6 +426,7 @@ class OpenStackHeatStack:
                       value in seconds has been exceeded (False)
         :param timeout: The timeout value
         :param poll_interval: The polling interval in seconds
                       value in seconds has been exceeded (False)
         :param timeout: The timeout value
         :param poll_interval: The polling interval in seconds
+        :param fail_status: Returns false if the fail_status code is found
         :return: T/F
         """
         # sleep and wait for stack status change
         :return: T/F
         """
         # sleep and wait for stack status change
@@ -151,7 +436,7 @@ class OpenStackHeatStack:
             start = time.time() - timeout
 
         while timeout > time.time() - start:
             start = time.time() - timeout
 
         while timeout > time.time() - start:
-            status = self._status(expected_status_code)
+            status = self._status(expected_status_code, fail_status)
             if status:
                 logger.debug(
                     'Stack is active with name - ' + self.stack_settings.name)
             if status:
                 logger.debug(
                     'Stack is active with name - ' + self.stack_settings.name)
@@ -167,7 +452,8 @@ class OpenStackHeatStack:
             'Timeout checking for stack status for ' + expected_status_code)
         return False
 
             'Timeout checking for stack status for ' + expected_status_code)
         return False
 
-    def _status(self, expected_status_code):
+    def _status(self, expected_status_code,
+                fail_status=snaps.config.stack.STATUS_CREATE_FAILED):
         """
         Returns True when active else False
         :param expected_status_code: stack status evaluated with this string
         """
         Returns True when active else False
         :param expected_status_code: stack status evaluated with this string
@@ -180,48 +466,37 @@ class OpenStackHeatStack:
                 'Cannot stack status for stack with ID - ' + self.__stack.id)
             return False
 
                 'Cannot stack status for stack with ID - ' + self.__stack.id)
             return False
 
-        if status == 'ERROR':
-            raise StackCreationError('Stack had an error during deployment')
+        if fail_status and status == fail_status:
+            resources = heat_utils.get_resources(self.__heat_cli, self.__stack)
+            logger.error('Stack %s failed', self.__stack.name)
+            for resource in resources:
+                if (resource.status !=
+                        snaps.config.stack.STATUS_CREATE_COMPLETE):
+                    logger.error(
+                        'Resource: [%s] status: [%s] reason: [%s]',
+                        resource.name, resource.status, resource.status_reason)
+                else:
+                    logger.debug(
+                        'Resource: [%s] status: [%s] reason: [%s]',
+                        resource.name, resource.status, resource.status_reason)
+
+            raise StackError('Stack had an error')
         logger.debug('Stack status is - ' + status)
         return status == expected_status_code
 
 
         logger.debug('Stack status is - ' + status)
         return status == expected_status_code
 
 
-class StackSettings:
-    def __init__(self, **kwargs):
-        """
-        Constructor
-        :param name: the stack's name (required)
-        :param template: the heat template in dict() format (required if
-                         template_path attribute is None)
-        :param template_path: the location of the heat template file (required
-                              if template attribute is None)
-        :param env_values: k/v pairs of strings for substitution of template
-                           default values (optional)
-        """
-
-        self.name = kwargs.get('name')
-        self.template = kwargs.get('template')
-        self.template_path = kwargs.get('template_path')
-        self.env_values = kwargs.get('env_values')
-        if 'stack_create_timeout' in kwargs:
-            self.stack_create_timeout = kwargs['stack_create_timeout']
-        else:
-            self.stack_create_timeout = STACK_COMPLETE_TIMEOUT
-
-        if not self.name:
-            raise StackSettingsError('name is required')
-
-        if not self.template and not self.template_path:
-            raise StackSettingsError('A Heat template is required')
-
-
-class StackSettingsError(Exception):
+class StackSettings(StackConfig):
     """
     """
-    Exception to be thrown when an stack settings are incorrect
+    Class to hold the configuration settings required for creating OpenStack
+    stack objects
+    deprecated
     """
 
     """
 
-    def __init__(self, message):
-        Exception.__init__(self, message)
+    def __init__(self, **kwargs):
+        from warnings import warn
+        warn('Use snaps.config.stack.StackConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
 
 
 class StackCreationError(Exception):
 
 
 class StackCreationError(Exception):
@@ -229,5 +504,8 @@ class StackCreationError(Exception):
     Exception to be thrown when an stack cannot be created
     """
 
     Exception to be thrown when an stack cannot be created
     """
 
-    def __init__(self, message):
-        Exception.__init__(self, message)
+
+class StackError(Exception):
+    """
+    General exception
+    """