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