Added support for Heat resource files.
[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 Heat 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     if stack_settings.files:
121         args['files'] = stack_settings.files
122
123     stack = heat_cli.stacks.create(**args)
124
125     return get_stack_by_id(heat_cli, stack_id=stack['stack']['id'])
126
127
128 def delete_stack(heat_cli, stack):
129     """
130     Deletes the Heat stack
131     :param heat_cli: the OpenStack heat client object
132     :param stack: the OpenStack Heat stack object
133     """
134     heat_cli.stacks.delete(stack.id)
135
136
137 def __get_os_resources(heat_cli, stack):
138     """
139     Returns all of the OpenStack resource objects for a given stack
140     :param heat_cli: the OpenStack heat client
141     :param stack: the SNAPS-OO Stack domain object
142     :return: a list
143     """
144     return heat_cli.resources.list(stack.id)
145
146
147 def get_resources(heat_cli, stack, res_type=None):
148     """
149     Returns all of the OpenStack resource objects for a given stack
150     :param heat_cli: the OpenStack heat client
151     :param stack: the SNAPS-OO Stack domain object
152     :param res_type: the type name to filter
153     :return: a list of Resource domain objects
154     """
155     os_resources = __get_os_resources(heat_cli, stack)
156
157     if os_resources:
158         out = list()
159         for os_resource in os_resources:
160             if ((res_type and os_resource.resource_type == res_type)
161                     or not res_type):
162                 out.append(Resource(
163                     name=os_resource.resource_name,
164                     resource_type=os_resource.resource_type,
165                     resource_id=os_resource.physical_resource_id,
166                     status=os_resource.resource_status,
167                     status_reason=os_resource.resource_status_reason))
168         return out
169
170
171 def get_outputs(heat_cli, stack):
172     """
173     Returns all of the SNAPS-OO Output domain objects for the defined outputs
174     for given stack
175     :param heat_cli: the OpenStack heat client
176     :param stack: the SNAPS-OO Stack domain object
177     :return: a list of Output domain objects
178     """
179     out = list()
180
181     os_stack = heat_cli.stacks.get(stack.id)
182
183     outputs = None
184     if os_stack:
185         outputs = os_stack.outputs
186
187     if outputs:
188         for output in outputs:
189             out.append(Output(**output))
190
191     return out
192
193
194 def get_stack_networks(heat_cli, neutron, stack):
195     """
196     Returns a list of Network domain objects deployed by this stack
197     :param heat_cli: the OpenStack heat client object
198     :param neutron: the OpenStack neutron client object
199     :param stack: the SNAPS-OO Stack domain object
200     :return: a list of Network objects
201     """
202
203     out = list()
204     resources = get_resources(heat_cli, stack, 'OS::Neutron::Net')
205     for resource in resources:
206         network = neutron_utils.get_network_by_id(neutron, resource.id)
207         if network:
208             out.append(network)
209
210     return out
211
212
213 def get_stack_routers(heat_cli, neutron, stack):
214     """
215     Returns a list of Network domain objects deployed by this stack
216     :param heat_cli: the OpenStack heat client object
217     :param neutron: the OpenStack neutron client object
218     :param stack: the SNAPS-OO Stack domain object
219     :return: a list of Network objects
220     """
221
222     out = list()
223     resources = get_resources(heat_cli, stack, 'OS::Neutron::Router')
224     for resource in resources:
225         router = neutron_utils.get_router_by_id(neutron, resource.id)
226         if router:
227             out.append(router)
228
229     return out
230
231
232 def get_stack_security_groups(heat_cli, neutron, stack):
233     """
234     Returns a list of SecurityGroup domain objects deployed by this stack
235     :param heat_cli: the OpenStack heat client object
236     :param neutron: the OpenStack neutron client object
237     :param stack: the SNAPS-OO Stack domain object
238     :return: a list of SecurityGroup objects
239     """
240
241     out = list()
242     resources = get_resources(heat_cli, stack, 'OS::Neutron::SecurityGroup')
243     for resource in resources:
244         security_group = neutron_utils.get_security_group_by_id(
245             neutron, resource.id)
246         if security_group:
247             out.append(security_group)
248
249     return out
250
251
252 def get_stack_servers(heat_cli, nova, neutron, stack):
253     """
254     Returns a list of VMInst domain objects associated with a Stack
255     :param heat_cli: the OpenStack heat client object
256     :param nova: the OpenStack nova client object
257     :param neutron: the OpenStack neutron client object
258     :param stack: the SNAPS-OO Stack domain object
259     :return: a list of VMInst domain objects
260     """
261
262     out = list()
263     resources = get_resources(heat_cli, stack, 'OS::Nova::Server')
264     for resource in resources:
265         try:
266             server = nova_utils.get_server_object_by_id(
267                 nova, neutron, resource.id)
268             if server:
269                 out.append(server)
270         except NotFound:
271             logger.warn('VmInst cannot be located with ID %s', resource.id)
272
273     return out
274
275
276 def get_stack_keypairs(heat_cli, nova, stack):
277     """
278     Returns a list of Keypair domain objects associated with a Stack
279     :param heat_cli: the OpenStack heat client object
280     :param nova: the OpenStack nova client object
281     :param stack: the SNAPS-OO Stack domain object
282     :return: a list of VMInst domain objects
283     """
284
285     out = list()
286     resources = get_resources(heat_cli, stack, 'OS::Nova::KeyPair')
287     for resource in resources:
288         try:
289             keypair = nova_utils.get_keypair_by_id(nova, resource.id)
290             if keypair:
291                 out.append(keypair)
292         except NotFound:
293             logger.warn('Keypair cannot be located with ID %s', resource.id)
294
295     return out
296
297
298 def get_stack_volumes(heat_cli, cinder, stack):
299     """
300     Returns an instance of Volume domain objects created by this stack
301     :param heat_cli: the OpenStack heat client object
302     :param cinder: the OpenStack cinder client object
303     :param stack: the SNAPS-OO Stack domain object
304     :return: a list of Volume domain objects
305     """
306
307     out = list()
308     resources = get_resources(heat_cli, stack, 'OS::Cinder::Volume')
309     for resource in resources:
310         try:
311             server = cinder_utils.get_volume_by_id(cinder, resource.id)
312             if server:
313                 out.append(server)
314         except NotFound:
315             logger.warn('Volume cannot be located with ID %s', resource.id)
316
317     return out
318
319
320 def get_stack_volume_types(heat_cli, cinder, stack):
321     """
322     Returns an instance of VolumeType domain objects created by this stack
323     :param heat_cli: the OpenStack heat client object
324     :param cinder: the OpenStack cinder client object
325     :param stack: the SNAPS-OO Stack domain object
326     :return: a list of VolumeType domain objects
327     """
328
329     out = list()
330     resources = get_resources(heat_cli, stack, 'OS::Cinder::VolumeType')
331     for resource in resources:
332         try:
333             vol_type = cinder_utils.get_volume_type_by_id(cinder, resource.id)
334             if vol_type:
335                 out.append(vol_type)
336         except NotFound:
337             logger.warn('VolumeType cannot be located with ID %s', resource.id)
338
339     return out
340
341
342 def get_stack_flavors(heat_cli, nova, stack):
343     """
344     Returns an instance of Flavor SNAPS domain object for each flavor created
345     by this stack
346     :param heat_cli: the OpenStack heat client object
347     :param nova: the OpenStack cinder client object
348     :param stack: the SNAPS-OO Stack domain object
349     :return: a list of Volume domain objects
350     """
351
352     out = list()
353     resources = get_resources(heat_cli, stack, 'OS::Nova::Flavor')
354     for resource in resources:
355         try:
356             flavor = nova_utils.get_flavor_by_id(nova, resource.id)
357             if flavor:
358                 out.append(flavor)
359         except NotFound:
360             logger.warn('Flavor cannot be located with ID %s', resource.id)
361
362     return out
363
364
365 def parse_heat_template_str(tmpl_str):
366     """
367     Takes a heat template string, performs some simple validation and returns a
368     dict containing the parsed structure. This function supports both JSON and
369     YAML Heat template formats.
370     """
371     if tmpl_str.startswith('{'):
372         tpl = jsonutils.loads(tmpl_str)
373     else:
374         try:
375             tpl = yaml.load(tmpl_str, Loader=yaml_loader)
376         except yaml.YAMLError as yea:
377             raise ValueError(yea)
378         else:
379             if tpl is None:
380                 tpl = {}
381     # Looking for supported version keys in the loaded template
382     if not ('HeatTemplateFormatVersion' in tpl or
383             'heat_template_version' in tpl or
384             'AWSTemplateFormatVersion' in tpl):
385         raise ValueError("Template format version not found.")
386     return tpl