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
22 from snaps.config.stack import StackConfig
23 from snaps.openstack.create_flavor import OpenStackFlavor
24 from snaps.openstack.create_instance import OpenStackVmInstance
25 from snaps.openstack.create_keypairs import OpenStackKeypair
26 from snaps.openstack.create_security_group import OpenStackSecurityGroup
27 from snaps.openstack.create_router import OpenStackRouter
28 from snaps.openstack.create_volume import OpenStackVolume
29 from snaps.openstack.create_volume_type import OpenStackVolumeType
30 from snaps.openstack.openstack_creator import OpenStackCloudObject
31 from snaps.openstack.utils import (
32 nova_utils, settings_utils, glance_utils, cinder_utils)
34 from snaps.openstack.create_network import OpenStackNetwork
35 from snaps.openstack.utils import heat_utils, neutron_utils
37 __author__ = 'spisarski'
39 logger = logging.getLogger('create_stack')
42 class OpenStackHeatStack(OpenStackCloudObject, object):
44 Class responsible for managing a heat stack in OpenStack
47 def __init__(self, os_creds, stack_settings, image_settings=None,
48 keypair_settings=None):
51 :param os_creds: The OpenStack connection credentials
52 :param stack_settings: The stack settings
53 :param image_settings: A list of ImageConfig objects that were used
54 for spawning this stack
55 :param keypair_settings: A list of KeypairConfig objects that were
56 used for spawning this stack
59 super(self.__class__, self).__init__(os_creds)
61 self.stack_settings = stack_settings
64 self.image_settings = image_settings
66 self.image_settings = None
69 self.keypair_settings = keypair_settings
71 self.keypair_settings = None
74 self.__heat_cli = None
83 Loads the existing heat stack
84 :return: The Stack domain object or None
86 super(self.__class__, self).initialize()
88 self.__neutron = neutron_utils.neutron_client(
89 self._os_creds, self._os_session)
90 self.__nova = nova_utils.nova_client(self._os_creds, self._os_session)
91 self.__glance = glance_utils.glance_client(
92 self._os_creds, self._os_session)
93 self.__cinder = cinder_utils.cinder_client(
94 self._os_creds, self._os_session)
96 self.__heat_cli = heat_utils.heat_client(
97 self._os_creds, self._os_session)
98 self.__stack = heat_utils.get_stack(
99 self.__heat_cli, stack_settings=self.stack_settings)
101 logger.info('Found stack with name - ' + self.stack_settings.name)
106 Creates the heat stack in OpenStack if it does not already exist and
107 returns the domain Stack object
108 :return: The Stack domain object or None
113 logger.info('Found stack with name - %s', self.stack_settings.name)
116 self.__stack = heat_utils.create_stack(self.__heat_cli,
119 'Created stack with name - %s', self.stack_settings.name)
120 if self.__stack and self.stack_complete(block=True):
121 logger.info('Stack is now active with name - %s',
122 self.stack_settings.name)
125 status = heat_utils.get_stack_status_reason(self.__heat_cli,
127 logger.error('ERROR: STACK CREATION FAILED: %s', status)
128 raise StackCreationError('Failure while creating stack')
132 Cleanse environment of all artifacts
137 logger.info('Deleting stack - %s', self.__stack.name)
138 heat_utils.delete_stack(self.__heat_cli, self.__stack)
141 self.stack_deleted(block=True)
142 except StackError as e:
143 # Stack deletion seems to fail quite a bit
144 logger.warn('Stack did not delete properly - %s', e)
147 for vm_inst_creator in self.get_vm_inst_creators():
149 vm_inst_creator.clean()
150 if not vm_inst_creator.vm_deleted(block=True):
151 logger.warn('Unable to deleted VM - %s',
152 vm_inst_creator.get_vm_inst().name)
154 logger.warn('Unexpected error deleting VM - %s ',
155 vm_inst_creator.get_vm_inst().name)
157 logger.info('Attempting to delete again stack - %s',
161 heat_utils.delete_stack(self.__heat_cli, self.__stack)
162 deleted = self.stack_deleted(block=True)
165 'Stack could not be deleted ' + self.__stack.name)
171 self.__neutron.httpclient.session.session.close()
172 self.__nova.client.session.session.close()
173 self.__glance.http_client.session.session.close()
174 self.__cinder.client.session.session.close()
176 super(self.__class__, self).clean()
180 Returns the domain Stack object as it was populated when create() was
185 return heat_utils.get_stack_by_id(self.__heat_cli, self.__stack.id)
187 def get_outputs(self):
189 Returns the list of outputs as contained on the OpenStack Heat Stack
193 return heat_utils.get_outputs(self.__heat_cli, self.__stack)
195 def get_status(self):
197 Returns the list of outputs as contained on the OpenStack Heat Stack
201 stack = self.get_stack()
205 def stack_complete(self, block=False, timeout=None,
206 poll_interval=snaps.config.stack.POLL_INTERVAL):
208 Returns true when the stack status returns the value of
210 :param block: When true, thread will block until active or timeout
211 value in seconds has been exceeded (False)
212 :param timeout: The timeout value
213 :param poll_interval: The polling interval in seconds
217 timeout = self.stack_settings.stack_create_timeout
218 return self._stack_status_check(
219 snaps.config.stack.STATUS_CREATE_COMPLETE, block, timeout,
220 poll_interval, snaps.config.stack.STATUS_CREATE_FAILED)
222 def stack_deleted(self, block=False,
223 timeout=snaps.config.stack.STACK_DELETE_TIMEOUT,
224 poll_interval=snaps.config.stack.POLL_INTERVAL):
226 Returns true when the stack status returns the value of
228 :param block: When true, thread will block until active or timeout
229 value in seconds has been exceeded (False)
230 :param timeout: The timeout value
231 :param poll_interval: The polling interval in seconds
234 return self._stack_status_check(
235 snaps.config.stack.STATUS_DELETE_COMPLETE, block, timeout,
236 poll_interval, snaps.config.stack.STATUS_DELETE_FAILED)
238 def get_network_creators(self):
240 Returns a list of network creator objects as configured by the heat
242 :return: list() of OpenStackNetwork objects
246 stack_networks = heat_utils.get_stack_networks(
247 self.__heat_cli, self.__neutron, self.__stack)
249 for stack_network in stack_networks:
250 net_settings = settings_utils.create_network_config(
251 self.__neutron, stack_network)
252 net_creator = OpenStackNetwork(self._os_creds, net_settings)
253 out.append(net_creator)
254 net_creator.initialize()
258 def get_security_group_creators(self):
260 Returns a list of security group creator objects as configured by the
262 :return: list() of OpenStackNetwork objects
266 stack_security_groups = heat_utils.get_stack_security_groups(
267 self.__heat_cli, self.__neutron, self.__stack)
269 for stack_security_group in stack_security_groups:
270 settings = settings_utils.create_security_group_config(
271 self.__neutron, stack_security_group)
272 creator = OpenStackSecurityGroup(self._os_creds, settings)
278 def get_router_creators(self):
280 Returns a list of router creator objects as configured by the heat
282 :return: list() of OpenStackRouter objects
286 stack_routers = heat_utils.get_stack_routers(
287 self.__heat_cli, self.__neutron, self.__stack)
289 for routers in stack_routers:
290 settings = settings_utils.create_router_config(
291 self.__neutron, routers)
292 creator = OpenStackRouter(self._os_creds, settings)
298 def get_vm_inst_creators(self, heat_keypair_option=None):
300 Returns a list of VM Instance creator objects as configured by the heat
302 :return: list() of OpenStackVmInstance objects
307 stack_servers = heat_utils.get_stack_servers(
308 self.__heat_cli, self.__nova, self.__neutron, self._keystone,
309 self.__stack, self._os_creds.project_name)
311 for stack_server in stack_servers:
312 vm_inst_settings = settings_utils.create_vm_inst_config(
313 self.__nova, self._keystone, self.__neutron, stack_server,
314 self._os_creds.project_name)
315 image_settings = settings_utils.determine_image_config(
316 self.__glance, stack_server, self.image_settings)
317 keypair_settings = settings_utils.determine_keypair_config(
318 self.__heat_cli, self.__stack, stack_server,
319 keypair_settings=self.keypair_settings,
320 priv_key_key=heat_keypair_option)
321 vm_inst_creator = OpenStackVmInstance(
322 self._os_creds, vm_inst_settings, image_settings,
324 out.append(vm_inst_creator)
325 vm_inst_creator.initialize()
329 def get_volume_creators(self):
331 Returns a list of Volume creator objects as configured by the heat
333 :return: list() of OpenStackVolume objects
337 volumes = heat_utils.get_stack_volumes(
338 self.__heat_cli, self.__cinder, self.__stack)
340 for volume in volumes:
341 settings = settings_utils.create_volume_config(volume)
342 creator = OpenStackVolume(self._os_creds, settings)
347 except Exception as e:
349 'Unexpected error initializing volume creator - %s', e)
353 def get_volume_type_creators(self):
355 Returns a list of VolumeType creator objects as configured by the heat
357 :return: list() of OpenStackVolumeType objects
361 vol_types = heat_utils.get_stack_volume_types(
362 self.__heat_cli, self.__cinder, self.__stack)
364 for volume in vol_types:
365 settings = settings_utils.create_volume_type_config(volume)
366 creator = OpenStackVolumeType(self._os_creds, settings)
371 except Exception as e:
373 'Unexpected error initializing volume type creator - %s',
378 def get_keypair_creators(self, outputs_pk_key=None):
380 Returns a list of keypair creator objects as configured by the heat
382 :return: list() of OpenStackKeypair objects
387 keypairs = heat_utils.get_stack_keypairs(
388 self.__heat_cli, self.__nova, self.__stack)
390 for keypair in keypairs:
391 settings = settings_utils.create_keypair_config(
392 self.__heat_cli, self.__stack, keypair, outputs_pk_key)
393 creator = OpenStackKeypair(self._os_creds, settings)
398 except Exception as e:
400 'Unexpected error initializing volume type creator - %s',
405 def get_flavor_creators(self):
407 Returns a list of Flavor creator objects as configured by the heat
409 :return: list() of OpenStackFlavor objects
414 flavors = heat_utils.get_stack_flavors(
415 self.__heat_cli, self.__nova, self.__stack)
417 for flavor in flavors:
418 settings = settings_utils.create_flavor_config(flavor)
419 creator = OpenStackFlavor(self._os_creds, settings)
424 except Exception as e:
426 'Unexpected error initializing volume creator - %s', e)
430 def _stack_status_check(self, expected_status_code, block, timeout,
431 poll_interval, fail_status):
433 Returns true when the stack status returns the value of
435 :param expected_status_code: stack status evaluated with this string
437 :param block: When true, thread will block until active or timeout
438 value in seconds has been exceeded (False)
439 :param timeout: The timeout value
440 :param poll_interval: The polling interval in seconds
441 :param fail_status: Returns false if the fail_status code is found
444 # sleep and wait for stack status change
448 start = time.time() - timeout
450 while timeout > time.time() - start:
451 status = self._status(expected_status_code, fail_status)
454 'Stack is active with name - ' + self.stack_settings.name)
457 logger.debug('Retry querying stack status in ' + str(
458 poll_interval) + ' seconds')
459 time.sleep(poll_interval)
460 logger.debug('Stack status query timeout in ' + str(
461 timeout - (time.time() - start)))
464 'Timeout checking for stack status for ' + expected_status_code)
467 def _status(self, expected_status_code,
468 fail_status=snaps.config.stack.STATUS_CREATE_FAILED):
470 Returns True when active else False
471 :param expected_status_code: stack status evaluated with this string
475 status = self.get_status()
478 'Cannot stack status for stack with ID - ' + self.__stack.id)
481 if fail_status and status == fail_status:
482 resources = heat_utils.get_resources(
483 self.__heat_cli, self.__stack.id)
484 logger.error('Stack %s failed', self.__stack.name)
485 for resource in resources:
486 if (resource.status !=
487 snaps.config.stack.STATUS_CREATE_COMPLETE):
489 'Resource: [%s] status: [%s] reason: [%s]',
490 resource.name, resource.status, resource.status_reason)
493 'Resource: [%s] status: [%s] reason: [%s]',
494 resource.name, resource.status, resource.status_reason)
496 raise StackError('Stack had an error')
497 logger.debug('Stack status is - ' + status)
498 return status == expected_status_code
501 class StackSettings(StackConfig):
503 Class to hold the configuration settings required for creating OpenStack
508 def __init__(self, **kwargs):
509 from warnings import warn
510 warn('Use snaps.config.stack.StackConfig instead',
512 super(self.__class__, self).__init__(**kwargs)
515 class StackCreationError(Exception):
517 Exception to be thrown when an stack cannot be created
521 class StackError(Exception):