1 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
2 # and others. All rights reserved.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
19 from heatclient.exc import HTTPNotFound
21 from snaps.openstack.create_instance import OpenStackVmInstance
22 from snaps.openstack.utils import nova_utils, settings_utils, glance_utils
24 from snaps.openstack.create_network import OpenStackNetwork
25 from snaps.openstack.utils import heat_utils, neutron_utils
27 __author__ = 'spisarski'
29 logger = logging.getLogger('create_stack')
31 STACK_COMPLETE_TIMEOUT = 1200
33 STATUS_CREATE_FAILED = 'CREATE_FAILED'
34 STATUS_CREATE_COMPLETE = 'CREATE_COMPLETE'
35 STATUS_DELETE_COMPLETE = 'DELETE_COMPLETE'
36 STATUS_DELETE_FAILED = 'DELETE_FAILED'
39 class OpenStackHeatStack:
41 Class responsible for creating an heat stack in OpenStack
44 def __init__(self, os_creds, stack_settings, image_settings=None,
45 keypair_settings=None):
48 :param os_creds: The OpenStack connection credentials
49 :param stack_settings: The stack settings
50 :param image_settings: A list of ImageSettings objects that were used
51 for spawning this stack
52 :param image_settings: A list of ImageSettings objects that were used
53 for spawning this stack
54 :param keypair_settings: A list of KeypairSettings objects that were
55 used for spawning this stack
58 self.__os_creds = os_creds
59 self.stack_settings = stack_settings
62 self.image_settings = image_settings
64 self.image_settings = None
67 self.keypair_settings = keypair_settings
69 self.keypair_settings = None
72 self.__heat_cli = None
74 def create(self, cleanup=False):
76 Creates the heat stack in OpenStack if it does not already exist and
77 returns the domain Stack object
78 :param cleanup: When true, this object is initialized only via queries,
79 else objects will be created when the queries return
80 None. The name of this parameter should be changed to
81 something like 'readonly' as the same goes with all of
82 the other creator classes.
83 :return: The OpenStack Stack object
85 self.__heat_cli = heat_utils.heat_client(self.__os_creds)
86 self.__stack = heat_utils.get_stack(
87 self.__heat_cli, stack_settings=self.stack_settings)
89 logger.info('Found stack with name - ' + self.stack_settings.name)
92 self.__stack = heat_utils.create_stack(self.__heat_cli,
95 'Created stack with name - ' + self.stack_settings.name)
96 if self.__stack and self.stack_complete(block=True):
98 'Stack is now active with name - ' +
99 self.stack_settings.name)
102 raise StackCreationError(
103 'Stack was not created or activated in the alloted amount '
106 logger.info('Did not create stack due to cleanup mode')
112 Cleanse environment of all artifacts
117 logger.info('Deleting stack - %s' + self.__stack.name)
118 heat_utils.delete_stack(self.__heat_cli, self.__stack)
121 self.stack_deleted(block=True)
122 except StackError as e:
123 # Stack deletion seems to fail quite a bit
124 logger.warn('Stack did not delete properly - %s', e)
127 for vm_inst_creator in self.get_vm_inst_creators():
129 vm_inst_creator.clean()
130 if not vm_inst_creator.vm_deleted(block=True):
131 logger.warn('Unable to deleted VM - %s',
132 vm_inst_creator.get_vm_inst().name)
134 logger.warn('Unexpected error deleting VM - %s ',
135 vm_inst_creator.get_vm_inst().name)
137 logger.info('Attempting to delete again stack - %s',
141 heat_utils.delete_stack(self.__heat_cli, self.__stack)
142 deleted = self.stack_deleted(block=True)
145 'Stack could not be deleted ' + self.__stack.name)
153 Returns the domain Stack object as it was populated when create() was
159 def get_outputs(self):
161 Returns the list of outputs as contained on the OpenStack Heat Stack
165 return heat_utils.get_outputs(self.__heat_cli, self.__stack)
167 def get_status(self):
169 Returns the list of outputs as contained on the OpenStack Heat Stack
173 return heat_utils.get_stack_status(self.__heat_cli, self.__stack.id)
175 def stack_complete(self, block=False, timeout=None,
176 poll_interval=POLL_INTERVAL):
178 Returns true when the stack status returns the value of
180 :param block: When true, thread will block until active or timeout
181 value in seconds has been exceeded (False)
182 :param timeout: The timeout value
183 :param poll_interval: The polling interval in seconds
187 timeout = self.stack_settings.stack_create_timeout
188 return self._stack_status_check(STATUS_CREATE_COMPLETE, block, timeout,
189 poll_interval, STATUS_CREATE_FAILED)
191 def stack_deleted(self, block=False, timeout=None,
192 poll_interval=POLL_INTERVAL):
194 Returns true when the stack status returns the value of
196 :param block: When true, thread will block until active or timeout
197 value in seconds has been exceeded (False)
198 :param timeout: The timeout value
199 :param poll_interval: The polling interval in seconds
203 timeout = self.stack_settings.stack_create_timeout
204 return self._stack_status_check(STATUS_DELETE_COMPLETE, block, timeout,
205 poll_interval, STATUS_DELETE_FAILED)
207 def get_network_creators(self):
209 Returns a list of network creator objects as configured by the heat
211 :return: list() of OpenStackNetwork objects
214 neutron = neutron_utils.neutron_client(self.__os_creds)
217 stack_networks = heat_utils.get_stack_networks(
218 self.__heat_cli, neutron, self.__stack)
220 for stack_network in stack_networks:
221 net_settings = settings_utils.create_network_settings(
222 neutron, stack_network)
223 net_creator = OpenStackNetwork(self.__os_creds, net_settings)
224 out.append(net_creator)
225 net_creator.create(cleanup=True)
229 def get_vm_inst_creators(self, heat_keypair_option=None):
231 Returns a list of VM Instance creator objects as configured by the heat
233 :return: list() of OpenStackVmInstance objects
237 nova = nova_utils.nova_client(self.__os_creds)
239 stack_servers = heat_utils.get_stack_servers(
240 self.__heat_cli, nova, self.__stack)
242 neutron = neutron_utils.neutron_client(self.__os_creds)
243 glance = glance_utils.glance_client(self.__os_creds)
245 for stack_server in stack_servers:
246 vm_inst_settings = settings_utils.create_vm_inst_settings(
247 nova, neutron, stack_server)
248 image_settings = settings_utils.determine_image_settings(
249 glance, stack_server, self.image_settings)
250 keypair_settings = settings_utils.determine_keypair_settings(
251 self.__heat_cli, self.__stack, stack_server,
252 keypair_settings=self.keypair_settings,
253 priv_key_key=heat_keypair_option)
254 vm_inst_creator = OpenStackVmInstance(
255 self.__os_creds, vm_inst_settings, image_settings,
257 out.append(vm_inst_creator)
258 vm_inst_creator.create(cleanup=True)
262 def _stack_status_check(self, expected_status_code, block, timeout,
263 poll_interval, fail_status):
265 Returns true when the stack status returns the value of
267 :param expected_status_code: stack status evaluated with this string
269 :param block: When true, thread will block until active or timeout
270 value in seconds has been exceeded (False)
271 :param timeout: The timeout value
272 :param poll_interval: The polling interval in seconds
273 :param fail_status: Returns false if the fail_status code is found
276 # sleep and wait for stack status change
280 start = time.time() - timeout
282 while timeout > time.time() - start:
283 status = self._status(expected_status_code, fail_status)
286 'Stack is active with name - ' + self.stack_settings.name)
289 logger.debug('Retry querying stack status in ' + str(
290 poll_interval) + ' seconds')
291 time.sleep(poll_interval)
292 logger.debug('Stack status query timeout in ' + str(
293 timeout - (time.time() - start)))
296 'Timeout checking for stack status for ' + expected_status_code)
299 def _status(self, expected_status_code, fail_status=STATUS_CREATE_FAILED):
301 Returns True when active else False
302 :param expected_status_code: stack status evaluated with this string
306 status = self.get_status()
309 'Cannot stack status for stack with ID - ' + self.__stack.id)
312 if fail_status and status == fail_status:
313 raise StackError('Stack had an error')
314 logger.debug('Stack status is - ' + status)
315 return status == expected_status_code
319 def __init__(self, **kwargs):
322 :param name: the stack's name (required)
323 :param template: the heat template in dict() format (required if
324 template_path attribute is None)
325 :param template_path: the location of the heat template file (required
326 if template attribute is None)
327 :param env_values: k/v pairs of strings for substitution of template
328 default values (optional)
331 self.name = kwargs.get('name')
332 self.template = kwargs.get('template')
333 self.template_path = kwargs.get('template_path')
334 self.env_values = kwargs.get('env_values')
335 if 'stack_create_timeout' in kwargs:
336 self.stack_create_timeout = kwargs['stack_create_timeout']
338 self.stack_create_timeout = STACK_COMPLETE_TIMEOUT
341 raise StackSettingsError('name is required')
343 if not self.template and not self.template_path:
344 raise StackSettingsError('A Heat template is required')
346 def __eq__(self, other):
347 return (self.name == other.name and
348 self.template == other.template and
349 self.template_path == other.template_path and
350 self.env_values == other.env_values and
351 self.stack_create_timeout == other.stack_create_timeout)
354 class StackSettingsError(Exception):
356 Exception to be thrown when an stack settings are incorrect
360 class StackCreationError(Exception):
362 Exception to be thrown when an stack cannot be created
366 class StackError(Exception):