diff options
author | spisarski <s.pisarski@cablelabs.com> | 2017-06-02 15:31:53 -0600 |
---|---|---|
committer | spisarski <s.pisarski@cablelabs.com> | 2017-06-05 13:22:49 -0600 |
commit | 48da17bfedb683b624faf08d2e0b7552d56cff21 (patch) | |
tree | 9219ed4ab9872b26f7ff685c4d3378212a641d08 /snaps/openstack/utils | |
parent | c01f193cad22895f86f726f588a46e44ed4ab68a (diff) |
Added support for applying Heat Templates
Second patch expanded support to both files and dict() objects.
Third patch exposes new accessor for status and outputs.
JIRA: SNAPS-86
Change-Id: Ie7e8d883b4cc1a08dbe851fc9cbf663396334909
Signed-off-by: spisarski <s.pisarski@cablelabs.com>
Diffstat (limited to 'snaps/openstack/utils')
-rw-r--r-- | snaps/openstack/utils/heat_utils.py | 139 | ||||
-rw-r--r-- | snaps/openstack/utils/tests/heat_utils_tests.py | 143 |
2 files changed, 282 insertions, 0 deletions
diff --git a/snaps/openstack/utils/heat_utils.py b/snaps/openstack/utils/heat_utils.py new file mode 100644 index 0000000..d40e3b9 --- /dev/null +++ b/snaps/openstack/utils/heat_utils.py @@ -0,0 +1,139 @@ +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +import yaml +from heatclient.client import Client +from heatclient.common.template_format import yaml_loader +from oslo_serialization import jsonutils + +from snaps import file_utils +from snaps.domain.stack import Stack + +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('heat_utils') + + +def heat_client(os_creds): + """ + Retrieves the Heat client + :param os_creds: the OpenStack credentials + :return: the client + """ + logger.debug('Retrieving Nova Client') + return Client(1, session=keystone_utils.keystone_session(os_creds)) + + +def get_stack_by_name(heat_cli, stack_name): + """ + Returns a domain Stack object + :param heat_cli: the OpenStack heat client + :param stack_name: the name of the heat stack + :return: the Stack domain object else None + """ + stacks = heat_cli.stacks.list(**{'name': stack_name}) + for stack in stacks: + return Stack(name=stack.identifier, stack_id=stack.id) + + return None + + +def get_stack_by_id(heat_cli, stack_id): + """ + Returns a domain Stack object for a given ID + :param heat_cli: the OpenStack heat client + :param stack_id: the ID of the heat stack to retrieve + :return: the Stack domain object else None + """ + stack = heat_cli.stacks.get(stack_id) + return Stack(name=stack.identifier, stack_id=stack.id) + + +def get_stack_status(heat_cli, stack_id): + """ + Returns the current status of the Heat stack + :param heat_cli: the OpenStack heat client + :param stack_id: the ID of the heat stack to retrieve + :return: + """ + return heat_cli.stacks.get(stack_id).stack_status + + +def get_stack_outputs(heat_cli, stack_id): + """ + Returns a domain Stack object for a given ID + :param heat_cli: the OpenStack heat client + :param stack_id: the ID of the heat stack to retrieve + :return: the Stack domain object else None + """ + stack = heat_cli.stacks.get(stack_id) + return stack.outputs + + +def create_stack(heat_cli, stack_settings): + """ + Executes an Ansible playbook to the given host + :param heat_cli: the OpenStack heat client object + :param stack_settings: the stack configuration + :return: the Stack domain object + """ + args = dict() + + if stack_settings.template: + args['template'] = stack_settings.template + else: + args['template'] = parse_heat_template_str(file_utils.read_file(stack_settings.template_path)) + args['stack_name'] = stack_settings.name + + if stack_settings.env_values: + args['parameters'] = stack_settings.env_values + + stack = heat_cli.stacks.create(**args) + + return get_stack_by_id(heat_cli, stack_id=stack['stack']['id']) + + +def delete_stack(heat_cli, stack): + """ + Deletes the Heat stack + :param heat_cli: the OpenStack heat client object + :param stack: the OpenStack Heat stack object + """ + heat_cli.stacks.delete(stack.id) + + +def parse_heat_template_str(tmpl_str): + """Takes a heat template string, performs some simple validation and returns a dict containing the parsed structure. + This function supports both JSON and YAML Heat template formats. + """ + if tmpl_str.startswith('{'): + tpl = jsonutils.loads(tmpl_str) + else: + try: + tpl = yaml.load(tmpl_str, Loader=yaml_loader) + except yaml.YAMLError as yea: + raise ValueError(yea) + else: + if tpl is None: + tpl = {} + # Looking for supported version keys in the loaded template + if not ('HeatTemplateFormatVersion' in tpl or + 'heat_template_version' in tpl or + 'AWSTemplateFormatVersion' in tpl): + raise ValueError("Template format version not found.") + return tpl diff --git a/snaps/openstack/utils/tests/heat_utils_tests.py b/snaps/openstack/utils/tests/heat_utils_tests.py new file mode 100644 index 0000000..08387d8 --- /dev/null +++ b/snaps/openstack/utils/tests/heat_utils_tests.py @@ -0,0 +1,143 @@ +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import uuid + +import time + +from snaps.openstack import create_stack +from snaps.openstack.create_flavor import OpenStackFlavor, FlavorSettings + +from snaps.openstack.create_image import OpenStackImage +from snaps.openstack.create_stack import StackSettings +from snaps.openstack.tests import openstack_tests +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.utils import heat_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('nova_utils_tests') + + +class HeatSmokeTests(OSComponentTestCase): + """ + Tests to ensure that the nova client can communicate with the cloud + """ + + def test_nova_connect_success(self): + """ + Tests to ensure that the proper credentials can connect. + """ + heat = heat_utils.heat_client(self.os_creds) + + # This should not throw an exception + heat.stacks.list() + + def test_nova_connect_fail(self): + """ + Tests to ensure that the improper credentials cannot connect. + """ + from snaps.openstack.os_credentials import OSCreds + + nova = heat_utils.heat_client( + OSCreds(username='user', password='pass', auth_url=self.os_creds.auth_url, + project_name=self.os_creds.project_name, proxy_settings=self.os_creds.proxy_settings)) + + # This should throw an exception + with self.assertRaises(Exception): + nova.flavors.list() + + +class HeatUtilsCreateStackTests(OSComponentTestCase): + """ + Test basic nova keypair functionality + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + stack_name = self.__class__.__name__ + '-' + str(guid) + '-stack' + + self.image_creator = OpenStackImage( + self.os_creds, openstack_tests.cirros_image_settings( + name=self.__class__.__name__ + '-' + str(guid) + '-image')) + self.image_creator.create() + + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.os_creds, + FlavorSettings(name=guid + '-flavor', ram=128, disk=10, vcpus=1)) + self.flavor_creator.create() + + env_values = {'image_name': self.image_creator.image_settings.name, + 'flavor_name': self.flavor_creator.flavor_settings.name} + self.stack_settings = StackSettings(name=stack_name, template_path='../examples/heat/test_heat_template.yaml', + env_values=env_values) + self.stack = None + self.heat_client = heat_utils.heat_client(self.os_creds) + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.stack: + try: + heat_utils.delete_stack(self.heat_client, self.stack) + except: + pass + + if self.image_creator: + try: + self.image_creator.clean() + except: + pass + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except: + pass + + def test_create_stack(self): + """ + Tests the creation of an OpenStack keypair that does not exist. + """ + self.stack = heat_utils.create_stack(self.heat_client, self.stack_settings) + + stack_query_1 = heat_utils.get_stack_by_name(self.heat_client, self.stack_settings.name) + self.assertEqual(self.stack.id, stack_query_1.id) + + stack_query_2 = heat_utils.get_stack_by_id(self.heat_client, self.stack.id) + self.assertEqual(self.stack.id, stack_query_2.id) + + outputs = heat_utils.get_stack_outputs(self.heat_client, self.stack.id) + self.assertIsNotNone(outputs) + self.assertEqual(0, len(outputs)) + + end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT + + is_active = False + while time.time() < end_time: + status = heat_utils.get_stack_status(self.heat_client, self.stack.id) + if status == create_stack.STATUS_CREATE_COMPLETE: + is_active = True + break + + time.sleep(3) + + self.assertTrue(is_active) |