41cc7252c51addcf97d699ae47b7b385896f83b4
[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 from snaps.openstack.utils import heat_utils
21
22 __author__ = 'spisarski'
23
24 logger = logging.getLogger('create_stack')
25
26 STACK_COMPLETE_TIMEOUT = 1200
27 POLL_INTERVAL = 3
28 STATUS_CREATE_FAILED = 'CREATE_FAILED'
29 STATUS_CREATE_COMPLETE = 'CREATE_COMPLETE'
30 STATUS_DELETE_COMPLETE = 'DELETE_COMPLETE'
31
32
33 class OpenStackHeatStack:
34     """
35     Class responsible for creating an heat stack in OpenStack
36     """
37
38     def __init__(self, os_creds, stack_settings):
39         """
40         Constructor
41         :param os_creds: The OpenStack connection credentials
42         :param stack_settings: The stack settings
43         :return:
44         """
45         self.__os_creds = os_creds
46         self.stack_settings = stack_settings
47         self.__stack = None
48         self.__heat_cli = None
49
50     def create(self, cleanup=False):
51         """
52         Creates the heat stack in OpenStack if it does not already exist and
53         returns the domain Stack object
54         :param cleanup: Denotes whether or not this is being called for cleanup
55         :return: The OpenStack Stack object
56         """
57         self.__heat_cli = heat_utils.heat_client(self.__os_creds)
58         self.__stack = heat_utils.get_stack(
59             self.__heat_cli, stack_settings=self.stack_settings)
60         if self.__stack:
61             logger.info('Found stack with name - ' + self.stack_settings.name)
62             return self.__stack
63         elif not cleanup:
64             self.__stack = heat_utils.create_stack(self.__heat_cli,
65                                                    self.stack_settings)
66             logger.info(
67                 'Created stack with name - ' + self.stack_settings.name)
68             if self.__stack and self.stack_complete(block=True):
69                 logger.info(
70                     'Stack is now active with name - ' +
71                     self.stack_settings.name)
72                 return self.__stack
73             else:
74                 raise StackCreationError(
75                     'Stack was not created or activated in the alloted amount '
76                     'of time')
77         else:
78             logger.info('Did not create stack due to cleanup mode')
79
80         return self.__stack
81
82     def clean(self):
83         """
84         Cleanse environment of all artifacts
85         :return: void
86         """
87         if self.__stack:
88             try:
89                 heat_utils.delete_stack(self.__heat_cli, self.__stack)
90             except HTTPNotFound:
91                 pass
92
93         self.__stack = None
94
95     def get_stack(self):
96         """
97         Returns the domain Stack object as it was populated when create() was
98         called
99         :return: the object
100         """
101         return self.__stack
102
103     def get_outputs(self):
104         """
105         Returns the list of outputs as contained on the OpenStack Heat Stack
106         object
107         :return:
108         """
109         return heat_utils.get_stack_outputs(self.__heat_cli, self.__stack.id)
110
111     def get_status(self):
112         """
113         Returns the list of outputs as contained on the OpenStack Heat Stack
114         object
115         :return:
116         """
117         return heat_utils.get_stack_status(self.__heat_cli, self.__stack.id)
118
119     def stack_complete(self, block=False, timeout=None,
120                        poll_interval=POLL_INTERVAL):
121         """
122         Returns true when the stack status returns the value of
123         expected_status_code
124         :param block: When true, thread will block until active or timeout
125                       value in seconds has been exceeded (False)
126         :param timeout: The timeout value
127         :param poll_interval: The polling interval in seconds
128         :return: T/F
129         """
130         if not timeout:
131             timeout = self.stack_settings.stack_create_timeout
132         return self._stack_status_check(STATUS_CREATE_COMPLETE, block, timeout,
133                                         poll_interval)
134
135     def _stack_status_check(self, expected_status_code, block, timeout,
136                             poll_interval):
137         """
138         Returns true when the stack status returns the value of
139         expected_status_code
140         :param expected_status_code: stack status evaluated with this string
141                                      value
142         :param block: When true, thread will block until active or timeout
143                       value in seconds has been exceeded (False)
144         :param timeout: The timeout value
145         :param poll_interval: The polling interval in seconds
146         :return: T/F
147         """
148         # sleep and wait for stack status change
149         if block:
150             start = time.time()
151         else:
152             start = time.time() - timeout
153
154         while timeout > time.time() - start:
155             status = self._status(expected_status_code)
156             if status:
157                 logger.debug(
158                     'Stack is active with name - ' + self.stack_settings.name)
159                 return True
160
161             logger.debug('Retry querying stack status in ' + str(
162                 poll_interval) + ' seconds')
163             time.sleep(poll_interval)
164             logger.debug('Stack status query timeout in ' + str(
165                 timeout - (time.time() - start)))
166
167         logger.error(
168             'Timeout checking for stack status for ' + expected_status_code)
169         return False
170
171     def _status(self, expected_status_code):
172         """
173         Returns True when active else False
174         :param expected_status_code: stack status evaluated with this string
175         value
176         :return: T/F
177         """
178         status = self.get_status()
179         if not status:
180             logger.warning(
181                 'Cannot stack status for stack with ID - ' + self.__stack.id)
182             return False
183
184         if status == STATUS_CREATE_FAILED:
185             raise StackCreationError('Stack had an error during deployment')
186         logger.debug('Stack status is - ' + status)
187         return status == expected_status_code
188
189
190 class StackSettings:
191     def __init__(self, **kwargs):
192         """
193         Constructor
194         :param name: the stack's name (required)
195         :param template: the heat template in dict() format (required if
196                          template_path attribute is None)
197         :param template_path: the location of the heat template file (required
198                               if template attribute is None)
199         :param env_values: k/v pairs of strings for substitution of template
200                            default values (optional)
201         """
202
203         self.name = kwargs.get('name')
204         self.template = kwargs.get('template')
205         self.template_path = kwargs.get('template_path')
206         self.env_values = kwargs.get('env_values')
207         if 'stack_create_timeout' in kwargs:
208             self.stack_create_timeout = kwargs['stack_create_timeout']
209         else:
210             self.stack_create_timeout = STACK_COMPLETE_TIMEOUT
211
212         if not self.name:
213             raise StackSettingsError('name is required')
214
215         if not self.template and not self.template_path:
216             raise StackSettingsError('A Heat template is required')
217
218     def __eq__(self, other):
219         return (self.name == other.name and
220                 self.template == other.template and
221                 self.template_path == other.template_path and
222                 self.env_values == other.env_values and
223                 self.stack_create_timeout == other.stack_create_timeout)
224
225
226 class StackSettingsError(Exception):
227     """
228     Exception to be thrown when an stack settings are incorrect
229     """
230
231     def __init__(self, message):
232         Exception.__init__(self, message)
233
234
235 class StackCreationError(Exception):
236     """
237     Exception to be thrown when an stack cannot be created
238     """
239
240     def __init__(self, message):
241         Exception.__init__(self, message)