1 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
2 # and others. All rights reserved.
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:
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
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
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
31 __author__ = 'spisarski'
33 logger = logging.getLogger('heat_utils')
36 def heat_client(os_creds, session=None):
38 Retrieves the Heat client
39 :param os_creds: the OpenStack credentials
42 logger.debug('Retrieving Heat Client')
44 session = keystone_utils.keystone_session(os_creds)
45 return Client(os_creds.heat_api_version,
47 region_name=os_creds.region_name)
50 def get_stack(heat_cli, stack_settings=None, stack_name=None):
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
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
65 stack_filter['stack_name'] = stack_settings.name
67 stack_filter['stack_name'] = stack_name
69 stacks = heat_cli.stacks.list(**stack_filter)
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)
78 def get_stack_by_id(heat_cli, stack_id):
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
85 stack = heat_cli.stacks.get(stack_id)
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)
93 def get_stack_status(heat_cli, stack_id):
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
100 return heat_cli.stacks.get(stack_id).stack_status
103 def get_stack_status_reason(heat_cli, stack_id):
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
110 return heat_cli.stacks.get(stack_id).stack_status_reason
113 def create_stack(heat_cli, stack_settings):
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
122 if stack_settings.template:
123 args['template'] = stack_settings.template
125 args['template'] = parse_heat_template_str(
126 file_utils.read_file(stack_settings.template_path))
127 args['stack_name'] = stack_settings.name
129 if stack_settings.env_values:
130 args['parameters'] = stack_settings.env_values
132 if stack_settings.resource_files:
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)
138 if heat_resource_contents and base_filename:
139 resources[base_filename] = heat_resource_contents
140 args['files'] = resources
142 stack = heat_cli.stacks.create(**args)
144 return get_stack_by_id(heat_cli, stack_id=stack['stack']['id'])
147 def update_stack(heat_cli, stack, env_vals):
149 Updates the specified parameters in the stack
150 :param heat_cli: the OpenStack heat client object
151 :param stack_settings: the stack configuration
155 args['stack_name'] = stack.name
156 args['existing'] = True
159 args['parameters'] = env_vals
160 heat_cli.stacks.update(stack.id, **args)
162 logger.warn('Stack not updated, env_vals are None')
165 def delete_stack(heat_cli, stack):
167 Deletes the Heat stack
168 :param heat_cli: the OpenStack heat client object
169 :param stack: the OpenStack Heat stack object
171 heat_cli.stacks.delete(stack.id)
174 def __get_os_resources(heat_cli, res_id):
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
181 return heat_cli.resources.list(res_id)
184 def get_resources(heat_cli, res_id, res_type=None):
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
192 os_resources = __get_os_resources(heat_cli, res_id)
196 for os_resource in os_resources:
197 if ((res_type and os_resource.resource_type == res_type)
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))
208 def get_outputs(heat_cli, stack):
210 Returns all of the SNAPS-OO Output domain objects for the defined outputs
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
218 os_stack = heat_cli.stacks.get(stack.id)
222 outputs = os_stack.outputs
225 for output in outputs:
226 out.append(Output(**output))
231 def get_stack_networks(heat_cli, neutron, stack):
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
241 resources = get_resources(heat_cli, stack.id, 'OS::Neutron::Net')
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)
248 for worker in workers:
249 network = worker.get()
256 def get_stack_routers(heat_cli, neutron, stack):
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
266 resources = get_resources(heat_cli, stack.id, 'OS::Neutron::Router')
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)
273 for worker in workers:
274 router = worker.get()
281 def get_stack_security_groups(heat_cli, neutron, stack):
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
291 resources = get_resources(heat_cli, stack.id, 'OS::Neutron::SecurityGroup')
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)
299 for worker in workers:
300 security_group = worker.get()
302 out.append(security_group)
307 def get_stack_servers(heat_cli, nova, neutron, keystone, stack, project_name):
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
320 srvr_res = get_resources(heat_cli, stack.id, 'OS::Nova::Server')
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))
328 for worker in workers:
329 resource_id = worker[0]
331 server = worker[1].get()
335 logger.warn('VmInst cannot be located with ID %s', resource_id)
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)
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)
350 for worker in workers:
351 server = worker.get()
358 def get_stack_keypairs(heat_cli, nova, stack):
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
368 resources = get_resources(heat_cli, stack.id, 'OS::Nova::KeyPair')
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))
375 for worker in workers:
376 resource_id = worker[0]
378 keypair = worker[1].get()
382 logger.warn('Keypair cannot be located with ID %s', resource_id)
387 def get_stack_volumes(heat_cli, cinder, stack):
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
397 resources = get_resources(heat_cli, stack.id, 'OS::Cinder::Volume')
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))
404 for worker in workers:
405 resource_id = worker[0]
407 server = worker[1].get()
411 logger.warn('Volume cannot be located with ID %s', resource_id)
416 def get_stack_volume_types(heat_cli, cinder, stack):
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
426 resources = get_resources(heat_cli, stack.id, 'OS::Cinder::VolumeType')
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))
433 for worker in workers:
434 resource_id = worker[0]
436 vol_type = worker[1].get()
440 logger.warn('VolumeType cannot be located with ID %s', resource_id)
445 def get_stack_flavors(heat_cli, nova, stack):
447 Returns an instance of Flavor SNAPS domain object for each flavor created
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
456 resources = get_resources(heat_cli, stack.id, 'OS::Nova::Flavor')
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))
463 for worker in workers:
464 resource_id = worker[0]
466 flavor = worker[1].get()
470 logger.warn('Flavor cannot be located with ID %s', resource_id)
475 def parse_heat_template_str(tmpl_str):
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.
481 if tmpl_str.startswith('{'):
482 tpl = jsonutils.loads(tmpl_str)
485 tpl = yaml.load(tmpl_str, Loader=yaml_loader)
486 except yaml.YAMLError as yea:
487 raise ValueError(yea)
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.")