# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") # and others. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import time from heatclient.exc import HTTPNotFound from snaps.openstack.create_instance import OpenStackVmInstance from snaps.openstack.utils import nova_utils, settings_utils, glance_utils from snaps.openstack.create_network import OpenStackNetwork from snaps.openstack.utils import heat_utils, neutron_utils __author__ = 'spisarski' logger = logging.getLogger('create_stack') STACK_COMPLETE_TIMEOUT = 1200 POLL_INTERVAL = 3 STATUS_CREATE_FAILED = 'CREATE_FAILED' STATUS_CREATE_COMPLETE = 'CREATE_COMPLETE' STATUS_DELETE_COMPLETE = 'DELETE_COMPLETE' STATUS_DELETE_FAILED = 'DELETE_FAILED' class OpenStackHeatStack: """ Class responsible for creating an heat stack in OpenStack """ 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 :param image_settings: A list of ImageSettings objects that were used for spawning this stack :param image_settings: A list of ImageSettings objects that were used for spawning this stack :param keypair_settings: A list of KeypairSettings objects that were used for spawning this stack :return: """ self.__os_creds = os_creds 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 def create(self, cleanup=False): """ Creates the heat stack in OpenStack if it does not already exist and returns the domain Stack object :param cleanup: When true, this object is initialized only via queries, else objects will be created when the queries return None. The name of this parameter should be changed to something like 'readonly' as the same goes with all of the other creator classes. :return: The OpenStack Stack object """ 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 elif not cleanup: self.__stack = heat_utils.create_stack(self.__heat_cli, self.stack_settings) logger.info( 'Created stack with name - ' + self.stack_settings.name) if self.__stack and self.stack_complete(block=True): logger.info( 'Stack is now active with name - ' + self.stack_settings.name) 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 def clean(self): """ Cleanse environment of all artifacts :return: void """ if self.__stack: try: logger.info('Deleting stack - %s' + self.__stack.name) 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 self.__stack = None def get_stack(self): """ Returns the domain Stack object as it was populated when create() was called :return: the object """ return self.__stack def get_outputs(self): """ Returns the list of outputs as contained on the OpenStack Heat Stack object :return: """ return heat_utils.get_outputs(self.__heat_cli, self.__stack) def get_status(self): """ Returns the list of outputs as contained on the OpenStack Heat Stack object :return: """ return heat_utils.get_stack_status(self.__heat_cli, self.__stack.id) def stack_complete(self, block=False, timeout=None, poll_interval=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 """ if not timeout: timeout = self.stack_settings.stack_create_timeout return self._stack_status_check(STATUS_CREATE_COMPLETE, block, timeout, poll_interval, STATUS_CREATE_FAILED) def stack_deleted(self, block=False, timeout=None, poll_interval=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 """ if not timeout: timeout = self.stack_settings.stack_create_timeout return self._stack_status_check(STATUS_DELETE_COMPLETE, block, timeout, poll_interval, 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.create(cleanup=True) 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_settings( glance, stack_server, self.image_settings) keypair_settings = settings_utils.determine_keypair_settings( 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.create(cleanup=True) return out def _stack_status_check(self, expected_status_code, block, timeout, poll_interval, fail_status): """ Returns true when the stack status returns the value of expected_status_code :param expected_status_code: stack status evaluated with this string value :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 :param fail_status: Returns false if the fail_status code is found :return: T/F """ # sleep and wait for stack status change if block: start = time.time() else: start = time.time() - timeout while timeout > time.time() - start: status = self._status(expected_status_code, fail_status) if status: logger.debug( 'Stack is active with name - ' + self.stack_settings.name) return True logger.debug('Retry querying stack status in ' + str( poll_interval) + ' seconds') time.sleep(poll_interval) logger.debug('Stack status query timeout in ' + str( timeout - (time.time() - start))) logger.error( 'Timeout checking for stack status for ' + expected_status_code) return False def _status(self, expected_status_code, fail_status=STATUS_CREATE_FAILED): """ Returns True when active else False :param expected_status_code: stack status evaluated with this string value :return: T/F """ status = self.get_status() if not status: logger.warning( 'Cannot stack status for stack with ID - ' + self.__stack.id) return False if fail_status and status == fail_status: raise StackError('Stack had an error') 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') def __eq__(self, other): return (self.name == other.name and self.template == other.template and self.template_path == other.template_path and self.env_values == other.env_values and self.stack_create_timeout == other.stack_create_timeout) class StackSettingsError(Exception): """ Exception to be thrown when an stack settings are incorrect """ class StackCreationError(Exception): """ Exception to be thrown when an stack cannot be created """ class StackError(Exception): """ General exception """