Package type plugins support was added
Adds support for package type classes in plugins. As a result additional package types may be introduced without changes to Murano source code Implements-Blueprint: pluggable-package-types Change-Id: I61c400b7be4c8836825c00ce7ca7d1b8501d0bd4
This commit is contained in:
parent
0ced007109
commit
150aed4910
@ -26,7 +26,7 @@ from oslo_serialization import jsonutils
|
||||
|
||||
from murano.common import auth_utils
|
||||
from murano.common.helpers import token_sanitizer
|
||||
from murano.common import plugin_loader
|
||||
from murano.common.plugins import extensions_loader
|
||||
from murano.common import rpc
|
||||
from murano.dsl import context_manager
|
||||
from murano.dsl import dsl_exception
|
||||
@ -70,7 +70,7 @@ def get_plugin_loader():
|
||||
global PLUGIN_LOADER
|
||||
|
||||
if PLUGIN_LOADER is None:
|
||||
PLUGIN_LOADER = plugin_loader.PluginLoader()
|
||||
PLUGIN_LOADER = extensions_loader.PluginLoader()
|
||||
return PLUGIN_LOADER
|
||||
|
||||
|
||||
|
0
murano/common/plugins/__init__.py
Normal file
0
murano/common/plugins/__init__.py
Normal file
@ -34,7 +34,7 @@ NAME_RE = re.compile(r'^[a-zA-Z]\w*(\.[a-zA-Z]\w*)*$')
|
||||
|
||||
class PluginLoader(object):
|
||||
def __init__(self, namespace="io.murano.extensions"):
|
||||
LOG.debug('Loading plugins')
|
||||
LOG.info('Loading extension plugins')
|
||||
self.namespace = namespace
|
||||
extension_manager = dispatch.EnabledExtensionManager(
|
||||
self.namespace,
|
||||
@ -85,6 +85,7 @@ class PluginLoader(object):
|
||||
@staticmethod
|
||||
def is_plugin_enabled(extension):
|
||||
if CONF.murano.enabled_plugins is None:
|
||||
# assume all plugins are enabled until manually specified otherwise
|
||||
return True
|
||||
else:
|
||||
return (extension.entry_point.dist.project_name in
|
96
murano/common/plugins/package_types_loader.py
Normal file
96
murano/common/plugins/package_types_loader.py
Normal file
@ -0,0 +1,96 @@
|
||||
# Copyright (c) 2015 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 semantic_version
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from stevedore import dispatch
|
||||
|
||||
from murano.common.i18n import _LE, _LW
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
NAMESPACE = 'io.murano.plugins.packages'
|
||||
|
||||
|
||||
class PluginLoader(object):
|
||||
def __init__(self):
|
||||
LOG.info('Loading package type plugins')
|
||||
extension_manager = dispatch.EnabledExtensionManager(
|
||||
NAMESPACE,
|
||||
self._is_plugin_enabled,
|
||||
on_load_failure_callback=self._on_load_failure)
|
||||
self.formats = {}
|
||||
for ext in extension_manager.extensions:
|
||||
self._load_plugin(ext)
|
||||
|
||||
def _load_plugin(self, extension):
|
||||
format_name = extension.entry_point.name
|
||||
self.register_format(format_name, extension.plugin)
|
||||
|
||||
@staticmethod
|
||||
def _is_plugin_enabled(extension):
|
||||
if CONF.murano.enabled_plugins is None:
|
||||
# assume all plugins are enabled until manually specified otherwise
|
||||
return True
|
||||
else:
|
||||
return (extension.entry_point.dist.project_name in
|
||||
CONF.murano.enabled_plugins)
|
||||
|
||||
@staticmethod
|
||||
def _on_load_failure(manager, ep, exc):
|
||||
LOG.warning(_LW("Error loading entry-point {ep} from package {dist}: "
|
||||
"{err}").format(ep=ep.name, dist=ep.dist, err=exc))
|
||||
|
||||
@staticmethod
|
||||
def _parse_format_string(format_string):
|
||||
parts = format_string.rsplit('/', 1)
|
||||
if len(parts) != 2:
|
||||
LOG.error(_LE("Incorrect format name {name}").format(
|
||||
name=format_string))
|
||||
raise ValueError(format_string)
|
||||
return (
|
||||
parts[0].strip(),
|
||||
semantic_version.Version.coerce(parts[1])
|
||||
)
|
||||
|
||||
def register_format(self, format_name, package_class):
|
||||
try:
|
||||
name, version = self._parse_format_string(format_name)
|
||||
except ValueError:
|
||||
return
|
||||
else:
|
||||
self._initialize_plugin(package_class)
|
||||
self.formats.setdefault(name, {})[version] = package_class
|
||||
LOG.info('Plugin for "{0}" package type was loaded'.format(
|
||||
format_name))
|
||||
|
||||
def get_package_handler(self, format_name):
|
||||
format_name, runtime_version = self._parse_format_string(format_name)
|
||||
|
||||
package_class = self.formats.get(format_name, {}).get(
|
||||
runtime_version)
|
||||
if package_class is None:
|
||||
return None
|
||||
return lambda *args, **kwargs: package_class(
|
||||
format_name, runtime_version, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _initialize_plugin(plugin):
|
||||
if hasattr(plugin, "init_plugin"):
|
||||
initializer = getattr(plugin, "init_plugin")
|
||||
LOG.debug("Initializing plugin class {name}".format(name=plugin))
|
||||
initializer()
|
@ -32,7 +32,7 @@ class YAQL(object):
|
||||
self.expr = expr
|
||||
|
||||
|
||||
class Dumper(yaml.Dumper):
|
||||
class Dumper(yaml.SafeDumper):
|
||||
pass
|
||||
|
||||
|
||||
@ -44,10 +44,10 @@ Dumper.add_representer(YAQL, yaql_representer)
|
||||
|
||||
|
||||
class HotPackage(package_base.PackageBase):
|
||||
def __init__(self, source_directory, manifest,
|
||||
package_format, runtime_version):
|
||||
def __init__(self, format_name, runtime_version, source_directory,
|
||||
manifest):
|
||||
super(HotPackage, self).__init__(
|
||||
source_directory, manifest, package_format, runtime_version)
|
||||
format_name, runtime_version, source_directory, manifest)
|
||||
|
||||
self._translated_class = None
|
||||
self._source_directory = source_directory
|
||||
@ -107,7 +107,12 @@ class HotPackage(package_base.PackageBase):
|
||||
|
||||
files = HotPackage._translate_files(self._source_directory)
|
||||
translated.update(HotPackage._generate_workflow(hot, files))
|
||||
self._translated_class = yaml.dump(translated, Dumper=Dumper)
|
||||
|
||||
# use default_style with double quote mark because by default PyYAML
|
||||
# doesn't put any quote marks ans as a result strings with e.g. dashes
|
||||
# may be interpreted as YAQL expressions upon load
|
||||
self._translated_class = yaml.dump(
|
||||
translated, Dumper=Dumper, default_style='"')
|
||||
|
||||
@staticmethod
|
||||
def _build_properties(hot, validate_hot_parameters):
|
||||
@ -525,4 +530,6 @@ class HotPackage(package_base.PackageBase):
|
||||
groups, self.full_name),
|
||||
'Forms': forms
|
||||
}
|
||||
return yaml.dump(translated, Dumper=Dumper)
|
||||
|
||||
# see comment above about default_style
|
||||
return yaml.dump(translated, Dumper=Dumper, default_style='"')
|
||||
|
@ -15,19 +15,35 @@
|
||||
import contextlib
|
||||
import os
|
||||
import shutil
|
||||
import string
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
import semantic_version
|
||||
import yaml
|
||||
|
||||
from murano.common.plugins import package_types_loader
|
||||
import murano.packages.exceptions as e
|
||||
import murano.packages.hot_package
|
||||
import murano.packages.mpl_package
|
||||
|
||||
|
||||
PLUGIN_LOADER = None
|
||||
|
||||
|
||||
def get_plugin_loader():
|
||||
global PLUGIN_LOADER
|
||||
|
||||
if PLUGIN_LOADER is None:
|
||||
PLUGIN_LOADER = package_types_loader.PluginLoader()
|
||||
for runtime_version in ('1.0', '1.1', '1.2'):
|
||||
format_string = 'MuranoPL/' + runtime_version
|
||||
PLUGIN_LOADER.register_format(
|
||||
format_string, murano.packages.mpl_package.MuranoPlPackage)
|
||||
PLUGIN_LOADER.register_format(
|
||||
'Heat.HOT/1.0', murano.packages.hot_package.HotPackage)
|
||||
return PLUGIN_LOADER
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def load_from_file(archive_path, target_dir=None, drop_dir=False):
|
||||
if not os.path.isfile(archive_path):
|
||||
@ -63,15 +79,6 @@ def load_from_file(archive_path, target_dir=None, drop_dir=False):
|
||||
|
||||
|
||||
def load_from_dir(source_directory, filename='manifest.yaml'):
|
||||
formats = {
|
||||
'MuranoPL': {
|
||||
('1.0.0', '1.2.0'): murano.packages.mpl_package.MuranoPlPackage,
|
||||
},
|
||||
'Heat.HOT': {
|
||||
('1.0.0', '1.0.0'): murano.packages.hot_package.HotPackage
|
||||
}
|
||||
}
|
||||
|
||||
if not os.path.isdir(source_directory) or not os.path.exists(
|
||||
source_directory):
|
||||
raise e.PackageLoadError('Invalid package directory')
|
||||
@ -87,21 +94,12 @@ def load_from_dir(source_directory, filename='manifest.yaml'):
|
||||
raise e.PackageLoadError(
|
||||
"Unable to load due to '{0}'".format(str(ex))), None, trace
|
||||
if content:
|
||||
p_format_spec = str(content.get('Format') or 'MuranoPL/1.0')
|
||||
if p_format_spec[0] in string.digits:
|
||||
p_format_spec = 'MuranoPL/' + p_format_spec
|
||||
parts = p_format_spec.split('/', 1)
|
||||
if parts[0] not in formats:
|
||||
format_spec = str(content.get('Format') or 'MuranoPL/1.0')
|
||||
if format_spec[0].isdigit():
|
||||
format_spec = 'MuranoPL/' + format_spec
|
||||
plugin_loader = get_plugin_loader()
|
||||
handler = plugin_loader.get_package_handler(format_spec)
|
||||
if handler is None:
|
||||
raise e.PackageFormatError(
|
||||
'Unknown or missing format version')
|
||||
format_set = formats[parts[0]]
|
||||
version = semantic_version.Version('0.0.0')
|
||||
if len(parts) > 1:
|
||||
version = semantic_version.Version.coerce(parts[1])
|
||||
for key, value in format_set.iteritems():
|
||||
min_version = semantic_version.Version(key[0])
|
||||
max_version = semantic_version.Version(key[1])
|
||||
if min_version <= version <= max_version:
|
||||
return value(source_directory, content, parts[0], version)
|
||||
raise e.PackageFormatError(
|
||||
'Unsupported {0} format version {1}'.format(parts[0], version))
|
||||
'Unsupported format {0}'.format(format_spec))
|
||||
return handler(source_directory, content)
|
||||
|
@ -19,10 +19,10 @@ from murano.packages import package_base
|
||||
|
||||
|
||||
class MuranoPlPackage(package_base.PackageBase):
|
||||
def __init__(self, source_directory, manifest,
|
||||
package_format, runtime_version):
|
||||
def __init__(self, format_name, runtime_version, source_directory,
|
||||
manifest):
|
||||
super(MuranoPlPackage, self).__init__(
|
||||
source_directory, manifest, package_format, runtime_version)
|
||||
format_name, runtime_version, source_directory, manifest)
|
||||
self._classes = manifest.get('Classes')
|
||||
self._ui_file = manifest.get('UI', 'ui.yaml')
|
||||
self._requirements = manifest.get('Require') or {}
|
||||
|
@ -29,15 +29,15 @@ class PackageType(object):
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Package(object):
|
||||
def __init__(self, source_directory, package_format, runtime_version):
|
||||
def __init__(self, format_name, runtime_version, source_directory):
|
||||
self._source_directory = source_directory
|
||||
self._format = package_format
|
||||
self._format_name = format_name
|
||||
self._runtime_version = runtime_version
|
||||
self._blob_cache = None
|
||||
|
||||
@property
|
||||
def format(self):
|
||||
return self._format
|
||||
def format_name(self):
|
||||
return self._format_name
|
||||
|
||||
@abc.abstractproperty
|
||||
def full_name(self):
|
||||
|
@ -26,10 +26,10 @@ from murano.packages import package
|
||||
|
||||
|
||||
class PackageBase(package.Package):
|
||||
def __init__(self, source_directory, manifest,
|
||||
package_format, runtime_version):
|
||||
def __init__(self, format_name, runtime_version,
|
||||
source_directory, manifest):
|
||||
super(PackageBase, self).__init__(
|
||||
source_directory, package_format, runtime_version)
|
||||
format_name, runtime_version, source_directory)
|
||||
self._full_name = manifest.get('FullName')
|
||||
if not self._full_name:
|
||||
raise exceptions.PackageFormatError('FullName is not specified')
|
||||
@ -49,6 +49,7 @@ class PackageBase(package.Package):
|
||||
|
||||
self._logo_cache = None
|
||||
self._supplier_logo_cache = None
|
||||
self._source_directory = source_directory
|
||||
|
||||
@abc.abstractproperty
|
||||
def requirements(self):
|
||||
@ -70,6 +71,10 @@ class PackageBase(package.Package):
|
||||
def full_name(self):
|
||||
return self._full_name
|
||||
|
||||
@property
|
||||
def source_directory(self):
|
||||
return self._source_directory
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self._version
|
||||
|
@ -13,7 +13,7 @@
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
|
||||
from murano.common import plugin_loader
|
||||
from murano.common.plugins import extensions_loader
|
||||
from murano.tests.unit import base
|
||||
|
||||
CONF = cfg.CONF
|
||||
@ -35,12 +35,12 @@ class PluginLoaderTest(base.MuranoTestCase):
|
||||
ext.entry_point.name = 'Test'
|
||||
|
||||
name_map = {}
|
||||
test_obj = plugin_loader.PluginLoader('test.namespace')
|
||||
test_obj = extensions_loader.PluginLoader('test.namespace')
|
||||
test_obj.load_extension(ext, name_map)
|
||||
self.assertEqual(1, len(test_obj.packages))
|
||||
loaded_pkg = test_obj.packages.values()[0]
|
||||
self.assertTrue(isinstance(loaded_pkg,
|
||||
plugin_loader.PackageDefinition))
|
||||
extensions_loader.PackageDefinition))
|
||||
self.assertEqual('test.namespace.Test', loaded_pkg.classes.keys()[0])
|
||||
self.assertEqual({'test.namespace.Test': test_obj.packages.keys()},
|
||||
name_map)
|
||||
@ -56,7 +56,7 @@ class PluginLoaderTest(base.MuranoTestCase):
|
||||
ext2 = mock.MagicMock(name='ext2')
|
||||
ext2.entry_point.name = 'Test1'
|
||||
|
||||
test_obj = plugin_loader.PluginLoader()
|
||||
test_obj = extensions_loader.PluginLoader()
|
||||
test_obj.load_extension(ext1, name_map)
|
||||
test_obj.load_extension(ext2, name_map)
|
||||
|
||||
@ -78,7 +78,7 @@ class PluginLoaderTest(base.MuranoTestCase):
|
||||
ext = mock.MagicMock(name='ext')
|
||||
ext.entry_point.name = 'murano-pl-class'
|
||||
|
||||
test_obj = plugin_loader.PluginLoader()
|
||||
test_obj = extensions_loader.PluginLoader()
|
||||
test_obj.load_extension(ext, name_map)
|
||||
# No packages are loaded
|
||||
self.assertEqual(0, len(test_obj.packages))
|
||||
@ -92,7 +92,7 @@ class PluginLoaderTest(base.MuranoTestCase):
|
||||
'plugin1, plugin2',
|
||||
group='murano')
|
||||
ext.entry_point.dist.project_name = 'test'
|
||||
test_method = plugin_loader.PluginLoader.is_plugin_enabled
|
||||
test_method = extensions_loader.PluginLoader.is_plugin_enabled
|
||||
self.assertFalse(test_method(ext))
|
||||
ext.entry_point.dist.project_name = 'plugin1'
|
||||
self.assertTrue(test_method(ext))
|
||||
|
Loading…
x
Reference in New Issue
Block a user