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:
Stan Lagun 2015-10-15 01:19:12 +03:00 committed by Victor Ryzhenkin
parent 0ced007109
commit 150aed4910
10 changed files with 160 additions and 53 deletions

View File

@ -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

View File

View 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

View 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()

View File

@ -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='"')

View File

@ -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)

View File

@ -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 {}

View File

@ -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):

View File

@ -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

View File

@ -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))