Added logging when a heat stack fails.
[snaps.git] / snaps / openstack / utils / heat_utils.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 import logging
16
17 import yaml
18 from heatclient.client import Client
19 from heatclient.common.template_format import yaml_loader
20 from novaclient.exceptions import NotFound
21 from oslo_serialization import jsonutils
22
23 from snaps import file_utils
24 from snaps.domain.stack import Stack, Resource, Output
25
26 from snaps.openstack.utils import (
27     keystone_utils, neutron_utils, nova_utils, cinder_utils)
28
29 __author__ = 'spisarski'
30
31 logger = logging.getLogger('heat_utils')
32
33
34 def heat_client(os_creds):
35     """
36     Retrieves the Heat client
37     :param os_creds: the OpenStack credentials
38     :return: the client
39     """
40     logger.debug('Retrieving Nova Client')
41     return Client(os_creds.heat_api_version,
42                   session=keystone_utils.keystone_session(os_creds),
43                   region_name=os_creds.region_name)
44
45
46 def get_stack(heat_cli, stack_settings=None, stack_name=None):
47     """
48     Returns the first domain Stack object found. When stack_setting
49     is not None, the filter created will take the name attribute. When
50     stack_settings is None and stack_name is not, stack_name will be used
51     instead. When both are None, the first stack object received will be
52     returned, else None
53     :param heat_cli: the OpenStack heat client
54     :param stack_settings: a StackSettings object
55     :param stack_name: the name of the heat stack to return
56     :return: the Stack domain object else None
57     """
58
59     stack_filter = dict()
60     if stack_settings:
61         stack_filter['stack_name'] = stack_settings.name
62     elif stack_name:
63         stack_filter['stack_name'] = stack_name
64
65     stacks = heat_cli.stacks.list(**stack_filter)
66     for stack in stacks:
67         return Stack(name=stack.identifier, stack_id=stack.id)
68
69
70 def get_stack_by_id(heat_cli, stack_id):
71     """
72     Returns a domain Stack object for a given ID
73     :param heat_cli: the OpenStack heat client
74     :param stack_id: the ID of the heat stack to retrieve
75     :return: the Stack domain object else None
76     """
77     stack = heat_cli.stacks.get(stack_id)
78     return Stack(name=stack.identifier, stack_id=stack.id)
79
80
81 def get_stack_status(heat_cli, stack_id):
82     """
83     Returns the current status of the Heat stack
84     :param heat_cli: the OpenStack heat client
85     :param stack_id: the ID of the heat stack to retrieve
86     :return:
87     """
88     return heat_cli.stacks.get(stack_id).stack_status
89
90
91 def get_stack_status_reason(heat_cli, stack_id):
92     """
93     Returns the current status of the Heat stack
94     :param heat_cli: the OpenStack heat client
95     :param stack_id: the ID of the heat stack to retrieve
96     :return: reason for stack creation failure
97     """
98     return heat_cli.stacks.get(stack_id).stack_status_reason
99
100
101 def create_stack(heat_cli, stack_settings):
102     """
103     Executes an Ansible playbook to the given host
104     :param heat_cli: the OpenStack heat client object
105     :param stack_settings: the stack configuration
106     :return: the Stack domain object
107     """
108     args = dict()
109
110     if stack_settings.template:
111         args['template'] = stack_settings.template
112     else:
113         args['template'] = parse_heat_template_str(
114             file_utils.read_file(stack_settings.template_path))
115     args['stack_name'] = stack_settings.name
116
117     if stack_settings.env_values:
118         args['parameters'] = stack_settings.env_values
119
120     stack = heat_cli.stacks.create(**args)
121
122     return get_stack_by_id(heat_cli, stack_id=stack['stack']['id'])
123
124
125 def delete_stack(heat_cli, stack):
126     """
127     Deletes the Heat stack
128     :param heat_cli: the OpenStack heat client object
129     :param stack: the OpenStack Heat stack object
130     """
131     heat_cli.stacks.delete(stack.id)
132
133
134 def __get_os_resources(heat_cli, stack):
135     """
136     Returns all of the OpenStack resource objects for a given stack
137     :param heat_cli: the OpenStack heat client
138     :param stack: the SNAPS-OO Stack domain object
139     :return: a list
140     """
141     return heat_cli.resources.list(stack.id)
142
143
144 def get_resources(heat_cli, stack, res_type=None):
145     """
146     Returns all of the OpenStack resource objects for a given stack
147     :param heat_cli: the OpenStack heat client
148     :param stack: the SNAPS-OO Stack domain object
149     :param res_type: the type name to filter
150     :return: a list of Resource domain objects
151     """
152     os_resources = __get_os_resources(heat_cli, stack)
153
154     if os_resources:
155         out = list()
156         for os_resource in os_resources:
157             if ((res_type and os_resource.resource_type == res_type)
158                 or not res_type):
159                 out.append(Resource(
160                     name=os_resource.resource_name,
161                     resource_type=os_resource.resource_type,
162                     resource_id=os_resource.physical_resource_id,
163                     status=os_resource.resource_status,
164                     status_reason=os_resource.resource_status_reason))
165         return out
166
167
168 def get_outputs(heat_cli, stack):
169     """
170     Returns all of the SNAPS-OO Output domain objects for the defined outputs
171     for given stack
172     :param heat_cli: the OpenStack heat client
173     :param stack: the SNAPS-OO Stack domain object
174     :return: a list of Output domain objects
175     """
176     out = list()
177
178     os_stack = heat_cli.stacks.get(stack.id)
179
180     outputs = None
181     if os_stack:
182         outputs = os_stack.outputs
183
184     if outputs:
185         for output in outputs:
186             out.append(Output(**output))
187
188     return out
189
190
191 def get_stack_networks(heat_cli, neutron, stack):
192     """
193     Returns a list of Network domain objects deployed by this stack
194     :param heat_cli: the OpenStack heat client object
195     :param neutron: the OpenStack neutron client object
196     :param stack: the SNAPS-OO Stack domain object
197     :return: a list of Network objects
198     """
199
200     out = list()
201     resources = get_resources(heat_cli, stack, 'OS::Neutron::Net')
202     for resource in resources:
203         network = neutron_utils.get_network_by_id(neutron, resource.id)
204         if network:
205             out.append(network)
206
207     return out
208
209
210 def get_stack_servers(heat_cli, nova, stack):
211     """
212     Returns a list of VMInst domain objects associated with a Stack
213     :param heat_cli: the OpenStack heat client object
214     :param nova: the OpenStack nova client object
215     :param stack: the SNAPS-OO Stack domain object
216     :return: a list of VMInst domain objects
217     """
218
219     out = list()
220     resources = get_resources(heat_cli, stack, 'OS::Nova::Server')
221     for resource in resources:
222         try:
223             server = nova_utils.get_server_object_by_id(nova, resource.id)
224             if server:
225                 out.append(server)
226         except NotFound:
227             logger.warn('VmInst cannot be located with ID %s', resource.id)
228
229     return out
230
231
232 def get_stack_keypairs(heat_cli, nova, stack):
233     """
234     Returns a list of Keypair domain objects associated with a Stack
235     :param heat_cli: the OpenStack heat client object
236     :param nova: the OpenStack nova client object
237     :param stack: the SNAPS-OO Stack domain object
238     :return: a list of VMInst domain objects
239     """
240
241     out = list()
242     resources = get_resources(heat_cli, stack, 'OS::Nova::KeyPair')
243     for resource in resources:
244         try:
245             keypair = nova_utils.get_keypair_by_id(nova, resource.id)
246             if keypair:
247                 out.append(keypair)
248         except NotFound:
249             logger.warn('Keypair cannot be located with ID %s', resource.id)
250
251     return out
252
253
254 def get_stack_volumes(heat_cli, cinder, stack):
255     """
256     Returns an instance of Volume domain objects created by this stack
257     :param heat_cli: the OpenStack heat client object
258     :param cinder: the OpenStack cinder client object
259     :param stack: the SNAPS-OO Stack domain object
260     :return: a list of Volume domain objects
261     """
262
263     out = list()
264     resources = get_resources(heat_cli, stack, 'OS::Cinder::Volume')
265     for resource in resources:
266         try:
267             server = cinder_utils.get_volume_by_id(cinder, resource.id)
268             if server:
269                 out.append(server)
270         except NotFound:
271             logger.warn('Volume cannot be located with ID %s', resource.id)
272
273     return out
274
275
276 def get_stack_volume_types(heat_cli, cinder, stack):
277     """
278     Returns an instance of VolumeType domain objects created by this stack
279     :param heat_cli: the OpenStack heat client object
280     :param cinder: the OpenStack cinder client object
281     :param stack: the SNAPS-OO Stack domain object
282     :return: a list of VolumeType domain objects
283     """
284
285     out = list()
286     resources = get_resources(heat_cli, stack, 'OS::Cinder::VolumeType')
287     for resource in resources:
288         try:
289             vol_type = cinder_utils.get_volume_type_by_id(cinder, resource.id)
290             if vol_type:
291                 out.append(vol_type)
292         except NotFound:
293             logger.warn('VolumeType cannot be located with ID %s', resource.id)
294
295     return out
296
297
298 def get_stack_flavors(heat_cli, nova, stack):
299     """
300     Returns an instance of Flavor SNAPS domain object for each flavor created
301     by this stack
302     :param heat_cli: the OpenStack heat client object
303     :param nova: the OpenStack cinder client object
304     :param stack: the SNAPS-OO Stack domain object
305     :return: a list of Volume domain objects
306     """
307
308     out = list()
309     resources = get_resources(heat_cli, stack, 'OS::Nova::Flavor')
310     for resource in resources:
311         try:
312             flavor = nova_utils.get_flavor_by_id(nova, resource.id)
313             if flavor:
314                 out.append(flavor)
315         except NotFound:
316             logger.warn('Flavor cannot be located with ID %s', resource.id)
317
318     return out
319
320
321 def parse_heat_template_str(tmpl_str):
322     """
323     Takes a heat template string, performs some simple validation and returns a
324     dict containing the parsed structure. This function supports both JSON and
325     YAML Heat template formats.
326     """
327     if tmpl_str.startswith('{'):
328         tpl = jsonutils.loads(tmpl_str)
329     else:
330         try:
331             tpl = yaml.load(tmpl_str, Loader=yaml_loader)
332         except yaml.YAMLError as yea:
333             raise ValueError(yea)
334         else:
335             if tpl is None:
336                 tpl = {}
337     # Looking for supported version keys in the loaded template
338     if not ('HeatTemplateFormatVersion' in tpl or
339             'heat_template_version' in tpl or
340             'AWSTemplateFormatVersion' in tpl):
341         raise ValueError("Template format version not found.")
342     return tpl