a90690b058be3c7dafa97332e45eed1b4d520c85
[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 delete_stack(heat_cli, stack):
148     """
149     Deletes the Heat stack
150     :param heat_cli: the OpenStack heat client object
151     :param stack: the OpenStack Heat stack object
152     """
153     heat_cli.stacks.delete(stack.id)
154
155
156 def __get_os_resources(heat_cli, res_id):
157     """
158     Returns all of the OpenStack resource objects for a given stack
159     :param heat_cli: the OpenStack heat client
160     :param res_id: the resource ID
161     :return: a list
162     """
163     return heat_cli.resources.list(res_id)
164
165
166 def get_resources(heat_cli, res_id, res_type=None):
167     """
168     Returns all of the OpenStack resource objects for a given stack
169     :param heat_cli: the OpenStack heat client
170     :param res_id: the SNAPS-OO Stack domain object
171     :param res_type: the type name to filter
172     :return: a list of Resource domain objects
173     """
174     os_resources = __get_os_resources(heat_cli, res_id)
175
176     if os_resources:
177         out = list()
178         for os_resource in os_resources:
179             if ((res_type and os_resource.resource_type == res_type)
180                     or not res_type):
181                 out.append(Resource(
182                     name=os_resource.resource_name,
183                     resource_type=os_resource.resource_type,
184                     resource_id=os_resource.physical_resource_id,
185                     status=os_resource.resource_status,
186                     status_reason=os_resource.resource_status_reason))
187         return out
188
189
190 def get_outputs(heat_cli, stack):
191     """
192     Returns all of the SNAPS-OO Output domain objects for the defined outputs
193     for given stack
194     :param heat_cli: the OpenStack heat client
195     :param stack: the SNAPS-OO Stack domain object
196     :return: a list of Output domain objects
197     """
198     out = list()
199
200     os_stack = heat_cli.stacks.get(stack.id)
201
202     outputs = None
203     if os_stack:
204         outputs = os_stack.outputs
205
206     if outputs:
207         for output in outputs:
208             out.append(Output(**output))
209
210     return out
211
212
213 def get_stack_networks(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.id, 'OS::Neutron::Net')
224     workers = []
225     for resource in resources:
226         worker = worker_pool().apply_async(neutron_utils.get_network_by_id,
227                                            (neutron, resource.id))
228         workers.append(worker)
229
230     for worker in workers:
231         network = worker.get()
232         if network:
233             out.append(network)
234
235     return out
236
237
238 def get_stack_routers(heat_cli, neutron, stack):
239     """
240     Returns a list of Network domain objects deployed by this stack
241     :param heat_cli: the OpenStack heat client object
242     :param neutron: the OpenStack neutron client object
243     :param stack: the SNAPS-OO Stack domain object
244     :return: a list of Network objects
245     """
246
247     out = list()
248     resources = get_resources(heat_cli, stack.id, 'OS::Neutron::Router')
249     workers = []
250     for resource in resources:
251         worker = worker_pool().apply_async(neutron_utils.get_router_by_id,
252                                            (neutron, resource.id))
253         workers.append(worker)
254
255     for worker in workers:
256         router = worker.get()
257         if router:
258             out.append(router)
259
260     return out
261
262
263 def get_stack_security_groups(heat_cli, neutron, stack):
264     """
265     Returns a list of SecurityGroup domain objects deployed by this stack
266     :param heat_cli: the OpenStack heat client object
267     :param neutron: the OpenStack neutron client object
268     :param stack: the SNAPS-OO Stack domain object
269     :return: a list of SecurityGroup objects
270     """
271
272     out = list()
273     resources = get_resources(heat_cli, stack.id, 'OS::Neutron::SecurityGroup')
274     workers = []
275     for resource in resources:
276         worker = worker_pool().apply_async(
277             neutron_utils.get_security_group_by_id,
278             (neutron, resource.id))
279         workers.append(worker)
280
281     for worker in workers:
282         security_group = worker.get()
283         if security_group:
284             out.append(security_group)
285
286     return out
287
288
289 def get_stack_servers(heat_cli, nova, neutron, keystone, stack, project_name):
290     """
291     Returns a list of VMInst domain objects associated with a Stack
292     :param heat_cli: the OpenStack heat client object
293     :param nova: the OpenStack nova client object
294     :param neutron: the OpenStack neutron client object
295     :param keystone: the OpenStack keystone client object
296     :param stack: the SNAPS-OO Stack domain object
297     :param project_name: the associated project ID
298     :return: a list of VMInst domain objects
299     """
300
301     out = list()
302     srvr_res = get_resources(heat_cli, stack.id, 'OS::Nova::Server')
303     workers = []
304     for resource in srvr_res:
305         worker = worker_pool().apply_async(
306             nova_utils.get_server_object_by_id,
307             (nova, neutron, keystone, resource.id, project_name))
308         workers.append((resource.id, worker))
309
310     for worker in workers:
311         resource_id = worker[0]
312         try:
313             server = worker[1].get()
314             if server:
315                 out.append(server)
316         except NotFound:
317             logger.warn('VmInst cannot be located with ID %s', resource_id)
318
319     res_grps = get_resources(heat_cli, stack.id, 'OS::Heat::ResourceGroup')
320     for res_grp in res_grps:
321         res_ress = get_resources(heat_cli, res_grp.id)
322         workers = []
323         for res_res in res_ress:
324             res_res_srvrs = get_resources(
325                 heat_cli, res_res.id, 'OS::Nova::Server')
326             for res_srvr in res_res_srvrs:
327                 worker = worker_pool().apply_async(
328                     nova_utils.get_server_object_by_id,
329                     (nova, neutron, keystone, res_srvr.id, project_name))
330                 workers.append(worker)
331
332         for worker in workers:
333             server = worker.get()
334             if server:
335                 out.append(server)
336
337     return out
338
339
340 def get_stack_keypairs(heat_cli, nova, stack):
341     """
342     Returns a list of Keypair domain objects associated with a Stack
343     :param heat_cli: the OpenStack heat client object
344     :param nova: the OpenStack nova client object
345     :param stack: the SNAPS-OO Stack domain object
346     :return: a list of VMInst domain objects
347     """
348
349     out = list()
350     resources = get_resources(heat_cli, stack.id, 'OS::Nova::KeyPair')
351     workers = []
352     for resource in resources:
353         worker = worker_pool().apply_async(
354             nova_utils.get_keypair_by_id, (nova, resource.id))
355         workers.append((resource.id, worker))
356
357     for worker in workers:
358         resource_id = worker[0]
359         try:
360             keypair = worker[1].get()
361             if keypair:
362                 out.append(keypair)
363         except NotFound:
364             logger.warn('Keypair cannot be located with ID %s', resource_id)
365
366     return out
367
368
369 def get_stack_volumes(heat_cli, cinder, stack):
370     """
371     Returns an instance of Volume domain objects created by this stack
372     :param heat_cli: the OpenStack heat client object
373     :param cinder: the OpenStack cinder client object
374     :param stack: the SNAPS-OO Stack domain object
375     :return: a list of Volume domain objects
376     """
377
378     out = list()
379     resources = get_resources(heat_cli, stack.id, 'OS::Cinder::Volume')
380     workers = []
381     for resource in resources:
382         worker = worker_pool().apply_async(
383             cinder_utils.get_volume_by_id, (cinder, resource.id))
384         workers.append((resource.id, worker))
385
386     for worker in workers:
387         resource_id = worker[0]
388         try:
389             server = worker[1].get()
390             if server:
391                 out.append(server)
392         except NotFound:
393             logger.warn('Volume cannot be located with ID %s', resource_id)
394
395     return out
396
397
398 def get_stack_volume_types(heat_cli, cinder, stack):
399     """
400     Returns an instance of VolumeType domain objects created by this stack
401     :param heat_cli: the OpenStack heat client object
402     :param cinder: the OpenStack cinder client object
403     :param stack: the SNAPS-OO Stack domain object
404     :return: a list of VolumeType domain objects
405     """
406
407     out = list()
408     resources = get_resources(heat_cli, stack.id, 'OS::Cinder::VolumeType')
409     workers = []
410     for resource in resources:
411         worker = worker_pool().apply_async(
412             cinder_utils.get_volume_type_by_id, (cinder, resource.id))
413         workers.append((resource.id, worker))
414
415     for worker in workers:
416         resource_id = worker[0]
417         try:
418             vol_type = worker[1].get()
419             if vol_type:
420                 out.append(vol_type)
421         except NotFound:
422             logger.warn('VolumeType cannot be located with ID %s', resource_id)
423
424     return out
425
426
427 def get_stack_flavors(heat_cli, nova, stack):
428     """
429     Returns an instance of Flavor SNAPS domain object for each flavor created
430     by this stack
431     :param heat_cli: the OpenStack heat client object
432     :param nova: the OpenStack cinder client object
433     :param stack: the SNAPS-OO Stack domain object
434     :return: a list of Volume domain objects
435     """
436
437     out = list()
438     resources = get_resources(heat_cli, stack.id, 'OS::Nova::Flavor')
439     workers = []
440     for resource in resources:
441         worker = worker_pool().apply_async(
442             nova_utils.get_flavor_by_id, (nova, resource.id))
443         workers.append((resource.id, worker))
444
445     for worker in workers:
446         resource_id = worker[0]
447         try:
448             flavor = worker[1].get()
449             if flavor:
450                 out.append(flavor)
451         except NotFound:
452             logger.warn('Flavor cannot be located with ID %s', resource_id)
453
454     return out
455
456
457 def parse_heat_template_str(tmpl_str):
458     """
459     Takes a heat template string, performs some simple validation and returns a
460     dict containing the parsed structure. This function supports both JSON and
461     YAML Heat template formats.
462     """
463     if tmpl_str.startswith('{'):
464         tpl = jsonutils.loads(tmpl_str)
465     else:
466         try:
467             tpl = yaml.load(tmpl_str, Loader=yaml_loader)
468         except yaml.YAMLError as yea:
469             raise ValueError(yea)
470         else:
471             if tpl is None:
472                 tpl = {}
473     # Looking for supported version keys in the loaded template
474     if not ('HeatTemplateFormatVersion' in tpl or
475             'heat_template_version' in tpl or
476             'AWSTemplateFormatVersion' in tpl):
477         raise ValueError("Template format version not found.")
478     return tpl