7ecf4494eab7aee7c6807af0f41ed4b8f4c97776
[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):
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=True):
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 clean(self):
132         """
133         Cleanse environment of all artifacts
134         :return: void
135         """
136         if self.__stack:
137             try:
138                 logger.info('Deleting stack - %s', self.__stack.name)
139                 heat_utils.delete_stack(self.__heat_cli, self.__stack)
140
141                 try:
142                     self.stack_deleted(block=True)
143                 except StackError as e:
144                     # Stack deletion seems to fail quite a bit
145                     logger.warn('Stack did not delete properly - %s', e)
146
147                     # Delete VMs first
148                     for vm_inst_creator in self.get_vm_inst_creators():
149                         try:
150                             vm_inst_creator.clean()
151                             if not vm_inst_creator.vm_deleted(block=True):
152                                 logger.warn('Unable to deleted VM - %s',
153                                             vm_inst_creator.get_vm_inst().name)
154                         except:
155                             logger.warn('Unexpected error deleting VM - %s ',
156                                         vm_inst_creator.get_vm_inst().name)
157
158                 logger.info('Attempting to delete again stack - %s',
159                             self.__stack.name)
160
161                 # Delete Stack again
162                 heat_utils.delete_stack(self.__heat_cli, self.__stack)
163                 deleted = self.stack_deleted(block=True)
164                 if not deleted:
165                     raise StackError(
166                         'Stack could not be deleted ' + self.__stack.name)
167             except HTTPNotFound:
168                 pass
169
170             self.__stack = None
171
172         self.__neutron.httpclient.session.session.close()
173         self.__nova.client.session.session.close()
174         self.__glance.http_client.session.session.close()
175         self.__cinder.client.session.session.close()
176
177         super(self.__class__, self).clean()
178
179     def get_stack(self):
180         """
181         Returns the domain Stack object as it was populated when create() was
182         called
183         :return: the object
184         """
185         if self.__stack:
186             return heat_utils.get_stack_by_id(self.__heat_cli, self.__stack.id)
187
188     def get_outputs(self):
189         """
190         Returns the list of outputs as contained on the OpenStack Heat Stack
191         object
192         :return:
193         """
194         return heat_utils.get_outputs(self.__heat_cli, self.__stack)
195
196     def get_status(self):
197         """
198         Returns the list of outputs as contained on the OpenStack Heat Stack
199         object
200         :return:
201         """
202         stack = self.get_stack()
203         if stack:
204             return stack.status
205
206     def stack_complete(self, block=False, timeout=None,
207                        poll_interval=snaps.config.stack.POLL_INTERVAL):
208         """
209         Returns true when the stack status returns the value of
210         expected_status_code
211         :param block: When true, thread will block until active or timeout
212                       value in seconds has been exceeded (False)
213         :param timeout: The timeout value
214         :param poll_interval: The polling interval in seconds
215         :return: T/F
216         """
217         if not timeout:
218             timeout = self.stack_settings.stack_create_timeout
219         return self._stack_status_check(
220             snaps.config.stack.STATUS_CREATE_COMPLETE, block, timeout,
221             poll_interval, snaps.config.stack.STATUS_CREATE_FAILED)
222
223     def stack_deleted(self, block=False,
224                       timeout=snaps.config.stack.STACK_DELETE_TIMEOUT,
225                       poll_interval=snaps.config.stack.POLL_INTERVAL):
226         """
227         Returns true when the stack status returns the value of
228         expected_status_code
229         :param block: When true, thread will block until active or timeout
230                       value in seconds has been exceeded (False)
231         :param timeout: The timeout value
232         :param poll_interval: The polling interval in seconds
233         :return: T/F
234         """
235         return self._stack_status_check(
236             snaps.config.stack.STATUS_DELETE_COMPLETE, block, timeout,
237             poll_interval, snaps.config.stack.STATUS_DELETE_FAILED)
238
239     def get_network_creators(self):
240         """
241         Returns a list of network creator objects as configured by the heat
242         template
243         :return: list() of OpenStackNetwork objects
244         """
245
246         out = list()
247         stack_networks = heat_utils.get_stack_networks(
248             self.__heat_cli, self.__neutron, self.__stack)
249
250         for stack_network in stack_networks:
251             net_settings = settings_utils.create_network_config(
252                 self.__neutron, stack_network)
253             net_creator = OpenStackNetwork(self._os_creds, net_settings)
254             out.append(net_creator)
255             net_creator.initialize()
256
257         return out
258
259     def get_security_group_creators(self):
260         """
261         Returns a list of security group creator objects as configured by the
262         heat template
263         :return: list() of OpenStackNetwork objects
264         """
265
266         out = list()
267         stack_security_groups = heat_utils.get_stack_security_groups(
268             self.__heat_cli, self.__neutron, self.__stack)
269
270         for stack_security_group in stack_security_groups:
271             settings = settings_utils.create_security_group_config(
272                 self.__neutron, stack_security_group)
273             creator = OpenStackSecurityGroup(self._os_creds, settings)
274             out.append(creator)
275             creator.initialize()
276
277         return out
278
279     def get_router_creators(self):
280         """
281         Returns a list of router creator objects as configured by the heat
282         template
283         :return: list() of OpenStackRouter objects
284         """
285
286         out = list()
287         stack_routers = heat_utils.get_stack_routers(
288             self.__heat_cli, self.__neutron, self.__stack)
289
290         for routers in stack_routers:
291             settings = settings_utils.create_router_config(
292                 self.__neutron, routers)
293             creator = OpenStackRouter(self._os_creds, settings)
294             out.append(creator)
295             creator.initialize()
296
297         return out
298
299     def __create_vm_inst(self, heat_keypair_option, stack_server):
300
301         vm_inst_settings = settings_utils.create_vm_inst_config(
302             self.__nova, self._keystone, self.__neutron, stack_server,
303             self._os_creds.project_name)
304         image_settings = settings_utils.determine_image_config(
305             self.__glance, stack_server, self.image_settings)
306         keypair_settings = settings_utils.determine_keypair_config(
307             self.__heat_cli, self.__stack, stack_server,
308             keypair_settings=self.keypair_settings,
309             priv_key_key=heat_keypair_option)
310         vm_inst_creator = OpenStackVmInstance(
311             self._os_creds, vm_inst_settings, image_settings,
312             keypair_settings)
313         vm_inst_creator.initialize()
314         return vm_inst_creator
315
316     def get_vm_inst_creators(self, heat_keypair_option=None):
317         """
318         Returns a list of VM Instance creator objects as configured by the heat
319         template
320         :return: list() of OpenStackVmInstance objects
321         """
322
323         out = list()
324
325         stack_servers = heat_utils.get_stack_servers(
326             self.__heat_cli, self.__nova, self.__neutron, self._keystone,
327             self.__stack, self._os_creds.project_name)
328
329         workers = []
330         for stack_server in stack_servers:
331             worker = worker_pool().apply_async(
332                 self.__create_vm_inst,
333                 (heat_keypair_option,
334                     stack_server))
335             workers.append(worker)
336
337         for worker in workers:
338             out.append(worker.get())
339
340         return out
341
342     def get_volume_creators(self):
343         """
344         Returns a list of Volume creator objects as configured by the heat
345         template
346         :return: list() of OpenStackVolume objects
347         """
348
349         out = list()
350         volumes = heat_utils.get_stack_volumes(
351             self.__heat_cli, self.__cinder, self.__stack)
352
353         for volume in volumes:
354             settings = settings_utils.create_volume_config(volume)
355             creator = OpenStackVolume(self._os_creds, settings)
356             out.append(creator)
357
358             try:
359                 creator.initialize()
360             except Exception as e:
361                 logger.error(
362                     'Unexpected error initializing volume creator - %s', e)
363
364         return out
365
366     def get_volume_type_creators(self):
367         """
368         Returns a list of VolumeType creator objects as configured by the heat
369         template
370         :return: list() of OpenStackVolumeType objects
371         """
372
373         out = list()
374         vol_types = heat_utils.get_stack_volume_types(
375             self.__heat_cli, self.__cinder, self.__stack)
376
377         for volume in vol_types:
378             settings = settings_utils.create_volume_type_config(volume)
379             creator = OpenStackVolumeType(self._os_creds, settings)
380             out.append(creator)
381
382             try:
383                 creator.initialize()
384             except Exception as e:
385                 logger.error(
386                     'Unexpected error initializing volume type creator - %s',
387                     e)
388
389         return out
390
391     def get_keypair_creators(self, outputs_pk_key=None):
392         """
393         Returns a list of keypair creator objects as configured by the heat
394         template
395         :return: list() of OpenStackKeypair objects
396         """
397
398         out = list()
399
400         keypairs = heat_utils.get_stack_keypairs(
401             self.__heat_cli, self.__nova, self.__stack)
402
403         for keypair in keypairs:
404             settings = settings_utils.create_keypair_config(
405                 self.__heat_cli, self.__stack, keypair, outputs_pk_key)
406             creator = OpenStackKeypair(self._os_creds, settings)
407             out.append(creator)
408
409             try:
410                 creator.initialize()
411             except Exception as e:
412                 logger.error(
413                     'Unexpected error initializing volume type creator - %s',
414                     e)
415
416         return out
417
418     def get_flavor_creators(self):
419         """
420         Returns a list of Flavor creator objects as configured by the heat
421         template
422         :return: list() of OpenStackFlavor objects
423         """
424
425         out = list()
426
427         flavors = heat_utils.get_stack_flavors(
428             self.__heat_cli, self.__nova, self.__stack)
429
430         for flavor in flavors:
431             settings = settings_utils.create_flavor_config(flavor)
432             creator = OpenStackFlavor(self._os_creds, settings)
433             out.append(creator)
434
435             try:
436                 creator.initialize()
437             except Exception as e:
438                 logger.error(
439                     'Unexpected error initializing volume creator - %s', e)
440
441         return out
442
443     def _stack_status_check(self, expected_status_code, block, timeout,
444                             poll_interval, fail_status):
445         """
446         Returns true when the stack status returns the value of
447         expected_status_code
448         :param expected_status_code: stack status evaluated with this string
449                                      value
450         :param block: When true, thread will block until active or timeout
451                       value in seconds has been exceeded (False)
452         :param timeout: The timeout value
453         :param poll_interval: The polling interval in seconds
454         :param fail_status: Returns false if the fail_status code is found
455         :return: T/F
456         """
457         # sleep and wait for stack status change
458         if block:
459             start = time.time()
460         else:
461             start = time.time() - timeout
462
463         while timeout > time.time() - start:
464             status = self._status(expected_status_code, fail_status)
465             if status:
466                 logger.debug(
467                     'Stack is active with name - ' + self.stack_settings.name)
468                 return True
469
470             logger.debug('Retry querying stack status in ' + str(
471                 poll_interval) + ' seconds')
472             time.sleep(poll_interval)
473             logger.debug('Stack status query timeout in ' + str(
474                 timeout - (time.time() - start)))
475
476         logger.error(
477             'Timeout checking for stack status for ' + expected_status_code)
478         return False
479
480     def _status(self, expected_status_code,
481                 fail_status=snaps.config.stack.STATUS_CREATE_FAILED):
482         """
483         Returns True when active else False
484         :param expected_status_code: stack status evaluated with this string
485         value
486         :return: T/F
487         """
488         status = self.get_status()
489         if not status:
490             logger.warning(
491                 'Cannot stack status for stack with ID - ' + self.__stack.id)
492             return False
493
494         if fail_status and status == fail_status:
495             resources = heat_utils.get_resources(
496                 self.__heat_cli, self.__stack.id)
497             logger.error('Stack %s failed', self.__stack.name)
498             for resource in resources:
499                 if (resource.status !=
500                         snaps.config.stack.STATUS_CREATE_COMPLETE):
501                     logger.error(
502                         'Resource: [%s] status: [%s] reason: [%s]',
503                         resource.name, resource.status, resource.status_reason)
504                 else:
505                     logger.debug(
506                         'Resource: [%s] status: [%s] reason: [%s]',
507                         resource.name, resource.status, resource.status_reason)
508
509             raise StackError('Stack had an error')
510         logger.debug('Stack status is - ' + status)
511         return status == expected_status_code
512
513
514 def generate_creator(os_creds, stack_inst, image_settings):
515     """
516     Initializes an OpenStackHeatStack object
517     :param os_creds: the OpenStack credentials
518     :param stack_inst: the SNAPS-OO VmInst domain object
519     :param image_settings: list of SNAPS-OO ImageConfig objects
520     :return: an initialized OpenStackHeatStack object
521     """
522
523     heat_config = StackConfig(
524         name=stack_inst.name, template={'place': 'holder'})
525     heat_creator = OpenStackHeatStack(os_creds, heat_config, image_settings)
526     heat_creator.initialize()
527     return heat_creator
528
529
530 class StackSettings(StackConfig):
531     """
532     Class to hold the configuration settings required for creating OpenStack
533     stack objects
534     deprecated
535     """
536
537     def __init__(self, **kwargs):
538         from warnings import warn
539         warn('Use snaps.config.stack.StackConfig instead',
540              DeprecationWarning)
541         super(self.__class__, self).__init__(**kwargs)
542
543
544 class StackCreationError(Exception):
545     """
546     Exception to be thrown when an stack cannot be created
547     """
548
549
550 class StackError(Exception):
551     """
552     General exception
553     """