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