diff --git a/tripleo_common/core/__init__.py b/tripleo_common/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tripleo_common/core/constants.py b/tripleo_common/core/constants.py new file mode 100644 index 000000000..ed06e28e2 --- /dev/null +++ b/tripleo_common/core/constants.py @@ -0,0 +1,28 @@ +# Copyright 2015 Red Hat, Inc. +# 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. + +# TRIPLEO_META_USAGE_KEY is inserted into metadata for containers created in +# Swift via SwiftPlanStorageBackend to identify them from other containers +TRIPLEO_META_USAGE_KEY = 'x-container-meta-usage-tripleo' + +# OBJECT_META_KEY_PREFIX is used to prefix Swift metadata keys per object +# in SwiftPlanStorageBackend +OBJECT_META_KEY_PREFIX = 'x-object-meta-' + +# The following keys are used when identifying metadata from the capabilities +# map file +ROOT_TEMPLATE_META = {'file-type': 'root-template'} +ROOT_ENVIRONMENT_META = {'file-type': 'root-environment', 'enabled': 'True'} +ENVIRONMENT_META = {'file-type': 'environment'} diff --git a/tripleo_common/core/exception.py b/tripleo_common/core/exception.py new file mode 100644 index 000000000..4d5e53e50 --- /dev/null +++ b/tripleo_common/core/exception.py @@ -0,0 +1,88 @@ +# Copyright 2015 Red Hat, Inc. +# 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 six +from six import reraise as raise_ +import sys + +from tripleo_common.core.i18n import _ +from tripleo_common.core.i18n import _LE + +_FATAL_EXCEPTION_FORMAT_ERRORS = False + +LOG = logging.getLogger(__name__) + + +@six.python_2_unicode_compatible +class TripleoCommonException(Exception): + """Base Tripleo-Common Exception. + + To correctly use this class, inherit from it and define a 'msg_fmt' + property. That msg_fmt will get printf'd with the keyword arguments + provided to the constructor. + """ + message = _("An unknown exception occurred.") + + def __init__(self, **kwargs): + self.kwargs = kwargs + + try: + self.message = self.msg_fmt % kwargs + except KeyError: + exc_info = sys.exc_info() + # kwargs doesn't match a variable in the message + # log the issue and the kwargs + LOG.exception(_LE('Exception in string format operation')) + for name, value in six.iteritems(kwargs): + LOG.error(_LE("%(name)s: %(value)s"), + {'name': name, 'value': value}) # noqa + + if _FATAL_EXCEPTION_FORMAT_ERRORS: + raise_(exc_info[0], exc_info[1], exc_info[2]) + + def __str__(self): + return self.message + + def __deepcopy__(self, memo): + return self.__class__(**self.kwargs) + + +class StackInUseError(TripleoCommonException): + msg_fmt = _("Cannot delete a plan that has an associated stack.") + + +class PlanDoesNotExistError(TripleoCommonException): + msg_fmt = _("A plan with the name %(name) does not exist.") + + +class PlanAlreadyExistsError(TripleoCommonException): + msg_fmt = _("A plan with the name %(name) already exists.") + + +class TooManyRootTemplatesError(TripleoCommonException): + msg_fmt = _("There can only be up to one root template in a given plan.") + + +class HeatValidationFailedError(TripleoCommonException): + msg_fmt = _("The plan failed to validate via the Heat service.") + + +class MappingFileNotFoundError(TripleoCommonException): + msg_fmt = _("The capabilities_map.yaml file was not found in the root" + " of the plan.") + + +class TooManyCapabilitiesMapFilesError(TripleoCommonException): + msg_fmt = _("There can only be up to one root template in a given plan.") diff --git a/tripleo_common/core/i18n.py b/tripleo_common/core/i18n.py new file mode 100644 index 000000000..79243d49f --- /dev/null +++ b/tripleo_common/core/i18n.py @@ -0,0 +1,35 @@ +# Copyright 2015 Red Hat, Inc. +# Copyright 2014 IBM Corp. +# 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. + +# http://docs.openstack.org/developer/oslo.i18n/usage.html + +import oslo_i18n as i18n + + +_translators = i18n.TranslatorFactory(domain='tripleo') + +# The primary translation function using the well-known name "_" +_ = _translators.primary + +# Translators for log levels. +# +# The abbreviated names are meant to reflect the usual use of a short +# name like '_'. The "L" is for "log" and the other letter comes from +# the level. +_LI = _translators.log_info +_LW = _translators.log_warning +_LE = _translators.log_error +_LC = _translators.log_critical diff --git a/tripleo_common/core/models.py b/tripleo_common/core/models.py new file mode 100644 index 000000000..5677b0409 --- /dev/null +++ b/tripleo_common/core/models.py @@ -0,0 +1,27 @@ +# Copyright 2015 Red Hat, Inc. +# 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 datetime + + +class Plan(object): + + def __init__(self, name): + self.name = name + self.files = {} + self.metadata = {} + + def created_date(self): + return datetime.datetime.fromtimestamp( + float(self.metadata['x-timestamp'])) diff --git a/tripleo_common/core/plan.py b/tripleo_common/core/plan.py new file mode 100644 index 000000000..30e0a820e --- /dev/null +++ b/tripleo_common/core/plan.py @@ -0,0 +1,241 @@ +# Copyright 2015 Red Hat, Inc. +# 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 import exc as heatexceptions +import six +from swiftclient import exceptions as swiftexceptions + +from tripleo_common.core import exception +from tripleo_common.utils import meta +from tripleo_common.utils import templates + +LOG = logging.getLogger(__name__) + + +class PlanManager(object): + + def __init__(self, plan_storage_backend, heatclient): + # TODO(rbrady) add code to create a storage backend based on config + # so the API can send a string representing a backend type and any + # client objects needed by the backend type + self.plan_store = plan_storage_backend + self.heatclient = heatclient + + def create_plan(self, plan_name, plan_files): + """Creates a plan to store templates + + Creates a plan by creating a container matching plan_name, and + import given plan_files into it. The plan files is a dictionary + where the keys are filenames and the values are file contents. + + :param plan_name: The name of the plan to use as the container name + :type plan_name: str + :param plan_files: The files to import into the container. + :type plan_files: dict + """ + + # create container with versioning + try: + self.plan_store.create(plan_name) + except Exception: + LOG.exception("Error creating plan.") + raise + + plan_files = meta.add_file_metadata(plan_files) + return self.update_plan(plan_name, plan_files) + + def delete_plan(self, plan_name): + """Deletes a plan and associated files + + Deletes a plan by deleting the container matching plan_name. It + will not delete the plan if a stack exists with the same name. + + Raises StackInUseError if a stack with the same name as plan_name + exists. + + :param plan_name: The name of the container to delete + :type plan_name: str + """ + # heat throws HTTPNotFound if the stack is not found + try: + stack = self.heatclient.stacks.get(plan_name) + if stack is not None: + raise exception.StackInUseError(name=plan_name) + except heatexceptions.HTTPNotFound: + try: + self.plan_store.delete(plan_name) + except swiftexceptions.ClientException as ce: + LOG.exception("Swift error deleting plan.") + if ce.http_status == 404: + six.raise_from(exception.PlanDoesNotExistError(), ce) + except Exception: + LOG.exception("Error deleting plan.") + raise + + def delete_temporary_environment(self, plan_name): + """Deletes the temporary environment files + + The temporary environment is the combination of deployment parameters + and selected environment information + + :param plan_name: The name of the plan to use as the container name + :type plan_name: str + """ + plan = self.get_plan(plan_name) + for item in {k: v for (k, v) in plan.files.items() if + v.get('meta', {}).get('file-type') == 'temp-environment'}: + self.plan_store.delete_file(plan_name, item) + + def get_plan(self, plan_name): + """Retrieves the Heat templates and environment file + + Retrieves the files from the container matching plan_name. + + :param plan_name: The name of the plan to retrieve files for. + :type plan_name: str + :rtype dict + """ + + try: + return self.plan_store.get(plan_name) + except Exception: + LOG.exception("Error retrieving plan.") + raise + + def get_plan_list(self): + """Gets a list of containers that store plans + + Gets a list of containers that contain metadata with the key of + X-Container-Meta-Usage-Tripleo and value or 'plan'. + + :return: a list of strings containing plan names + """ + try: + return self.plan_store.list() + except Exception: + LOG.exception("Error retrieving plan list.") + raise + + def get_deployment_parameters(self, plan_name): + """Determine available deployment parameters + + :param plan_name: The name of the plan and container name + """ + + plan = self.get_plan(plan_name) + template, environment, files = templates.process_plan_data(plan.files) + try: + params = self.heatclient.stacks.validate( + template=template, + files=files, + environment=environment, + show_nested=True) + except heatexceptions.HTTPBadRequest as exc: + six.raise_from(exception.HeatValidationFailedError(), exc) + return params + + def update_deployment_parameters(self, plan_name, deployment_parameters): + """Update the deployment parameters + + :param plan_name: The name of the plan and container name + :type plan_name: str + :param deployment_parameters: dictionary of deployment parameters + :type deployment_parameters: dict + """ + + plan = self.get_plan(plan_name) + deployment_params_file = 'environments/deployment_parameters.yaml' + # Make sure the dict has the expected environment file format. + if not deployment_parameters.get('parameter_defaults'): + deployment_parameters = { + 'parameter_defaults': deployment_parameters + } + # pop the deployment params temporary environment file from the plan + # so it's not included in the validation call. If the stack is valid + # the deployment params temporary environment file is overwritten + if deployment_params_file in plan.files: + plan.files.pop(deployment_params_file) + # Update deployment params and validate through heat API. + template, environment, files = templates.process_plan_data(plan.files) + environment = templates.deep_update(environment, deployment_parameters) + try: + self.heatclient.stacks.validate( + template=template, + files=files, + environment=environment, + show_nested=True) + except heatexceptions.HTTPBadRequest as exc: + six.raise_from(exception.HeatValidationFailedError(), exc) + + env = yaml.dump(deployment_parameters, default_flow_style=False) + plan.files[deployment_params_file] = { + 'contents': env, + 'meta': { + 'file-type': 'temp-environment', + } + } + self.update_plan(plan_name, plan.files) + + def update_plan(self, plan_name, plan_files): + """Updates files in a plan container + + :param plan_name: The name of the plan to use as the container name + :type plan_name: str + :param plan_files: The files to import into the container. + :type plan_files: dict + """ + + try: + self.plan_store.update(plan_name, plan_files) + except Exception: + LOG.exception("Error updating plan.") + raise + + return self.get_plan(plan_name) + + def validate_plan(self, plan_name): + """Validate Plan + + This private method provides validations to ensure a plan + meets the proper criteria before allowed to persist in storage. + + :param plan_files: The files to import into the container. + :type plan_files: dict + :returns boolean + """ + + plan = self.get_plan(plan_name) + # there can only be up to one root-template file in metadata + rt = {k: v for (k, v) in plan.files.items() + if v.get('meta', {}).get('file-type') == 'root-template'} + if len(rt) > 1: + raise exception.TooManyRootTemplatesError() + + # the plan needs to be validated with heat to ensure it conforms + template, environment, files = templates.process_plan_data(plan.files) + try: + self.heatclient.stacks.validate( + template=template, + files=files, + environment=environment, + show_nested=True) + except heatexceptions.HTTPBadRequest as exc: + LOG.exception("Error validating the plan.") + six.raise_from(exception.HeatValidationFailedError(), exc) + + # no validation issues found + return True diff --git a/tripleo_common/core/plan_storage.py b/tripleo_common/core/plan_storage.py new file mode 100644 index 000000000..5126e3100 --- /dev/null +++ b/tripleo_common/core/plan_storage.py @@ -0,0 +1,135 @@ +# Copyright 2015 Red Hat, Inc. +# 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. +from tripleo_common.core import constants +from tripleo_common.core import exception +from tripleo_common.core.models import Plan +from tripleo_common.utils import meta + +default_container_headers = { + 'X-Versions-Location': 'versions', + constants.TRIPLEO_META_USAGE_KEY: 'plan' +} + + +class SwiftPlanStorageBackend(object): + def __init__(self, swiftclient): + self.swiftclient = swiftclient + + def create(self, plan_name): + """Creates a plan to store files + + Creates a plan by creating a Swift container matching plan_name, and + given files into it. + + :param plan_name: The name of the plan + :type plan_name: str + :param plan_files: names and contents of files to store + :type plan_files: dict + """ + if plan_name not in self.list(): + self.swiftclient.put_container( + plan_name, + headers=default_container_headers + ) + else: + raise exception.PlanAlreadyExistsError() + + def delete(self, plan_name): + """Deletes a plan and associated files + + Deletes a plan by deleting the Swift container matching plan_name. + + :param plan_name: The name of the plan + :type plan_name: str + """ + # delete files from plan + for data in self.swiftclient.get_container(plan_name)[1]: + self.swiftclient.delete_object(plan_name, data['name']) + # delete plan container + self.swiftclient.delete_container(plan_name) + + def delete_file(self, plan_name, filepath): + """Deletes a file for a given filepath from a plan container + + :param plan_name: The name of the plan + :type plan_name: str + :param filepath: The path of the file to be deleted + """ + self.swiftclient.delete_object(plan_name, filepath) + + def get(self, plan_name): + """Retrieves the files for a given container name + + Retrieves the files from the Swift container matching plan_name. + + :param plan_name: The name of the plan + :type plan_name: str + :return: a list of files + :rtype list + """ + plan = Plan(plan_name) + container = self.swiftclient.get_container(plan_name) + plan.metadata = container[0] + for data in container[1]: + filename = data['name'] + plan_obj = self.swiftclient.get_object(plan_name, filename) + plan.files[filename] = {} + plan.files[filename]['contents'] = plan_obj[1] + meta_info = {k: v for (k, v) in plan_obj[0].items() + if constants.OBJECT_META_KEY_PREFIX in k} + if len(meta_info) > 0: + plan.files[filename]['meta'] = \ + meta.remove_key_prefix(meta_info) + + return plan + + def list(self): + """Gets a list of containers that store plans + + Gets a list of containers that contain metadata with the key of + X-Container-Meta-Usage-Tripleo and value or 'plan'. + + :return: a list of strings containing plan names + """ + plan_list = [] + for item in self.swiftclient.get_account()[1]: + container = self.swiftclient.get_container(item['name'])[0] + if constants.TRIPLEO_META_USAGE_KEY in container.keys(): + plan_list.append(item['name']) + + return plan_list + + def update(self, plan_name, plan_files): + """Updates a plan by updating the files in container + + Updates a plan by updating the files in the Swift container + matching plan_name. + + :param plan_name: The name of the plan + :type plan_name: str + :param plan_files: names and contents of files to store + :type plan_files: dict + """ + + for filename, details in plan_files.items(): + custom_headers = {} + if 'meta' in details: + custom_headers = meta.add_key_prefix(details['meta']) + self.swiftclient.put_object( + plan_name, + filename, + details['contents'], + headers=custom_headers + ) diff --git a/tripleo_common/tests/core/__init__.py b/tripleo_common/tests/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tripleo_common/tests/core/test_models.py b/tripleo_common/tests/core/test_models.py new file mode 100644 index 000000000..39dd927f7 --- /dev/null +++ b/tripleo_common/tests/core/test_models.py @@ -0,0 +1,50 @@ +# Copyright 2015 Red Hat, Inc. +# 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 datetime +import time + +from tripleo_common.core.models import Plan +from tripleo_common.tests import base + + +class ModelTest(base.TestCase): + + def setUp(self): + super(ModelTest, self).setUp() + self.timestamp = time.time() + + def test_plan(self): + plan = Plan('overcloud') + plan.metadata = { + 'x-container-meta-usage-tripleo': 'plan', + 'accept-ranges': 'bytes', + 'x-storage-policy': 'Policy-0', + 'connection': 'keep-alive', + 'x-timestamp': self.timestamp, + 'x-trans-id': 'tx1f41a9d34a2a437d8f8dd-00565dd486', + 'content-type': 'application/json; charset=utf-8', + 'x-versions-location': 'versions' + } + plan.files = { + 'some-name.yaml': { + 'contents': "some fake contents", + 'meta': {'file-type': 'environment'} + }, + } + + expected_date = datetime.datetime.fromtimestamp( + float(self.timestamp)) + self.assertEqual(expected_date, plan.created_date(), "Date mismatch") diff --git a/tripleo_common/tests/core/test_plan.py b/tripleo_common/tests/core/test_plan.py new file mode 100644 index 000000000..7aded250a --- /dev/null +++ b/tripleo_common/tests/core/test_plan.py @@ -0,0 +1,338 @@ +# Copyright 2015 Red Hat, Inc. +# 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 mock + +from heatclient import exc as heatexceptions +from swiftclient import exceptions as swiftexceptions + +from tripleo_common.core import exception +from tripleo_common.core import models +from tripleo_common.core import plan +from tripleo_common.tests import base + + +class PlanManagerTest(base.TestCase): + + def setUp(self): + super(PlanManagerTest, self).setUp() + self.heatclient = mock.MagicMock() + self.plan_store = mock.MagicMock() + self.plan_name = "overcloud" + self.stack = mock.MagicMock( + id='123', + status='CREATE_COMPLETE', + stack_name=self.plan_name + ) + + self.expected_plan = models.Plan(self.plan_name) + self.expected_plan.metadata = { + 'x-container-meta-usage-tripleo': 'plan', + } + self.expected_plan.files = { + 'some-environment.yaml': { + 'contents': "some fake contents", + 'meta': {'file-type': 'environment'} + }, + 'some-root-environment.yaml': { + 'contents': "parameters:\n" + " one: uno\n" + " obj:\n" + " two: due\n" + " three: tre\n", + 'meta': { + 'file-type': 'root-environment', + 'enabled': 'True' + } + }, + 'some-root-template.yaml': { + 'contents': "some fake contents", + 'meta': {'file-type': 'root-template'} + }, + 'some-template.yaml': { + 'contents': "some fake contents", + }, + } + + def test_create_plan(self): + self.plan_store.create = mock.MagicMock() + plan_mgr = plan.PlanManager(self.plan_store, self.heatclient) + plan_mgr.create_plan(self.plan_name, self.expected_plan.files) + self.plan_store.create.assert_called_with(self.plan_name) + # calls the Exception handling in create_plan + with mock.patch('tripleo_common.core.plan.LOG') as log_mock: + self.plan_store.create = mock.Mock(side_effect=ValueError()) + self.assertRaises(ValueError, + plan_mgr.create_plan, + self.plan_name, self.expected_plan.files) + log_mock.exception.assert_called_with("Error creating plan.") + + def test_delete_plan(self): + # test that stack exists + self.heatclient.stacks.get = mock.MagicMock(return_value=self.stack) + plan_mgr = plan.PlanManager(self.plan_store, self.heatclient) + self.assertRaises(exception.StackInUseError, + plan_mgr.delete_plan, + self.plan_name) + self.heatclient.stacks.get.assert_called_with(self.plan_name) + + # test that stack doesn't exist yet + self.plan_store.delete = mock.MagicMock() + self.heatclient.stacks.get = mock.Mock( + side_effect=heatexceptions.HTTPNotFound) + plan_mgr.delete_plan(self.plan_name) + self.plan_store.delete.assert_called_with(self.plan_name) + + # set side effect of swiftexceptions.ClientException + with mock.patch('tripleo_common.core.plan.LOG') as log_mock: + self.plan_store.delete = mock.Mock( + side_effect=swiftexceptions.ClientException( + "test-error", http_status=404)) + self.assertRaises(exception.PlanDoesNotExistError, + plan_mgr.delete_plan, + self.plan_name) + log_mock.exception.assert_called_with('Swift error deleting plan.') + + # set side effect of random Exception + self.heatclient.stacks.get = mock.Mock( + side_effect=ValueError()) + self.assertRaises(ValueError, + plan_mgr.delete_plan, + self.plan_name) + + def test_delete_temporary_environment(self): + + self.expected_plan.files = { + 'some-name.yaml': { + 'contents': "some fake contents", + 'meta': {'file-type': 'temp-environment'} + }, + } + self.plan_store.get = mock.MagicMock(return_value=self.expected_plan) + self.plan_store.delete_file = mock.MagicMock() + plan_mgr = plan.PlanManager(self.plan_store, self.heatclient) + plan_mgr.delete_temporary_environment(self.plan_name) + self.plan_store.delete_file.assert_called_with( + self.plan_name, 'some-name.yaml') + + def test_get_plan(self): + self.expected_plan.files = { + 'some-name.yaml': { + 'contents': "some fake contents", + 'meta': {'file-type': 'environment'} + }, + } + self.plan_store.get = mock.MagicMock(return_value=self.expected_plan) + plan_mgr = plan.PlanManager(self.plan_store, self.heatclient) + self.assertEqual(self.expected_plan, + plan_mgr.get_plan(self.plan_name), + "Plan mismatch") + self.plan_store.get.assert_called_with(self.plan_name) + with mock.patch('tripleo_common.core.plan.LOG') as log_mock: + self.plan_store.get = mock.Mock(side_effect=ValueError()) + self.assertRaises(ValueError, plan_mgr.get_plan, 'overcloud') + log_mock.exception.assert_called_with("Error retrieving plan.") + + def test_get_plan_list(self): + plan_mgr = plan.PlanManager(self.plan_store, self.heatclient) + self.plan_store.list = mock.MagicMock(return_value=['overcloud']) + self.assertEqual(['overcloud'], plan_mgr.get_plan_list(), + "get_plan_list failed") + with mock.patch('tripleo_common.core.plan.LOG') as log_mock: + self.plan_store.list = mock.Mock(side_effect=ValueError()) + self.assertRaises(ValueError, plan_mgr.get_plan_list) + log_mock.exception.assert_called_with( + "Error retrieving plan list.") + + def test_get_deployment_parameters(self): + self.plan_store.get = mock.MagicMock(return_value=self.expected_plan) + # calls templates.process_plan_data(plan.files) + plan_mgr = plan.PlanManager(self.plan_store, self.heatclient) + with mock.patch('tripleo_common.utils.templates') as templates: + templates.process_plan_data.return_value = ( + "some fake contents", { + 'parameters': { + 'obj': { + 'two': 'due', + 'three': 'tre' + }, + 'one': 'uno' + } + }, {'some-template.yaml': 'some fake contents'}) + self.heatclient.stacks.validate = mock.MagicMock( + return_value={ + 'parameters': { + 'obj': { + 'two': 'due', + 'three': 'tre' + }, + 'one': 'uno' + } + }) + self.assertEqual({ + 'parameters': { + 'obj': { + 'two': 'due', + 'three': 'tre' + }, + 'one': 'uno' + } + }, + plan_mgr.get_deployment_parameters(self.plan_name), + "Bad params") + self.heatclient.stacks.validate.assert_called_with( + template="some fake contents", + files={'some-template.yaml': 'some fake contents'}, + environment={ + 'parameters': { + 'obj': { + 'two': 'due', + 'three': 'tre' + }, + 'one': 'uno' + } + }, + show_nested=True + ) + # set side effect of heatexceptions.HTTPBadRequest on validate + self.heatclient.stacks.validate = mock.Mock( + side_effect=heatexceptions.HTTPBadRequest + ) + + self.assertRaises(exception.HeatValidationFailedError, + plan_mgr.get_deployment_parameters, + self.plan_name) + + def test_update_deployment_parameters(self): + self.plan_store.get = mock.MagicMock(return_value=self.expected_plan) + # calls templates.process_plan_data(plan.files) + plan_mgr = plan.PlanManager(self.plan_store, self.heatclient) + self.expected_plan.files['environments/deployment_parameters.yaml'] = { + 'contents': + "parameters:\n" + " one: uno\n" + " obj:\n" + " two: due\n" + " three: tre\n", + 'meta': {'file-type': 'temp-environment'} + } + with mock.patch('tripleo_common.utils.templates') as templates: + templates.process_plan_data.return_value = ( + "some fake contents", { + 'parameters': { + 'obj': { + 'two': 'due', + 'three': 'tre' + }, + 'one': 'uno' + } + }, {'some-template.yaml': 'some fake contents'}) + self.heatclient.stacks.validate = mock.MagicMock() + plan_mgr.validate_plan(self.plan_name) + self.heatclient.stacks.validate.assert_called_with( + template="some fake contents", + files={'some-template.yaml': 'some fake contents'}, + environment={ + 'parameters': { + 'obj': { + 'two': 'due', + 'three': 'tre' + }, + 'one': 'uno' + } + }, + show_nested=True + ) + # set side effect of heatexceptions.HTTPBadRequest on validate + self.heatclient.stacks.validate = mock.Mock( + side_effect=heatexceptions.HTTPBadRequest + ) + + self.assertRaises(exception.HeatValidationFailedError, + plan_mgr.get_deployment_parameters, + self.plan_name) + + # calls self.heatclient.stacks.validate + # set side effect on validate of heatexceptions.HTTPBadRequest + # calls six.raise_from(exception.HeatValidationFailedError(), exc) + # assert params match + pass + + def test_update_plan(self): + self.plan_store.update = mock.MagicMock() + self.plan_store.get = mock.MagicMock(return_value=self.expected_plan) + plan_mgr = plan.PlanManager(self.plan_store, self.heatclient) + self.assertEqual(self.expected_plan, + plan_mgr.update_plan( + self.plan_name, self.expected_plan.files), + "Plan mismatch") + self.plan_store.get.assert_called_with(self.plan_name) + with mock.patch('tripleo_common.core.plan.LOG') as log_mock: + self.plan_store.get = mock.Mock(side_effect=ValueError()) + self.assertRaises(ValueError, plan_mgr.update_plan, + self.plan_name, self.expected_plan.files) + log_mock.exception.assert_called_with("Error retrieving plan.") + + def test_validate_plan(self): + # calls self.get_plan(plan_name) + self.plan_store.get = mock.MagicMock(return_value=self.expected_plan) + # test 2 root-templates to get exception.TooManyRootTemplatesError + self.expected_plan.files['another-root-template.yaml'] = { + 'contents': "some fake contents", + 'meta': {'file-type': 'root-template'} + } + plan_mgr = plan.PlanManager(self.plan_store, self.heatclient) + self.assertRaises(exception.TooManyRootTemplatesError, + plan_mgr.validate_plan, + self.plan_name) + del(self.expected_plan.files['another-root-template.yaml']) + # calls templates.process_plan_data(plan.files) (mock and assert_call) + with mock.patch('tripleo_common.utils.templates') as templates: + templates.process_plan_data = mock.MagicMock(return_value=( + "some fake contents", { + 'parameters': { + 'obj': { + 'two': 'due', + 'three': 'tre' + }, + 'one': 'uno' + } + }, {'some-template.yaml': 'some fake contents'})) + self.heatclient.stacks.validate = mock.MagicMock() + plan_mgr.validate_plan(self.plan_name) + self.heatclient.stacks.validate.assert_called_with( + template="some fake contents", + files={'some-template.yaml': 'some fake contents'}, + environment={ + 'parameters': { + 'obj': { + 'two': 'due', + 'three': 'tre' + }, + 'one': 'uno' + } + }, + show_nested=True + ) + # set side effect of heatexceptions.HTTPBadRequest on validate + self.heatclient.stacks.validate = mock.Mock( + side_effect=heatexceptions.HTTPBadRequest + ) + with mock.patch('tripleo_common.core.plan.LOG') as log_mock: + self.assertRaises(exception.HeatValidationFailedError, + plan_mgr.validate_plan, + self.plan_name) + log_mock.exception.assert_called_with( + "Error validating the plan.") diff --git a/tripleo_common/tests/core/test_plan_storage.py b/tripleo_common/tests/core/test_plan_storage.py new file mode 100644 index 000000000..1d3f914ce --- /dev/null +++ b/tripleo_common/tests/core/test_plan_storage.py @@ -0,0 +1,193 @@ +# Copyright 2015 Red Hat, Inc. +# 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 mock + +from tripleo_common.core import exception +from tripleo_common.core.models import Plan +from tripleo_common.core.plan_storage import default_container_headers +from tripleo_common.core.plan_storage import SwiftPlanStorageBackend +from tripleo_common.tests import base + +PLAN_DATA = { + '/path/to/overcloud.yaml': { + 'contents': "heat_template_version: 2015-04-30\n\n" + "resources:\n" + "\n" + " HorizonSecret:\n" + " type: OS::Heat::RandomString\n" + " properties:\n" + " length: 10\n" + "\n" + " Controller:\n" + " type: OS::Heat::ResourceGroup\n" + " depends_on: Networks\n" + " properties:\n" + " count: {get_param: ControllerCount}\n", + 'meta': {'file-type': 'root-template'}, + }, + '/path/to/environment.yaml': { + 'contents': "parameters:\n" + " one: uno\n" + " obj:\n" + " two: due\n" + " three: tre\n", + 'meta': {'file-type': 'root-environment'}, + }, + '/path/to/network-isolation.json': { + 'contents': '{"parameters": {"one": "one"}}', + 'meta': {'file-type': 'environment', 'order': 1}, + }, + '/path/to/ceph-storage-env.yaml': { + 'contents': "parameters:\n" + " obj:\n" + " two: dos,\n" + " three: three", + 'meta': {'file-type': 'environment', 'order': 2}, + }, + '/path/to/poc-custom-env.yaml': { + 'contents': "parameters:\n" + " obj:\n" + " two: two\n" + " some::resource: /path/to/somefile.yaml", + 'meta': {'file-type': 'environment', 'order': 0} + }, + '/path/to/somefile.yaml': {'contents': "description: lorem ipsum"} +} + + +class PlanStorageTest(base.TestCase): + + def setUp(self): + super(PlanStorageTest, self).setUp() + self.swiftclient = mock.MagicMock() + self.plan_store = SwiftPlanStorageBackend(self.swiftclient) + self.plan_name = "overcloud" + + def test_create(self): + # create a plan + self.plan_store.list = mock.MagicMock(return_value=['test1', 'test2']) + self.swiftclient.put_container = mock.MagicMock() + self.plan_store.create(self.plan_name) + self.swiftclient.put_container.assert_called_with( + self.plan_name, + headers=default_container_headers + ) + + # attempt to create a 2nd plan should fail + self.plan_store.list = mock.MagicMock(return_value=['overcloud']) + self.assertRaises(exception.PlanAlreadyExistsError, + self.plan_store.create, + self.plan_name) + + def test_delete(self): + self.swiftclient.get_container = mock.MagicMock( + return_value=({}, [ + {'name': 'some-name.yaml'}, + {'name': 'some-other-name.yaml'}, + {'name': 'yet-some-other-name.yaml'}, + {'name': 'finally-another-name.yaml'} + ]) + ) + self.swiftclient.delete_object = mock.MagicMock() + self.plan_store.delete(self.plan_name) + mock_calls = [ + mock.call('overcloud', 'some-name.yaml'), + mock.call('overcloud', 'some-other-name.yaml'), + mock.call('overcloud', 'yet-some-other-name.yaml'), + mock.call('overcloud', 'finally-another-name.yaml') + ] + self.swiftclient.delete_object.assert_has_calls( + mock_calls, any_order=True) + + def test_delete_file(self): + self.swiftclient.delete_object = mock.MagicMock() + filepath = '/a/random/path/to/file.yaml' + self.plan_store.delete_file(self.plan_name, filepath) + self.swiftclient.delete_object.assert_called_with( + self.plan_name, filepath) + + def test_get(self): + metadata = { + 'x-container-meta-usage-tripleo': 'plan', + 'accept-ranges': 'bytes', + 'x-storage-policy': 'Policy-0', + 'connection': 'keep-alive', + 'x-timestamp': '1447161410.72641', + 'x-trans-id': 'tx1f41a9d34a2a437d8f8dd-00565dd486', + 'content-type': 'application/json; charset=utf-8', + 'x-versions-location': 'versions' + } + expected_plan = Plan(self.plan_name) + expected_plan.metadata = metadata + expected_plan.files = { + 'some-name.yaml': { + 'contents': "some fake contents", + 'meta': {'file-type': 'environment'} + }, + } + self.swiftclient.get_container = mock.MagicMock(return_value=( + metadata, [ + {'name': 'some-name.yaml'}, + ]) + ) + self.swiftclient.get_object = mock.MagicMock(return_value=( + {'x-object-meta-file-type': 'environment'}, "some fake contents" + )) + self.assertEqual(expected_plan.name, + self.plan_store.get(self.plan_name).name) + self.swiftclient.get_container.assert_called_with(self.plan_name) + self.swiftclient.get_object.assert_called_with( + 'overcloud', 'some-name.yaml') + + def test_list(self): + self.swiftclient.get_account = mock.MagicMock( + return_value=({}, [ + { + 'count': 1, + 'bytes': 55, + 'name': 'overcloud' + }, + ]) + ) + self.swiftclient.get_container = mock.MagicMock( + return_value=({ + 'x-container-meta-usage-tripleo': 'plan', + }, []) + ) + self.assertEqual(['overcloud'], self.plan_store.list()) + self.swiftclient.get_container.assert_called_with('overcloud') + + def test_update(self): + expected_plan = Plan(self.plan_name) + expected_plan.metadata = { + 'x-container-meta-usage-tripleo': 'plan', + 'accept-ranges': 'bytes', + 'x-storage-policy': 'Policy-0', + } + expected_plan.files = { + 'some-name.yaml': { + 'contents': "some fake contents", + 'meta': {'file-type': 'environment'} + }, + } + self.swiftclient.put_object = mock.MagicMock() + self.plan_store.update(self.plan_name, expected_plan.files) + self.swiftclient.put_object.assert_called_with( + self.plan_name, + 'some-name.yaml', + "some fake contents", + headers={'x-object-meta-file-type': 'environment'} + ) diff --git a/tripleo_common/tests/utils/__init__.py b/tripleo_common/tests/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tripleo_common/tests/utils/test_meta.py b/tripleo_common/tests/utils/test_meta.py new file mode 100644 index 000000000..a882df692 --- /dev/null +++ b/tripleo_common/tests/utils/test_meta.py @@ -0,0 +1,165 @@ +# Copyright 2015 Red Hat, Inc. +# 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. +from tripleo_common.core import exception +from tripleo_common.tests import base +from tripleo_common.utils.meta import add_file_metadata + + +MAPPING_YAML_CONTENTS = """root_template: /path/to/overcloud.yaml +root_environment: /path/to/environment.yaml +topics: + - title: Fake Single Environment Group Configuration + description: + environment_groups: + - title: + description: Random fake string of text + environments: + - file: /path/to/network-isolation.json + title: Default Configuration + description: + + - title: Fake Multiple Environment Group Configuration + description: + environment_groups: + - title: Random Fake 1 + description: Random fake string of text + environments: + - file: /path/to/ceph-storage-env.yaml + title: Fake1 + description: Random fake string of text + + - title: Random Fake 2 + description: + environments: + - file: /path/to/poc-custom-env.yaml + title: Fake2 + description: +""" + +PLAN_DATA = { + '/path/to/overcloud.yaml': { + 'contents': 'heat_template_version: 2015-04-30', + 'meta': {'file-type': 'root-template'}, + }, + '/path/to/environment.yaml': { + 'contents': "parameters:\n" + " one: uno\n" + " obj:\n" + " two: due\n" + " three: tre\n", + 'meta': {'file-type': 'root-environment', 'enabled': 'True'}, + }, + '/path/to/network-isolation.json': { + 'contents': '{"parameters": {"one": "one"}}', + 'meta': {'file-type': 'environment'}, + }, + '/path/to/ceph-storage-env.yaml': { + 'contents': "parameters:\n" + " obj:\n" + " two: dos,\n" + " three: three", + 'meta': {'file-type': 'environment'}, + }, + '/path/to/poc-custom-env.yaml': { + 'contents': "parameters:\n" + " obj:\n" + " two: two\n" + " some::resource: /path/to/somefile.yaml", + 'meta': {'file-type': 'environment'} + }, + '/path/to/somefile.yaml': {'contents': "description: lorem ipsum"}, + 'capabilities-map.yaml': { + 'contents': MAPPING_YAML_CONTENTS, + 'meta': {'file-type': 'capabilities-map'}, + }, +} + +PLAN_DATA_NO_META = { + '/path/to/overcloud.yaml': { + 'contents': 'heat_template_version: 2015-04-30', + }, + '/path/to/environment.yaml': { + 'contents': "parameters:\n" + " one: uno\n" + " obj:\n" + " two: due\n" + " three: tre\n", + }, + '/path/to/network-isolation.json': { + 'contents': '{"parameters": {"one": "one"}}', + }, + '/path/to/ceph-storage-env.yaml': { + 'contents': "parameters:\n" + " obj:\n" + " two: dos,\n" + " three: three", + }, + '/path/to/poc-custom-env.yaml': { + 'contents': "parameters:\n" + " obj:\n" + " two: two\n" + " some::resource: /path/to/somefile.yaml", + }, + '/path/to/somefile.yaml': {'contents': "description: lorem ipsum"}, + 'capabilities-map.yaml': { + 'contents': MAPPING_YAML_CONTENTS, + 'meta': {'file-type': 'capabilities-map'}, + }, +} + + +class UtilsMetaTest(base.TestCase): + + def test_add_file_metadata(self): + # tests case where files have no metadata yet + plan_files_with_metadata = add_file_metadata(PLAN_DATA_NO_META) + self.assertEqual( + PLAN_DATA, + plan_files_with_metadata, + "Metadata not added properly" + ) + + # tests case where files already have a metadata dict per file + for k, v in PLAN_DATA.items(): + if 'meta' in v: + v.update({'enabled': 'True'}) + else: + v['meta'] = {'enabled': 'True'} + + for k, v in PLAN_DATA_NO_META.items(): + if 'meta' in v: + v.update({'enabled': 'True'}) + else: + v['meta'] = {'enabled': 'True'} + + plan_files_with_metadata = add_file_metadata(PLAN_DATA_NO_META) + self.assertEqual( + PLAN_DATA, + plan_files_with_metadata, + "Metadata not added properly" + ) + + # test to ensure having more than one capabilities-map file + # results in an exception + PLAN_DATA_NO_META.update({ + 'capabilities-map2.yaml': { + 'contents': MAPPING_YAML_CONTENTS, + 'meta': {'file-type': 'capabilities-map'} + } + }) + self.assertRaises(exception.TooManyCapabilitiesMapFilesError, + add_file_metadata, + PLAN_DATA_NO_META) diff --git a/tripleo_common/tests/utils/test_templates.py b/tripleo_common/tests/utils/test_templates.py new file mode 100644 index 000000000..05b025750 --- /dev/null +++ b/tripleo_common/tests/utils/test_templates.py @@ -0,0 +1,88 @@ +# Copyright 2015 Red Hat, Inc. +# 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. +from tripleo_common.tests import base +from tripleo_common.utils.templates import find_root_template +from tripleo_common.utils.templates import process_plan_data + +PLAN_DATA = { + '/path/to/overcloud.yaml': { + 'contents': 'heat_template_version: 2015-04-30', + 'meta': {'file-type': 'root-template'}, + }, + '/path/to/environment.yaml': { + 'contents': "parameters:\n" + " one: uno\n" + " obj:\n" + " two: due\n" + " three: tre\n", + 'meta': { + 'file-type': 'root-environment', + 'enabled': 'True' + } + }, + '/path/to/network-isolation.json': { + 'contents': '{"parameters": {"one": "one"}}', + 'meta': {'file-type': 'environment'}, + }, + '/path/to/ceph-storage-env.yaml': { + 'contents': "parameters:\n" + " obj:\n" + " two: dos,\n" + " three: three", + 'meta': {'file-type': 'environment'}, + }, + '/path/to/poc-custom-env.yaml': { + 'contents': "parameters:\n" + " obj:\n" + " two: two\n" + " some::resource: /path/to/somefile.yaml", + 'meta': {'file-type': 'environment'} + }, + '/path/to/somefile.yaml': {'contents': "description: lorem ipsum"} +} + + +class UtilsTemplatesTest(base.TestCase): + + def setUp(self): + super(UtilsTemplatesTest, self).setUp() + self.tpl, self.env, self.files = process_plan_data(PLAN_DATA) + print(self.files) + + def test_find_root_template(self): + # delete the root_template from sample data + del PLAN_DATA['/path/to/overcloud.yaml'] + + # without root, should return {} + self.assertEqual({}, find_root_template(PLAN_DATA)) + + # add root_template back to sample data + root_template = { + '/path/to/overcloud.yaml': { + 'contents': 'heat_template_version: 2015-04-30', + 'meta': {'file-type': 'root-template'}} + } + PLAN_DATA.update(root_template) + + self.assertEqual(root_template, find_root_template(PLAN_DATA)) + + def test_template_found(self): + self.assertEqual(self.tpl, 'heat_template_version: 2015-04-30') + + def test_files_found(self): + self.assertEqual(self.files, { + '/path/to/somefile.yaml': 'description: lorem ipsum', + }) diff --git a/tripleo_common/utils/__init__.py b/tripleo_common/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tripleo_common/utils/meta.py b/tripleo_common/utils/meta.py new file mode 100644 index 000000000..11776c1f3 --- /dev/null +++ b/tripleo_common/utils/meta.py @@ -0,0 +1,85 @@ +# Copyright 2015 Red Hat, Inc. +# 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 yaml + +from tripleo_common.core import constants +from tripleo_common.core import exception + + +def add_key_prefix(source): + result = dict() + for keyname, value in source.items(): + new_keyname = "%s%s" % (constants.OBJECT_META_KEY_PREFIX, keyname) + result[new_keyname] = value + return result + + +def remove_key_prefix(source): + result = dict() + for keyname, value in source.items(): + new_keyname = keyname.replace(constants.OBJECT_META_KEY_PREFIX, '') + result[new_keyname] = value + return result + + +def add_file_metadata(plan_files): + cm = {k: v for (k, v) in plan_files.items() + if v.get('meta', {}).get('file-type') == 'capabilities-map'} + # if there is more than one capabilities-map file, throw an exception + # if there is a capabilities-map file, then process it and set metadata + # in files found + if len(cm) > 1: + raise exception.TooManyCapabilitiesMapFilesError() + if len(cm) == 1: + mapfile = yaml.load(list(cm.items())[0][1]['contents']) + + # identify the root template + if mapfile['root_template']: + if plan_files[mapfile['root_template']]: + # if the file exists in the plan and has meta, update it + # otherwise add meta dict + if 'meta' in plan_files[mapfile['root_template']]: + plan_files[mapfile['root_template']]['meta'].update( + dict(constants.ROOT_TEMPLATE_META) + ) + else: + plan_files[mapfile['root_template']]['meta'] =\ + dict(constants.ROOT_TEMPLATE_META) + + # identify all environments + for topic in mapfile['topics']: + for eg in topic['environment_groups']: + for env in eg['environments']: + if 'meta' in plan_files[env['file']]: + plan_files[env['file']]['meta'].update( + dict(constants.ENVIRONMENT_META) + ) + else: + plan_files[env['file']]['meta'] =\ + dict(constants.ENVIRONMENT_META) + + # identify the root environment + if mapfile['root_environment']: + if plan_files[mapfile['root_environment']]: + # if the file exists in the plan and has meta, update it + # otherwise add meta dict + if 'meta' in plan_files[mapfile['root_environment']]: + plan_files[mapfile['root_environment']]['meta'].update( + dict(constants.ROOT_ENVIRONMENT_META) + ) + else: + plan_files[mapfile['root_environment']]['meta'] =\ + dict(constants.ROOT_ENVIRONMENT_META) + return plan_files diff --git a/tripleo_common/utils/templates.py b/tripleo_common/utils/templates.py new file mode 100644 index 000000000..0af683eba --- /dev/null +++ b/tripleo_common/utils/templates.py @@ -0,0 +1,81 @@ +# Copyright 2015 Red Hat, Inc. +# 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 json +import logging +import yaml + +LOG = logging.getLogger(__name__) + + +def _get_dict_from_env_string(env_name, env_string): + """Returns environment dict, either from yaml or json.""" + if '.yaml' in env_name: + return yaml.load(env_string) + else: + return json.loads(env_string) + + +def deep_update(base, new): + for key, val in new.items(): + if isinstance(val, dict): + tmp = deep_update(base.get(key, {}), val) + base[key] = tmp + else: + base[key] = val + return base + + +def process_plan_data(plan_data): + """Processes the plan data.""" + template = '' + environment = {} + env_items = [] + temp_env_items = [] + files = {} + + for key, val in plan_data.items(): + file_type = val.get('meta', {}).get('file-type') + enabled = val.get('meta', {}).get('enabled') + if not file_type: + files[key] = val['contents'] + elif file_type == 'environment' and enabled: + env_items.append({'name': key, + 'meta': val['meta'], + 'contents': val['contents']}) + elif file_type == 'temp-environment': + temp_env_items.append({'name': key, + 'meta': val['meta'], + 'contents': val['contents']}) + elif file_type == 'root-template': + template = val['contents'] + elif file_type == 'root-environment' and enabled: + environment = _get_dict_from_env_string(key, val['contents']) + + # merge environment files + for item in env_items: + env_dict = _get_dict_from_env_string(item['name'], item['contents']) + environment = deep_update(environment, env_dict) + + # merge the temporary environment files last + for item in temp_env_items: + env_dict = _get_dict_from_env_string(item['name'], item['contents']) + environment = deep_update(environment, env_dict) + + return template, environment, files + + +def find_root_template(plan_files): + return {k: v for (k, v) in plan_files.items() + if v.get('meta', {}).get('file-type') == 'root-template'}