Merge "Added logging when a heat stack fails."
[snaps.git] / snaps / openstack / create_stack.py
1 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
2 #                    and others.  All rights reserved.
3 #
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:
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
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.
15
16 import logging
17 import time
18
19 from heatclient.exc import HTTPNotFound
20
21 from snaps.openstack.create_flavor import OpenStackFlavor
22 from snaps.openstack.create_instance import OpenStackVmInstance
23 from snaps.openstack.create_keypairs import OpenStackKeypair
24 from snaps.openstack.create_router import OpenStackRouter
25 from snaps.openstack.create_volume import OpenStackVolume
26 from snaps.openstack.create_volume_type import OpenStackVolumeType
27 from snaps.openstack.openstack_creator import OpenStackCloudObject
28 from snaps.openstack.utils import (
29     nova_utils, settings_utils, glance_utils, cinder_utils)
30
31 from snaps.openstack.create_network import OpenStackNetwork
32 from snaps.openstack.utils import heat_utils, neutron_utils
33
34 __author__ = 'spisarski'
35
36 logger = logging.getLogger('create_stack')
37
38 STACK_DELETE_TIMEOUT = 1200
39 STACK_COMPLETE_TIMEOUT = 1200
40 POLL_INTERVAL = 3
41 STATUS_CREATE_FAILED = 'CREATE_FAILED'
42 STATUS_CREATE_COMPLETE = 'CREATE_COMPLETE'
43 STATUS_DELETE_COMPLETE = 'DELETE_COMPLETE'
44 STATUS_DELETE_FAILED = 'DELETE_FAILED'
45
46
47 class OpenStackHeatStack(OpenStackCloudObject, object):
48     """
49     Class responsible for managing a heat stack in OpenStack
50     """
51
52     def __init__(self, os_creds, stack_settings, image_settings=None,
53                  keypair_settings=None):
54         """
55         Constructor
56         :param os_creds: The OpenStack connection credentials
57         :param stack_settings: The stack settings
58         :param image_settings: A list of ImageSettings objects that were used
59                                for spawning this stack
60         :param image_settings: A list of ImageSettings objects that were used
61                                for spawning this stack
62         :param keypair_settings: A list of KeypairSettings objects that were
63                                  used for spawning this stack
64         :return:
65         """
66         super(self.__class__, self).__init__(os_creds)
67
68         self.stack_settings = stack_settings
69
70         if image_settings:
71             self.image_settings = image_settings
72         else:
73             self.image_settings = None
74
75         if image_settings:
76             self.keypair_settings = keypair_settings
77         else:
78             self.keypair_settings = None
79
80         self.__stack = None
81         self.__heat_cli = None
82
83     def initialize(self):
84         """
85         Loads the existing heat stack
86         :return: The Stack domain object or None
87         """
88         self.__heat_cli = heat_utils.heat_client(self._os_creds)
89         self.__stack = heat_utils.get_stack(
90             self.__heat_cli, stack_settings=self.stack_settings)
91         if self.__stack:
92             logger.info('Found stack with name - ' + self.stack_settings.name)
93             return self.__stack
94
95     def create(self):
96         """
97         Creates the heat stack in OpenStack if it does not already exist and
98         returns the domain Stack object
99         :return: The Stack domain object or None
100         """
101         self.initialize()
102
103         if self.__stack:
104             logger.info('Found stack with name - %s', self.stack_settings.name)
105             return self.__stack
106         else:
107             self.__stack = heat_utils.create_stack(self.__heat_cli,
108                                                    self.stack_settings)
109             logger.info(
110                 'Created stack with name - %s', self.stack_settings.name)
111             if self.__stack and self.stack_complete(block=True):
112                 logger.info('Stack is now active with name - %s',
113                             self.stack_settings.name)
114                 return self.__stack
115             else:
116                 status = heat_utils.get_stack_status_reason(self.__heat_cli,
117                                                             self.__stack.id)
118                 logger.error('ERROR: STACK CREATION FAILED: %s', status)
119                 raise StackCreationError('Failure while creating stack')
120
121     def clean(self):
122         """
123         Cleanse environment of all artifacts
124         :return: void
125         """
126         if self.__stack:
127             try:
128                 logger.info('Deleting stack - %s', self.__stack.name)
129                 heat_utils.delete_stack(self.__heat_cli, self.__stack)
130
131                 try:
132                     self.stack_deleted(block=True)
133                 except StackError as e:
134                     # Stack deletion seems to fail quite a bit
135                     logger.warn('Stack did not delete properly - %s', e)
136
137                     # Delete VMs first
138                     for vm_inst_creator in self.get_vm_inst_creators():
139                         try:
140                             vm_inst_creator.clean()
141                             if not vm_inst_creator.vm_deleted(block=True):
142                                 logger.warn('Unable to deleted VM - %s',
143                                             vm_inst_creator.get_vm_inst().name)
144                         except:
145                             logger.warn('Unexpected error deleting VM - %s ',
146                                         vm_inst_creator.get_vm_inst().name)
147
148                 logger.info('Attempting to delete again stack - %s',
149                             self.__stack.name)
150
151                 # Delete Stack again
152                 heat_utils.delete_stack(self.__heat_cli, self.__stack)
153                 deleted = self.stack_deleted(block=True)
154                 if not deleted:
155                     raise StackError(
156                         'Stack could not be deleted ' + self.__stack.name)
157             except HTTPNotFound:
158                 pass
159
160             self.__stack = None
161
162     def get_stack(self):
163         """
164         Returns the domain Stack object as it was populated when create() was
165         called
166         :return: the object
167         """
168         return self.__stack
169
170     def get_outputs(self):
171         """
172         Returns the list of outputs as contained on the OpenStack Heat Stack
173         object
174         :return:
175         """
176         return heat_utils.get_outputs(self.__heat_cli, self.__stack)
177
178     def get_status(self):
179         """
180         Returns the list of outputs as contained on the OpenStack Heat Stack
181         object
182         :return:
183         """
184         return heat_utils.get_stack_status(self.__heat_cli, self.__stack.id)
185
186     def stack_complete(self, block=False, timeout=None,
187                        poll_interval=POLL_INTERVAL):
188         """
189         Returns true when the stack status returns the value of
190         expected_status_code
191         :param block: When true, thread will block until active or timeout
192                       value in seconds has been exceeded (False)
193         :param timeout: The timeout value
194         :param poll_interval: The polling interval in seconds
195         :return: T/F
196         """
197         if not timeout:
198             timeout = self.stack_settings.stack_create_timeout
199         return self._stack_status_check(STATUS_CREATE_COMPLETE, block, timeout,
200                                         poll_interval, STATUS_CREATE_FAILED)
201
202     def stack_deleted(self, block=False, timeout=STACK_DELETE_TIMEOUT,
203                       poll_interval=POLL_INTERVAL):
204         """
205         Returns true when the stack status returns the value of
206         expected_status_code
207         :param block: When true, thread will block until active or timeout
208                       value in seconds has been exceeded (False)
209         :param timeout: The timeout value
210         :param poll_interval: The polling interval in seconds
211         :return: T/F
212         """
213         return self._stack_status_check(STATUS_DELETE_COMPLETE, block, timeout,
214                                         poll_interval, STATUS_DELETE_FAILED)
215
216     def get_network_creators(self):
217         """
218         Returns a list of network creator objects as configured by the heat
219         template
220         :return: list() of OpenStackNetwork objects
221         """
222
223         neutron = neutron_utils.neutron_client(self._os_creds)
224
225         out = list()
226         stack_networks = heat_utils.get_stack_networks(
227             self.__heat_cli, neutron, self.__stack)
228
229         for stack_network in stack_networks:
230             net_settings = settings_utils.create_network_settings(
231                 neutron, stack_network)
232             net_creator = OpenStackNetwork(self._os_creds, net_settings)
233             out.append(net_creator)
234             net_creator.initialize()
235
236         return out
237
238     def get_router_creators(self):
239         """
240         Returns a list of router creator objects as configured by the heat
241         template
242         :return: list() of OpenStackRouter objects
243         """
244
245         neutron = neutron_utils.neutron_client(self._os_creds)
246
247         out = list()
248         stack_routers = heat_utils.get_stack_routers(
249             self.__heat_cli, neutron, self.__stack)
250
251         for routers in stack_routers:
252             settings = settings_utils.create_router_settings(
253                 neutron, routers)
254             creator = OpenStackRouter(self._os_creds, settings)
255             out.append(creator)
256             creator.initialize()
257
258         return out
259
260     def get_vm_inst_creators(self, heat_keypair_option=None):
261         """
262         Returns a list of VM Instance creator objects as configured by the heat
263         template
264         :return: list() of OpenStackVmInstance objects
265         """
266
267         out = list()
268         nova = nova_utils.nova_client(self._os_creds)
269
270         stack_servers = heat_utils.get_stack_servers(
271             self.__heat_cli, nova, self.__stack)
272
273         neutron = neutron_utils.neutron_client(self._os_creds)
274         glance = glance_utils.glance_client(self._os_creds)
275
276         for stack_server in stack_servers:
277             vm_inst_settings = settings_utils.create_vm_inst_settings(
278                 nova, neutron, stack_server)
279             image_settings = settings_utils.determine_image_settings(
280                 glance, stack_server, self.image_settings)
281             keypair_settings = settings_utils.determine_keypair_settings(
282                 self.__heat_cli, self.__stack, stack_server,
283                 keypair_settings=self.keypair_settings,
284                 priv_key_key=heat_keypair_option)
285             vm_inst_creator = OpenStackVmInstance(
286                 self._os_creds, vm_inst_settings, image_settings,
287                 keypair_settings)
288             out.append(vm_inst_creator)
289             vm_inst_creator.initialize()
290
291         return out
292
293     def get_volume_creators(self):
294         """
295         Returns a list of Volume creator objects as configured by the heat
296         template
297         :return: list() of OpenStackVolume objects
298         """
299
300         out = list()
301         cinder = cinder_utils.cinder_client(self._os_creds)
302
303         volumes = heat_utils.get_stack_volumes(
304             self.__heat_cli, cinder, self.__stack)
305
306         for volume in volumes:
307             settings = settings_utils.create_volume_settings(volume)
308             creator = OpenStackVolume(self._os_creds, settings)
309             out.append(creator)
310
311             try:
312                 creator.initialize()
313             except Exception as e:
314                 logger.error(
315                     'Unexpected error initializing volume creator - %s', e)
316
317         return out
318
319     def get_volume_type_creators(self):
320         """
321         Returns a list of VolumeType creator objects as configured by the heat
322         template
323         :return: list() of OpenStackVolumeType objects
324         """
325
326         out = list()
327         cinder = cinder_utils.cinder_client(self._os_creds)
328
329         vol_types = heat_utils.get_stack_volume_types(
330             self.__heat_cli, cinder, self.__stack)
331
332         for volume in vol_types:
333             settings = settings_utils.create_volume_type_settings(volume)
334             creator = OpenStackVolumeType(self._os_creds, settings)
335             out.append(creator)
336
337             try:
338                 creator.initialize()
339             except Exception as e:
340                 logger.error(
341                     'Unexpected error initializing volume type creator - %s',
342                     e)
343
344         return out
345
346     def get_keypair_creators(self, outputs_pk_key=None):
347         """
348         Returns a list of keypair creator objects as configured by the heat
349         template
350         :return: list() of OpenStackKeypair objects
351         """
352
353         out = list()
354         nova = nova_utils.nova_client(self._os_creds)
355
356         keypairs = heat_utils.get_stack_keypairs(
357             self.__heat_cli, nova, self.__stack)
358
359         for keypair in keypairs:
360             settings = settings_utils.create_keypair_settings(
361                 self.__heat_cli, self.__stack, keypair, outputs_pk_key)
362             creator = OpenStackKeypair(self._os_creds, settings)
363             out.append(creator)
364
365             try:
366                 creator.initialize()
367             except Exception as e:
368                 logger.error(
369                     'Unexpected error initializing volume type creator - %s',
370                     e)
371
372         return out
373
374     def get_flavor_creators(self):
375         """
376         Returns a list of Flavor creator objects as configured by the heat
377         template
378         :return: list() of OpenStackFlavor objects
379         """
380
381         out = list()
382         nova = nova_utils.nova_client(self._os_creds)
383
384         flavors = heat_utils.get_stack_flavors(
385             self.__heat_cli, nova, self.__stack)
386
387         for flavor in flavors:
388             settings = settings_utils.create_flavor_settings(flavor)
389             creator = OpenStackFlavor(self._os_creds, settings)
390             out.append(creator)
391
392             try:
393                 creator.initialize()
394             except Exception as e:
395                 logger.error(
396                     'Unexpected error initializing volume creator - %s', e)
397
398         return out
399
400     def _stack_status_check(self, expected_status_code, block, timeout,
401                             poll_interval, fail_status):
402         """
403         Returns true when the stack status returns the value of
404         expected_status_code
405         :param expected_status_code: stack status evaluated with this string
406                                      value
407         :param block: When true, thread will block until active or timeout
408                       value in seconds has been exceeded (False)
409         :param timeout: The timeout value
410         :param poll_interval: The polling interval in seconds
411         :param fail_status: Returns false if the fail_status code is found
412         :return: T/F
413         """
414         # sleep and wait for stack status change
415         if block:
416             start = time.time()
417         else:
418             start = time.time() - timeout
419
420         while timeout > time.time() - start:
421             status = self._status(expected_status_code, fail_status)
422             if status:
423                 logger.debug(
424                     'Stack is active with name - ' + self.stack_settings.name)
425                 return True
426
427             logger.debug('Retry querying stack status in ' + str(
428                 poll_interval) + ' seconds')
429             time.sleep(poll_interval)
430             logger.debug('Stack status query timeout in ' + str(
431                 timeout - (time.time() - start)))
432
433         logger.error(
434             'Timeout checking for stack status for ' + expected_status_code)
435         return False
436
437     def _status(self, expected_status_code, fail_status=STATUS_CREATE_FAILED):
438         """
439         Returns True when active else False
440         :param expected_status_code: stack status evaluated with this string
441         value
442         :return: T/F
443         """
444         status = self.get_status()
445         if not status:
446             logger.warning(
447                 'Cannot stack status for stack with ID - ' + self.__stack.id)
448             return False
449
450         if fail_status and status == fail_status:
451             resources = heat_utils.get_resources(self.__heat_cli, self.__stack)
452             logger.error('Stack %s failed', self.__stack.name)
453             for resource in resources:
454                 if resource.status != STATUS_CREATE_COMPLETE:
455                     logger.error(
456                         'Resource: [%s] status: [%s] reason: [%s]',
457                         resource.name, resource.status, resource.status_reason)
458                 else:
459                     logger.debug(
460                         'Resource: [%s] status: [%s] reason: [%s]',
461                         resource.name, resource.status, resource.status_reason)
462
463             raise StackError('Stack had an error')
464         logger.debug('Stack status is - ' + status)
465         return status == expected_status_code
466
467
468 class StackSettings:
469     def __init__(self, **kwargs):
470         """
471         Constructor
472         :param name: the stack's name (required)
473         :param template: the heat template in dict() format (required if
474                          template_path attribute is None)
475         :param template_path: the location of the heat template file (required
476                               if template attribute is None)
477         :param env_values: dict() of strings for substitution of template
478                            default values (optional)
479         """
480
481         self.name = kwargs.get('name')
482         self.template = kwargs.get('template')
483         self.template_path = kwargs.get('template_path')
484         self.env_values = kwargs.get('env_values')
485         if 'stack_create_timeout' in kwargs:
486             self.stack_create_timeout = kwargs['stack_create_timeout']
487         else:
488             self.stack_create_timeout = STACK_COMPLETE_TIMEOUT
489
490         if not self.name:
491             raise StackSettingsError('name is required')
492
493         if not self.template and not self.template_path:
494             raise StackSettingsError('A Heat template is required')
495
496     def __eq__(self, other):
497         return (self.name == other.name and
498                 self.template == other.template and
499                 self.template_path == other.template_path and
500                 self.env_values == other.env_values and
501                 self.stack_create_timeout == other.stack_create_timeout)
502
503
504 class StackSettingsError(Exception):
505     """
506     Exception to be thrown when an stack settings are incorrect
507     """
508
509
510 class StackCreationError(Exception):
511     """
512     Exception to be thrown when an stack cannot be created
513     """
514
515
516 class StackError(Exception):
517     """
518     General exception
519     """