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