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