SNAPS Stack creators can now return SNAPS network creators.
[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_network import (
22     OpenStackNetwork, NetworkSettings, SubnetSettings)
23 from snaps.openstack.utils import heat_utils, neutron_utils
24
25 __author__ = 'spisarski'
26
27 logger = logging.getLogger('create_stack')
28
29 STACK_COMPLETE_TIMEOUT = 1200
30 POLL_INTERVAL = 3
31 STATUS_CREATE_FAILED = 'CREATE_FAILED'
32 STATUS_CREATE_COMPLETE = 'CREATE_COMPLETE'
33 STATUS_DELETE_COMPLETE = 'DELETE_COMPLETE'
34
35
36 class OpenStackHeatStack:
37     """
38     Class responsible for creating an heat stack in OpenStack
39     """
40
41     def __init__(self, os_creds, stack_settings):
42         """
43         Constructor
44         :param os_creds: The OpenStack connection credentials
45         :param stack_settings: The stack settings
46         :return:
47         """
48         self.__os_creds = os_creds
49         self.stack_settings = stack_settings
50         self.__stack = None
51         self.__heat_cli = None
52
53     def create(self, cleanup=False):
54         """
55         Creates the heat stack in OpenStack if it does not already exist and
56         returns the domain Stack object
57         :param cleanup: When true, this object is initialized only via queries,
58                         else objects will be created when the queries return
59                         None. The name of this parameter should be changed to
60                         something like 'readonly' as the same goes with all of
61                         the other creator classes.
62         :return: The OpenStack Stack object
63         """
64         self.__heat_cli = heat_utils.heat_client(self.__os_creds)
65         self.__stack = heat_utils.get_stack(
66             self.__heat_cli, stack_settings=self.stack_settings)
67         if self.__stack:
68             logger.info('Found stack with name - ' + self.stack_settings.name)
69             return self.__stack
70         elif not cleanup:
71             self.__stack = heat_utils.create_stack(self.__heat_cli,
72                                                    self.stack_settings)
73             logger.info(
74                 'Created stack with name - ' + self.stack_settings.name)
75             if self.__stack and self.stack_complete(block=True):
76                 logger.info(
77                     'Stack is now active with name - ' +
78                     self.stack_settings.name)
79                 return self.__stack
80             else:
81                 raise StackCreationError(
82                     'Stack was not created or activated in the alloted amount '
83                     'of time')
84         else:
85             logger.info('Did not create stack due to cleanup mode')
86
87         return self.__stack
88
89     def clean(self):
90         """
91         Cleanse environment of all artifacts
92         :return: void
93         """
94         if self.__stack:
95             try:
96                 heat_utils.delete_stack(self.__heat_cli, self.__stack)
97             except HTTPNotFound:
98                 pass
99
100         self.__stack = None
101
102     def get_stack(self):
103         """
104         Returns the domain Stack object as it was populated when create() was
105         called
106         :return: the object
107         """
108         return self.__stack
109
110     def get_outputs(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_outputs(self.__heat_cli, self.__stack.id)
117
118     def get_status(self):
119         """
120         Returns the list of outputs as contained on the OpenStack Heat Stack
121         object
122         :return:
123         """
124         return heat_utils.get_stack_status(self.__heat_cli, self.__stack.id)
125
126     def stack_complete(self, block=False, timeout=None,
127                        poll_interval=POLL_INTERVAL):
128         """
129         Returns true when the stack status returns the value of
130         expected_status_code
131         :param block: When true, thread will block until active or timeout
132                       value in seconds has been exceeded (False)
133         :param timeout: The timeout value
134         :param poll_interval: The polling interval in seconds
135         :return: T/F
136         """
137         if not timeout:
138             timeout = self.stack_settings.stack_create_timeout
139         return self._stack_status_check(STATUS_CREATE_COMPLETE, block, timeout,
140                                         poll_interval)
141
142     def get_network_creators(self):
143         """
144         Returns a list of network creator objects as configured by the heat
145         template
146         :return: list() of OpenStackNetwork objects
147         """
148
149         neutron = neutron_utils.neutron_client(self.__os_creds)
150
151         out = list()
152         stack_networks = heat_utils.get_stack_networks(
153             self.__heat_cli, neutron, self.__stack)
154
155         for stack_network in stack_networks:
156             net_settings = self.__create_network_settings(
157                 neutron, stack_network)
158             net_creator = OpenStackNetwork(self.__os_creds, net_settings)
159             out.append(net_creator)
160             net_creator.create(cleanup=True)
161
162         return out
163
164     def __create_network_settings(self, neutron, network):
165         """
166         Returns a NetworkSettings object
167         :param neutron: the neutron client
168         :param network: a SNAPS-OO Network domain object
169         :return:
170         """
171         return NetworkSettings(
172             name=network.name, network_type=network.type,
173             subnet_settings=self.__create_subnet_settings(neutron, network))
174
175     def __create_subnet_settings(self, neutron, network):
176         """
177         Returns a list of SubnetSettings objects for a given network
178         :param neutron: the OpenStack neutron client
179         :param network: the SNAPS-OO Network domain object
180         :return: a list
181         """
182         out = list()
183
184         subnets = neutron_utils.get_subnets_by_network(neutron, network)
185         for subnet in subnets:
186             kwargs = dict()
187             kwargs['cidr'] = subnet.cidr
188             kwargs['ip_version'] = subnet.ip_version
189             kwargs['name'] = subnet.name
190             kwargs['start'] = subnet.start
191             kwargs['end'] = subnet.end
192             kwargs['gateway_ip'] = subnet.gateway_ip
193             kwargs['enable_dhcp'] = subnet.enable_dhcp
194             kwargs['dns_nameservers'] = subnet.dns_nameservers
195             kwargs['host_routes'] = subnet.host_routes
196             kwargs['ipv6_ra_mode'] = subnet.ipv6_ra_mode
197             kwargs['ipv6_address_mode'] = subnet.ipv6_address_mode
198             out.append(SubnetSettings(**kwargs))
199         return out
200
201     def _stack_status_check(self, expected_status_code, block, timeout,
202                             poll_interval):
203         """
204         Returns true when the stack status returns the value of
205         expected_status_code
206         :param expected_status_code: stack status evaluated with this string
207                                      value
208         :param block: When true, thread will block until active or timeout
209                       value in seconds has been exceeded (False)
210         :param timeout: The timeout value
211         :param poll_interval: The polling interval in seconds
212         :return: T/F
213         """
214         # sleep and wait for stack status change
215         if block:
216             start = time.time()
217         else:
218             start = time.time() - timeout
219
220         while timeout > time.time() - start:
221             status = self._status(expected_status_code)
222             if status:
223                 logger.debug(
224                     'Stack is active with name - ' + self.stack_settings.name)
225                 return True
226
227             logger.debug('Retry querying stack status in ' + str(
228                 poll_interval) + ' seconds')
229             time.sleep(poll_interval)
230             logger.debug('Stack status query timeout in ' + str(
231                 timeout - (time.time() - start)))
232
233         logger.error(
234             'Timeout checking for stack status for ' + expected_status_code)
235         return False
236
237     def _status(self, expected_status_code):
238         """
239         Returns True when active else False
240         :param expected_status_code: stack status evaluated with this string
241         value
242         :return: T/F
243         """
244         status = self.get_status()
245         if not status:
246             logger.warning(
247                 'Cannot stack status for stack with ID - ' + self.__stack.id)
248             return False
249
250         if status == STATUS_CREATE_FAILED:
251             raise StackCreationError('Stack had an error during deployment')
252         logger.debug('Stack status is - ' + status)
253         return status == expected_status_code
254
255
256 class StackSettings:
257     def __init__(self, **kwargs):
258         """
259         Constructor
260         :param name: the stack's name (required)
261         :param template: the heat template in dict() format (required if
262                          template_path attribute is None)
263         :param template_path: the location of the heat template file (required
264                               if template attribute is None)
265         :param env_values: k/v pairs of strings for substitution of template
266                            default values (optional)
267         """
268
269         self.name = kwargs.get('name')
270         self.template = kwargs.get('template')
271         self.template_path = kwargs.get('template_path')
272         self.env_values = kwargs.get('env_values')
273         if 'stack_create_timeout' in kwargs:
274             self.stack_create_timeout = kwargs['stack_create_timeout']
275         else:
276             self.stack_create_timeout = STACK_COMPLETE_TIMEOUT
277
278         if not self.name:
279             raise StackSettingsError('name is required')
280
281         if not self.template and not self.template_path:
282             raise StackSettingsError('A Heat template is required')
283
284     def __eq__(self, other):
285         return (self.name == other.name and
286                 self.template == other.template and
287                 self.template_path == other.template_path and
288                 self.env_values == other.env_values and
289                 self.stack_create_timeout == other.stack_create_timeout)
290
291
292 class StackSettingsError(Exception):
293     """
294     Exception to be thrown when an stack settings are incorrect
295     """
296
297
298 class StackCreationError(Exception):
299     """
300     Exception to be thrown when an stack cannot be created
301     """