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