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