Added method to return OpenStackVmInstance from Heat.
[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_instance import OpenStackVmInstance
22 from snaps.openstack.utils import nova_utils, settings_utils, glance_utils
23
24 from snaps.openstack.create_network import OpenStackNetwork
25 from snaps.openstack.utils import heat_utils, neutron_utils
26
27 __author__ = 'spisarski'
28
29 logger = logging.getLogger('create_stack')
30
31 STACK_COMPLETE_TIMEOUT = 1200
32 POLL_INTERVAL = 3
33 STATUS_CREATE_FAILED = 'CREATE_FAILED'
34 STATUS_CREATE_COMPLETE = 'CREATE_COMPLETE'
35 STATUS_DELETE_COMPLETE = 'DELETE_COMPLETE'
36 STATUS_DELETE_FAILED = 'DELETE_FAILED'
37
38
39 class OpenStackHeatStack:
40     """
41     Class responsible for creating an heat stack in OpenStack
42     """
43
44     def __init__(self, os_creds, stack_settings, image_settings=None,
45                  keypair_settings=None):
46         """
47         Constructor
48         :param os_creds: The OpenStack connection credentials
49         :param stack_settings: The stack settings
50         :param image_settings: A list of ImageSettings objects that were used
51                                for spawning this stack
52         :param image_settings: A list of ImageSettings objects that were used
53                                for spawning this stack
54         :param keypair_settings: A list of KeypairSettings objects that were
55                                  used for spawning this stack
56         :return:
57         """
58         self.__os_creds = os_creds
59         self.stack_settings = stack_settings
60
61         if image_settings:
62             self.image_settings = image_settings
63         else:
64             self.image_settings = None
65
66         if image_settings:
67             self.keypair_settings = keypair_settings
68         else:
69             self.keypair_settings = None
70
71         self.__stack = None
72         self.__heat_cli = None
73
74     def create(self, cleanup=False):
75         """
76         Creates the heat stack in OpenStack if it does not already exist and
77         returns the domain Stack object
78         :param cleanup: When true, this object is initialized only via queries,
79                         else objects will be created when the queries return
80                         None. The name of this parameter should be changed to
81                         something like 'readonly' as the same goes with all of
82                         the other creator classes.
83         :return: The OpenStack Stack object
84         """
85         self.__heat_cli = heat_utils.heat_client(self.__os_creds)
86         self.__stack = heat_utils.get_stack(
87             self.__heat_cli, stack_settings=self.stack_settings)
88         if self.__stack:
89             logger.info('Found stack with name - ' + self.stack_settings.name)
90             return self.__stack
91         elif not cleanup:
92             self.__stack = heat_utils.create_stack(self.__heat_cli,
93                                                    self.stack_settings)
94             logger.info(
95                 'Created stack with name - ' + self.stack_settings.name)
96             if self.__stack and self.stack_complete(block=True):
97                 logger.info(
98                     'Stack is now active with name - ' +
99                     self.stack_settings.name)
100                 return self.__stack
101             else:
102                 raise StackCreationError(
103                     'Stack was not created or activated in the alloted amount '
104                     'of time')
105         else:
106             logger.info('Did not create stack due to cleanup mode')
107
108         return self.__stack
109
110     def clean(self):
111         """
112         Cleanse environment of all artifacts
113         :return: void
114         """
115         if self.__stack:
116             try:
117                 logger.info('Deleting stack - %s' + self.__stack.name)
118                 heat_utils.delete_stack(self.__heat_cli, self.__stack)
119
120                 try:
121                     self.stack_deleted(block=True)
122                 except StackError as e:
123                     # Stack deletion seems to fail quite a bit
124                     logger.warn('Stack did not delete properly - %s', e)
125
126                     # Delete VMs first
127                     for vm_inst_creator in self.get_vm_inst_creators():
128                         try:
129                             vm_inst_creator.clean()
130                             if not vm_inst_creator.vm_deleted(block=True):
131                                 logger.warn('Unable to deleted VM - %s',
132                                             vm_inst_creator.get_vm_inst().name)
133                         except:
134                             logger.warn('Unexpected error deleting VM - %s ',
135                                         vm_inst_creator.get_vm_inst().name)
136
137                 logger.info('Attempting to delete again stack - %s',
138                             self.__stack.name)
139
140                 # Delete Stack again
141                 heat_utils.delete_stack(self.__heat_cli, self.__stack)
142                 deleted = self.stack_deleted(block=True)
143                 if not deleted:
144                     raise StackError(
145                         'Stack could not be deleted ' + self.__stack.name)
146             except HTTPNotFound:
147                 pass
148
149             self.__stack = None
150
151     def get_stack(self):
152         """
153         Returns the domain Stack object as it was populated when create() was
154         called
155         :return: the object
156         """
157         return self.__stack
158
159     def get_outputs(self):
160         """
161         Returns the list of outputs as contained on the OpenStack Heat Stack
162         object
163         :return:
164         """
165         return heat_utils.get_outputs(self.__heat_cli, self.__stack)
166
167     def get_status(self):
168         """
169         Returns the list of outputs as contained on the OpenStack Heat Stack
170         object
171         :return:
172         """
173         return heat_utils.get_stack_status(self.__heat_cli, self.__stack.id)
174
175     def stack_complete(self, block=False, timeout=None,
176                        poll_interval=POLL_INTERVAL):
177         """
178         Returns true when the stack status returns the value of
179         expected_status_code
180         :param block: When true, thread will block until active or timeout
181                       value in seconds has been exceeded (False)
182         :param timeout: The timeout value
183         :param poll_interval: The polling interval in seconds
184         :return: T/F
185         """
186         if not timeout:
187             timeout = self.stack_settings.stack_create_timeout
188         return self._stack_status_check(STATUS_CREATE_COMPLETE, block, timeout,
189                                         poll_interval, STATUS_CREATE_FAILED)
190
191     def stack_deleted(self, block=False, timeout=None,
192                       poll_interval=POLL_INTERVAL):
193         """
194         Returns true when the stack status returns the value of
195         expected_status_code
196         :param block: When true, thread will block until active or timeout
197                       value in seconds has been exceeded (False)
198         :param timeout: The timeout value
199         :param poll_interval: The polling interval in seconds
200         :return: T/F
201         """
202         if not timeout:
203             timeout = self.stack_settings.stack_create_timeout
204         return self._stack_status_check(STATUS_DELETE_COMPLETE, block, timeout,
205                                         poll_interval, STATUS_DELETE_FAILED)
206
207     def get_network_creators(self):
208         """
209         Returns a list of network creator objects as configured by the heat
210         template
211         :return: list() of OpenStackNetwork objects
212         """
213
214         neutron = neutron_utils.neutron_client(self.__os_creds)
215
216         out = list()
217         stack_networks = heat_utils.get_stack_networks(
218             self.__heat_cli, neutron, self.__stack)
219
220         for stack_network in stack_networks:
221             net_settings = settings_utils.create_network_settings(
222                 neutron, stack_network)
223             net_creator = OpenStackNetwork(self.__os_creds, net_settings)
224             out.append(net_creator)
225             net_creator.create(cleanup=True)
226
227         return out
228
229     def get_vm_inst_creators(self, heat_keypair_option=None):
230         """
231         Returns a list of VM Instance creator objects as configured by the heat
232         template
233         :return: list() of OpenStackVmInstance objects
234         """
235
236         out = list()
237         nova = nova_utils.nova_client(self.__os_creds)
238
239         stack_servers = heat_utils.get_stack_servers(
240             self.__heat_cli, nova, self.__stack)
241
242         neutron = neutron_utils.neutron_client(self.__os_creds)
243         glance = glance_utils.glance_client(self.__os_creds)
244
245         for stack_server in stack_servers:
246             vm_inst_settings = settings_utils.create_vm_inst_settings(
247                 nova, neutron, stack_server)
248             image_settings = settings_utils.determine_image_settings(
249                 glance, stack_server, self.image_settings)
250             keypair_settings = settings_utils.determine_keypair_settings(
251                 self.__heat_cli, self.__stack, stack_server,
252                 keypair_settings=self.keypair_settings,
253                 priv_key_key=heat_keypair_option)
254             vm_inst_creator = OpenStackVmInstance(
255                 self.__os_creds, vm_inst_settings, image_settings,
256                 keypair_settings)
257             out.append(vm_inst_creator)
258             vm_inst_creator.create(cleanup=True)
259
260         return out
261
262     def _stack_status_check(self, expected_status_code, block, timeout,
263                             poll_interval, fail_status):
264         """
265         Returns true when the stack status returns the value of
266         expected_status_code
267         :param expected_status_code: stack status evaluated with this string
268                                      value
269         :param block: When true, thread will block until active or timeout
270                       value in seconds has been exceeded (False)
271         :param timeout: The timeout value
272         :param poll_interval: The polling interval in seconds
273         :param fail_status: Returns false if the fail_status code is found
274         :return: T/F
275         """
276         # sleep and wait for stack status change
277         if block:
278             start = time.time()
279         else:
280             start = time.time() - timeout
281
282         while timeout > time.time() - start:
283             status = self._status(expected_status_code, fail_status)
284             if status:
285                 logger.debug(
286                     'Stack is active with name - ' + self.stack_settings.name)
287                 return True
288
289             logger.debug('Retry querying stack status in ' + str(
290                 poll_interval) + ' seconds')
291             time.sleep(poll_interval)
292             logger.debug('Stack status query timeout in ' + str(
293                 timeout - (time.time() - start)))
294
295         logger.error(
296             'Timeout checking for stack status for ' + expected_status_code)
297         return False
298
299     def _status(self, expected_status_code, fail_status=STATUS_CREATE_FAILED):
300         """
301         Returns True when active else False
302         :param expected_status_code: stack status evaluated with this string
303         value
304         :return: T/F
305         """
306         status = self.get_status()
307         if not status:
308             logger.warning(
309                 'Cannot stack status for stack with ID - ' + self.__stack.id)
310             return False
311
312         if fail_status and status == fail_status:
313             raise StackError('Stack had an error')
314         logger.debug('Stack status is - ' + status)
315         return status == expected_status_code
316
317
318 class StackSettings:
319     def __init__(self, **kwargs):
320         """
321         Constructor
322         :param name: the stack's name (required)
323         :param template: the heat template in dict() format (required if
324                          template_path attribute is None)
325         :param template_path: the location of the heat template file (required
326                               if template attribute is None)
327         :param env_values: k/v pairs of strings for substitution of template
328                            default values (optional)
329         """
330
331         self.name = kwargs.get('name')
332         self.template = kwargs.get('template')
333         self.template_path = kwargs.get('template_path')
334         self.env_values = kwargs.get('env_values')
335         if 'stack_create_timeout' in kwargs:
336             self.stack_create_timeout = kwargs['stack_create_timeout']
337         else:
338             self.stack_create_timeout = STACK_COMPLETE_TIMEOUT
339
340         if not self.name:
341             raise StackSettingsError('name is required')
342
343         if not self.template and not self.template_path:
344             raise StackSettingsError('A Heat template is required')
345
346     def __eq__(self, other):
347         return (self.name == other.name and
348                 self.template == other.template and
349                 self.template_path == other.template_path and
350                 self.env_values == other.env_values and
351                 self.stack_create_timeout == other.stack_create_timeout)
352
353
354 class StackSettingsError(Exception):
355     """
356     Exception to be thrown when an stack settings are incorrect
357     """
358
359
360 class StackCreationError(Exception):
361     """
362     Exception to be thrown when an stack cannot be created
363     """
364
365
366 class StackError(Exception):
367     """
368     General exception
369     """