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