diff --git a/murano/common/config.py b/murano/common/config.py index 42cc32ac..8e56aced 100644 --- a/murano/common/config.py +++ b/murano/common/config.py @@ -179,6 +179,8 @@ stats_opts = [ engine_opts = [ cfg.BoolOpt('disable_murano_agent', default=False, help=_('Disallow the use of murano-agent')), + cfg.StrOpt('class_configs', default='/etc/murano/class-configs', + help=_('Path to class configuration files')), cfg.BoolOpt('use_trusts', default=False, help=_("Create resources using trust token rather " "than user's token")) diff --git a/murano/dsl/class_loader.py b/murano/dsl/class_loader.py index f19bf637..024f6836 100644 --- a/murano/dsl/class_loader.py +++ b/murano/dsl/class_loader.py @@ -73,7 +73,7 @@ class MuranoClassLoader(object): properties = data.get('Properties', {}) for property_name, property_spec in properties.iteritems(): - spec = typespec.PropertySpec(property_spec, ns_resolver) + spec = typespec.PropertySpec(property_spec, type_obj) type_obj.add_property(property_name, spec) methods = data.get('Methods') or data.get('Workflow') or {} @@ -95,6 +95,9 @@ class MuranoClassLoader(object): def create_root_context(self): return yaql.create_context(True) + def get_class_config(self, name): + return {} + def create_local_context(self, parent_context, murano_class): return yaql.context.Context(parent_context=parent_context) diff --git a/murano/dsl/murano_class.py b/murano/dsl/murano_class.py index 3844f009..a020dabe 100644 --- a/murano/dsl/murano_class.py +++ b/murano/dsl/murano_class.py @@ -38,6 +38,7 @@ class MuranoClass(object): self._namespace_resolver = namespace_resolver self._name = namespace_resolver.resolve_name(name) self._properties = {} + self._config = {} if self._name == 'io.murano.Object': self._parents = [] else: @@ -74,8 +75,7 @@ class MuranoClass(object): return self._methods.get(name) def add_method(self, name, payload): - method = murano_method.MuranoMethod(self._namespace_resolver, - self, name, payload) + method = murano_method.MuranoMethod(self, name, payload) self._methods[name] = method return method diff --git a/murano/dsl/murano_method.py b/murano/dsl/murano_method.py index dfded992..49be8ee6 100644 --- a/murano/dsl/murano_method.py +++ b/murano/dsl/murano_method.py @@ -40,10 +40,9 @@ def methodusage(usage): class MuranoMethod(object): - def __init__(self, namespace_resolver, - murano_class, name, payload): + def __init__(self, murano_class, name, payload): self._name = name - self._namespace_resolver = namespace_resolver + self._murano_class = murano_class if callable(payload): self._body = payload @@ -65,9 +64,7 @@ class MuranoMethod(object): raise ValueError() name = record.keys()[0] self._arguments_scheme[name] = typespec.ArgumentSpec( - record[name], self._namespace_resolver) - - self._murano_class = murano_class + record[name], murano_class) @property def name(self): @@ -99,8 +96,7 @@ class MuranoMethod(object): for i in xrange(len(defaults)): data[i + len(data) - len(defaults)][1]['Default'] = defaults[i] result = collections.OrderedDict([ - (name, typespec.ArgumentSpec( - declaration, self._namespace_resolver)) + (name, typespec.ArgumentSpec(declaration, self.murano_class)) for name, declaration in data]) if '_context' in result: del result['_context'] diff --git a/murano/dsl/murano_object.py b/murano/dsl/murano_object.py index 26b26596..66ce06b7 100644 --- a/murano/dsl/murano_object.py +++ b/murano/dsl/murano_object.py @@ -36,6 +36,10 @@ class MuranoObject(object): self.__context = context self.__defaults = defaults or {} self.__this = this + self.__config = object_store.class_loader.get_class_config( + murano_class.name) + if not isinstance(self.__config, dict): + self.__config = {} known_classes[murano_class.name] = self for parent_class in murano_class.parents: name = parent_class.name @@ -51,9 +55,20 @@ class MuranoObject(object): def initialize(self, **kwargs): used_names = set() + for property_name in self.__type.properties: + spec = self.__type.get_property(property_name) + if spec.usage == typespec.PropertyUsages.Config: + if property_name in self.__config: + property_value = self.__config[property_name] + else: + property_value = type_scheme.NoValue + self.set_property(property_name, property_value) + for i in xrange(2): for property_name in self.__type.properties: spec = self.__type.get_property(property_name) + if spec.usage == typespec.PropertyUsages.Config: + continue needs_evaluation = murano.dsl.helpers.needs_evaluation if i == 0 and needs_evaluation(spec.default) or i == 1\ and property_name in used_names: @@ -137,7 +152,8 @@ class MuranoObject(object): or not derived: raise exceptions.NoWriteAccessError(name) - default = self.__defaults.get(name, spec.default) + default = self.__config.get(name, spec.default) + default = self.__defaults.get(name, default) child_context = yaql.context.Context( parent_context=self.__context) child_context.set_data(self) diff --git a/murano/dsl/typespec.py b/murano/dsl/typespec.py index 8241f59a..5fcebbe4 100644 --- a/murano/dsl/typespec.py +++ b/murano/dsl/typespec.py @@ -22,17 +22,18 @@ class PropertyUsages(object): InOut = 'InOut' Runtime = 'Runtime' Const = 'Const' - All = set([In, Out, InOut, Runtime, Const]) + Config = 'Config' + All = set([In, Out, InOut, Runtime, Const, Config]) Writable = set([Out, InOut, Runtime]) class Spec(object): - def __init__(self, declaration, namespace_resolver): - self._namespace_resolver = namespace_resolver + def __init__(self, declaration, owner_class): + self._namespace_resolver = owner_class.namespace_resolver self._contract = type_scheme.TypeScheme(declaration['Contract']) + self._usage = declaration.get('Usage') or 'In' self._default = declaration.get('Default') self._has_default = 'Default' in declaration - self._usage = declaration.get('Usage') or 'In' if self._usage not in PropertyUsages.All: raise exceptions.DslSyntaxError( 'Unknown type {0}. Must be one of ({1})'.format( diff --git a/murano/engine/package_class_loader.py b/murano/engine/package_class_loader.py index 4e33308c..0ea291e4 100644 --- a/murano/engine/package_class_loader.py +++ b/murano/engine/package_class_loader.py @@ -13,9 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json +import os.path import sys from oslo.config import cfg +import yaml from murano.dsl import class_loader from murano.dsl import exceptions @@ -70,3 +73,14 @@ class PackageClassLoader(class_loader.MuranoClassLoader): context = super(PackageClassLoader, self).create_root_context() yaql_functions.register(context) return context + + def get_class_config(self, name): + json_config = os.path.join(CONF.engine.class_configs, name + '.json') + if os.path.exists(json_config): + with open(json_config) as f: + return json.load(f) + yaml_config = os.path.join(CONF.engine.class_configs, name + '.yaml') + if os.path.exists(yaml_config): + with open(yaml_config) as f: + return yaml.safe_load(f) + return {} diff --git a/murano/engine/simple_cloader.py b/murano/engine/simple_cloader.py deleted file mode 100644 index d5600df1..00000000 --- a/murano/engine/simple_cloader.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) 2013 Mirantis Inc. -# -# 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 os.path - -import yaml - -import murano.dsl.class_loader as class_loader -import murano.dsl.yaql_expression as yaql_expression -import murano.engine.system.yaql_functions as yaql_functions - - -def yaql_constructor(loader, node): - value = loader.construct_scalar(node) - return yaql_expression.YaqlExpression(value) - -yaml.add_constructor(u'!yaql', yaql_constructor) -yaml.add_implicit_resolver(u'!yaql', yaql_expression.YaqlExpression) - - -class SimpleClassLoader(class_loader.MuranoClassLoader): - def __init__(self, base_path): - self._base_path = base_path - super(SimpleClassLoader, self).__init__() - - def load_definition(self, name): - path = os.path.join(self._base_path, name, 'manifest.yaml') - if not os.path.exists(path): - return None - with open(path) as stream: - return yaml.load(stream) - - def create_root_context(self): - context = super(SimpleClassLoader, self).create_root_context() - yaql_functions.register(context) - return context diff --git a/murano/tests/unit/dsl/foundation/test_case.py b/murano/tests/unit/dsl/foundation/test_case.py index 85f8752c..d6eec78c 100644 --- a/murano/tests/unit/dsl/foundation/test_case.py +++ b/murano/tests/unit/dsl/foundation/test_case.py @@ -38,6 +38,7 @@ class DslTestCase(base.MuranoTestCase): self.register_function( lambda data: self._traces.append(data()), 'trace') self._traces = [] + test_class_loader.TestClassLoader.clear_configs() eventlet.debug.hub_exceptions(False) def new_runner(self, model): diff --git a/murano/tests/unit/dsl/foundation/test_class_loader.py b/murano/tests/unit/dsl/foundation/test_class_loader.py index eda32c4a..ff07f7d0 100644 --- a/murano/tests/unit/dsl/foundation/test_class_loader.py +++ b/murano/tests/unit/dsl/foundation/test_class_loader.py @@ -22,10 +22,12 @@ from murano.dsl import murano_package from murano.dsl import namespace_resolver from murano.engine.system import yaql_functions from murano.engine import yaql_yaml_loader +from murano.tests.unit.dsl.foundation import object_model class TestClassLoader(class_loader.MuranoClassLoader): _classes_cache = {} + _configs = {} def __init__(self, directory, package_name, parent_loader=None): self._package = murano_package.MuranoPackage() @@ -90,3 +92,16 @@ class TestClassLoader(class_loader.MuranoClassLoader): def register_function(self, func, name): self._functions[name] = func + + def get_class_config(self, name): + return TestClassLoader._configs.get(name, {}) + + def set_config_value(self, class_name, property_name, value): + if isinstance(class_name, object_model.Object): + class_name = class_name.type_name + TestClassLoader._configs.setdefault(class_name, {})[ + property_name] = value + + @staticmethod + def clear_configs(): + TestClassLoader._configs = {} diff --git a/murano/tests/unit/dsl/meta/ConfigProperties.yaml b/murano/tests/unit/dsl/meta/ConfigProperties.yaml new file mode 100644 index 00000000..81165d8b --- /dev/null +++ b/murano/tests/unit/dsl/meta/ConfigProperties.yaml @@ -0,0 +1,17 @@ +Name: ConfigProperties + +Properties: + cfgProperty: + Usage: Config + Contract: $.int().notNull() + Default: 123 + + normalProperty: + Contract: $.string().notNull() + Default: DEFAULT + +Methods: + testPropertyValues: + Body: + - trace($.cfgProperty) + - trace($.normalProperty) diff --git a/murano/tests/unit/dsl/meta/DerivedFrom2Classes.yaml b/murano/tests/unit/dsl/meta/DerivedFrom2Classes.yaml index 4aa59812..6dba86fd 100644 --- a/murano/tests/unit/dsl/meta/DerivedFrom2Classes.yaml +++ b/murano/tests/unit/dsl/meta/DerivedFrom2Classes.yaml @@ -24,6 +24,9 @@ Properties: usageTestProperty6: Contract: $.int() Usage: Const + usageTestProperty7: + Contract: $.int() + Usage: Config Methods: @@ -79,6 +82,12 @@ Methods: - $.usageTestProperty6: 66 - Return: $.usageTestProperty6 + testModifyUsageTestProperty7: + Body: + - $.usageTestProperty7: 77 + - Return: $.usageTestProperty7 + + testMixinOverride: Body: - $.virtualMethod() diff --git a/murano/tests/unit/dsl/test_config_properties.py b/murano/tests/unit/dsl/test_config_properties.py new file mode 100644 index 00000000..7ac73d23 --- /dev/null +++ b/murano/tests/unit/dsl/test_config_properties.py @@ -0,0 +1,57 @@ +# Copyright (c) 2014 Mirantis, Inc. +# +# 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 murano.tests.unit.dsl.foundation import object_model as om +from murano.tests.unit.dsl.foundation import test_case + + +class TestConfigProperties(test_case.DslTestCase): + def test_config_property(self): + obj = om.Object('ConfigProperties') + self.class_loader.set_config_value(obj, 'cfgProperty', '987') + runner = self.new_runner(obj) + runner.testPropertyValues() + self.assertEqual( + [987, 'DEFAULT'], + self.traces + ) + + def test_config_property_exclusion_from_obect_model(self): + obj = om.Object('ConfigProperties', cfgProperty=555) + runner = self.new_runner(obj) + runner.testPropertyValues() + self.assertEqual( + [123, 'DEFAULT'], + self.traces + ) + + def test_config_affects_default(self): + obj = om.Object('ConfigProperties') + self.class_loader.set_config_value(obj, 'normalProperty', 'custom') + runner = self.new_runner(obj) + runner.testPropertyValues() + self.assertEqual( + [123, 'custom'], + self.traces + ) + + def test_config_not_affects_in_properties(self): + obj = om.Object('ConfigProperties', normalProperty='qq') + self.class_loader.set_config_value(obj, 'normalProperty', 'custom') + runner = self.new_runner(obj) + runner.testPropertyValues() + self.assertEqual( + [123, 'qq'], + self.traces + ) diff --git a/murano/tests/unit/dsl/test_property_access.py b/murano/tests/unit/dsl/test_property_access.py index aa334d18..5a10d4a8 100644 --- a/murano/tests/unit/dsl/test_property_access.py +++ b/murano/tests/unit/dsl/test_property_access.py @@ -113,3 +113,7 @@ class TestPropertyAccess(test_case.DslTestCase): exceptions.NoWriteAccessError, self._runner.on(self._multi_derived). testModifyUsageTestProperty6) + self.assertRaises( + exceptions.NoWriteAccessError, + self._runner.on(self._multi_derived). + testModifyUsageTestProperty7) diff --git a/murano/tests/unit/test_heat_stack.py b/murano/tests/unit/test_heat_stack.py index b46b383f..b067b490 100644 --- a/murano/tests/unit/test_heat_stack.py +++ b/murano/tests/unit/test_heat_stack.py @@ -16,7 +16,9 @@ from heatclient.v1 import stacks import mock -from murano.dsl import murano_object +from murano.dsl import class_loader +from murano.dsl import murano_class +from murano.dsl import object_store from murano.engine import client_manager from murano.engine.system import heat_stack from murano.tests.unit import base @@ -28,11 +30,14 @@ MOD_NAME = 'murano.engine.system.heat_stack' class TestHeatStack(base.MuranoTestCase): def setUp(self): super(TestHeatStack, self).setUp() - self.mock_murano_obj = mock.Mock(spec=murano_object.MuranoObject) - self.mock_murano_obj.name = 'TestObj' - self.mock_murano_obj.parents = [] + self.mock_murano_class = mock.Mock(spec=murano_class.MuranoClass) + self.mock_murano_class.name = 'io.murano.system.HeatStack' + self.mock_murano_class.parents = [] self.heat_client_mock = mock.MagicMock() self.heat_client_mock.stacks = mock.MagicMock(spec=stacks.StackManager) + self.mock_object_store = mock.Mock(spec=object_store.ObjectStore) + self.mock_object_store.class_loader = mock.Mock( + spec=class_loader.MuranoClassLoader) self.client_manager_mock = mock.Mock( spec=client_manager.ClientManager) @@ -49,8 +54,8 @@ class TestHeatStack(base.MuranoTestCase): status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} - hs = heat_stack.HeatStack(self.mock_murano_obj, - None, None, None) + hs = heat_stack.HeatStack(self.mock_murano_class, + None, self.mock_object_store, None) hs._name = 'test-stack' hs._description = 'Generated by TestHeatStack' hs._template = {'resources': {'test': 1}} @@ -82,8 +87,8 @@ class TestHeatStack(base.MuranoTestCase): status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} - hs = heat_stack.HeatStack(self.mock_murano_obj, - None, None, None) + hs = heat_stack.HeatStack(self.mock_murano_class, + None, self.mock_object_store, None) hs._clients = self.client_manager_mock hs._name = 'test-stack' hs._description = None @@ -107,8 +112,8 @@ class TestHeatStack(base.MuranoTestCase): def test_update_wrong_template_version(self): """Template version other than expected should cause error.""" - hs = heat_stack.HeatStack(self.mock_murano_obj, - None, None, None) + hs = heat_stack.HeatStack(self.mock_murano_class, + None, self.mock_object_store, None) hs._name = 'test-stack' hs._description = 'Generated by TestHeatStack' hs._template = {'resources': {'test': 1}}