Artifact Plugins Loader

Adds a Stevedore-based plugin loader which is capable to load custom
Artifact Types implemented as classes or lists of classes.

The Loader validates if entry-points' names match the type names of the
ArtifactTypes, ensures that no version conflicts occur (i.e. there are
no Artifact Types which have identical combination of Type Name and Type
Version), maintains the mapping of Type Names to the actual classes and
also keeps an index of Artifact Types by their endpoint aliases.

Modifies the Serialization utility of Declarative framework to use the
actual set of plugins instead of synthetic type dictionary.

Adds several example plugins to glance/contrib directory.

Implements-blueprint: artifact-repository

Co-Authored-By: Inessa Vasilevskaya <ivasilevskaya@mirantis.com>
Co-Authored-By: Alexander Tivelkov <ativelkov@mirantis.com>

Change-Id: I5ff5d4c257a1c42885068f4343f52e55189265a5
This commit is contained in:
Inessa Vasilevskaya 2014-11-13 21:07:15 +04:00 committed by Mike Fedosin
parent b66f3904c8
commit 8e4b2dad0b
26 changed files with 725 additions and 33 deletions

View File

@ -0,0 +1,195 @@
# Copyright 2011-2012 OpenStack Foundation
# 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 copy
from oslo.config import cfg
import semantic_version
from stevedore import enabled
from glance.common.artifacts import definitions
from glance.common import exception
from glance import i18n
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
_ = i18n._
_LE = i18n._LE
_LW = i18n._LW
_LI = i18n._LI
plugins_opts = [
cfg.BoolOpt('load_enabled', default=True,
help=_('When false, no artifacts can be loaded regardless of'
' available_plugins. When true, artifacts can be'
' loaded.')),
cfg.ListOpt('available_plugins', default=[],
help=_('A list of artifacts that are allowed in the'
' format name or name-version. Empty list means that'
' any artifact can be loaded.'))
]
CONF = cfg.CONF
CONF.register_opts(plugins_opts)
class ArtifactsPluginLoader(object):
def __init__(self, namespace):
self.mgr = enabled.EnabledExtensionManager(
check_func=self._gen_check_func(),
namespace=namespace,
propagate_map_exceptions=True,
on_load_failure_callback=self._on_load_failure)
self.plugin_map = {'by_typename': {},
'by_endpoint': {}}
def _add_extention(ext):
"""
Plugins can be loaded as entry_point=single plugin and
entry_point=PLUGIN_LIST, where PLUGIN_LIST is a python variable
holding a list of plugins
"""
def _load_one(plugin):
if issubclass(plugin, definitions.ArtifactType):
# make sure that have correct plugin name
art_name = plugin.metadata.type_name
if art_name != ext.name:
raise exception.ArtifactNonMatchingTypeName(
name=art_name, plugin=ext.name)
# make sure that no plugin with the same name and version
# already exists
exists = self._get_plugins(ext.name)
new_tv = plugin.metadata.type_version
if any(e.metadata.type_version == new_tv for e in exists):
raise exception.ArtifactDuplicateNameTypeVersion()
self._add_plugin("by_endpoint", plugin.metadata.endpoint,
plugin)
self._add_plugin("by_typename", plugin.metadata.type_name,
plugin)
if isinstance(ext.plugin, list):
for p in ext.plugin:
_load_one(p)
else:
_load_one(ext.plugin)
# (ivasilevskaya) that looks pretty bad as RuntimeError is too general,
# but stevedore has awful exception wrapping with no specific class
# for this very case (no extensions for given namespace found)
try:
self.mgr.map(_add_extention)
except RuntimeError as re:
LOG.error(_LE("Unable to load artifacts: %s") % re.message)
def _version(self, artifact):
return semantic_version.Version.coerce(artifact.metadata.type_version)
def _add_plugin(self, spec, name, plugin):
"""
Inserts a new plugin into a sorted by desc type_version list
of existing plugins in order to retrieve the latest by next()
"""
def _add(name, value):
self.plugin_map[spec][name] = value
old_order = copy.copy(self._get_plugins(name, spec=spec))
for i, p in enumerate(old_order):
if self._version(p) < self._version(plugin):
_add(name, old_order[0:i] + [plugin] + old_order[i:])
return
_add(name, old_order + [plugin])
def _get_plugins(self, name, spec="by_typename"):
if spec not in self.plugin_map.keys():
return []
return self.plugin_map[spec].get(name, [])
def _gen_check_func(self):
"""generates check_func for EnabledExtensionManager"""
def _all_forbidden(ext):
LOG.warn(_LW("Can't load artifact %s: load disabled in config") %
ext.name)
raise exception.ArtifactLoadError(name=ext.name)
def _all_allowed(ext):
LOG.info(
_LI("Artifact %s has been successfully loaded") % ext.name)
return True
if not CONF.load_enabled:
return _all_forbidden
if len(CONF.available_plugins) == 0:
return _all_allowed
available = []
for name in CONF.available_plugins:
type_name, version = (name.split('-', 1)
if '-' in name else (name, None))
available.append((type_name, version))
def _check_ext(ext):
try:
next(n for n, v in available
if n == ext.plugin.metadata.type_name and
(v is None or v == ext.plugin.metadata.type_version))
except StopIteration:
LOG.warn(_LW("Can't load artifact %s: not in"
" available_plugins list") % ext.name)
raise exception.ArtifactLoadError(name=ext.name)
LOG.info(
_LI("Artifact %s has been successfully loaded") % ext.name)
return True
return _check_ext
# this has to be done explicitly as stevedore is pretty ignorant when
# face to face with an Exception and tries to swallow it and print sth
# irrelevant instead of expected error message
def _on_load_failure(self, manager, ep, exc):
msg = (_LE("Could not load plugin from %(module)s: %(msg)s") %
{"module": ep.module_name, "msg": exc})
LOG.error(msg)
raise exc
def _find_class_in_collection(self, collection, name, version=None):
try:
def _cmp_version(plugin, version):
ver = semantic_version.Version.coerce
return (ver(plugin.metadata.type_version) ==
ver(version))
if version:
return next((p for p in collection
if _cmp_version(p, version)))
return next((p for p in collection))
except StopIteration:
raise exception.ArtifactPluginNotFound(
name="%s %s" % (name, "v %s" % version if version else ""))
def get_class_by_endpoint(self, name, version=None):
if version is None:
classlist = self._get_plugins(name, spec="by_endpoint")
if not classlist:
raise exception.ArtifactPluginNotFound(name=name)
return self._find_class_in_collection(classlist, name)
return self._find_class_in_collection(
self._get_plugins(name, spec="by_endpoint"), name, version)
def get_class_by_typename(self, name, version=None):
return self._find_class_in_collection(
self._get_plugins(name, spec="by_typename"), name, version)

View File

@ -194,7 +194,7 @@ def _deserialize_blobs(artifact_type, blobs_from_db, artifact_properties):
def _deserialize_dependencies(artifact_type, deps_from_db,
artifact_properties, type_dictionary):
artifact_properties, plugins):
"""Retrieves dependencies from database"""
for dep_name, dep_value in six.iteritems(deps_from_db):
if not dep_value:
@ -204,9 +204,9 @@ def _deserialize_dependencies(artifact_type, deps_from_db,
declarative.ListAttributeDefinition):
val = []
for v in dep_value:
val.append(deserialize_from_db(v, type_dictionary))
val.append(deserialize_from_db(v, plugins))
elif len(dep_value) == 1:
val = deserialize_from_db(dep_value[0], type_dictionary)
val = deserialize_from_db(dep_value[0], plugins)
else:
raise exception.InvalidArtifactPropertyValue(
message=_('Relation %(name)s may not have multiple values'),
@ -214,7 +214,7 @@ def _deserialize_dependencies(artifact_type, deps_from_db,
artifact_properties[dep_name] = val
def deserialize_from_db(db_dict, type_dictionary):
def deserialize_from_db(db_dict, plugins):
artifact_properties = {}
type_name = None
type_version = None
@ -228,10 +228,9 @@ def deserialize_from_db(db_dict, type_dictionary):
else:
artifact_properties[prop_name] = prop_value
if type_name and type_version and (type_version in
type_dictionary.get(type_name, [])):
artifact_type = type_dictionary[type_name][type_version]
else:
try:
artifact_type = plugins.get_class_by_typename(type_name, type_version)
except exception.ArtifactPluginNotFound:
raise exception.UnknownArtifactType(name=type_name,
version=type_version)
@ -260,6 +259,6 @@ def deserialize_from_db(db_dict, type_dictionary):
dependencies = db_dict.pop('dependencies', {})
_deserialize_dependencies(artifact_type, dependencies,
artifact_properties, type_dictionary)
artifact_properties, plugins)
return artifact_type(**artifact_properties)

View File

@ -452,6 +452,24 @@ class InvalidVersion(Invalid):
message = _("Version is invalid: %(reason)s")
class InvalidArtifactTypePropertyDefinition(Invalid):
message = _("Invalid property definition")
class InvalidArtifactTypeDefinition(Invalid):
message = _("Invalid type definition")
class InvalidArtifactPropertyValue(Invalid):
message = _("Property '%(name)s' may not have value '%(val)s': %(msg)s")
def __init__(self, message=None, *args, **kwargs):
super(InvalidArtifactPropertyValue, self).__init__(message, *args,
**kwargs)
self.name = kwargs.get('name')
self.value = kwargs.get('val')
class ArtifactNotFound(NotFound):
message = _("Artifact with id=%(id)s was not found")
@ -499,28 +517,23 @@ class ArtifactInvalidPropertyParameter(Invalid):
message = _("Cannot use this parameter with the operator %(op)s")
class ArtifactInvalidStateTransition(Invalid):
message = _("Artifact state cannot be changed from %(curr)s to %(to)s")
class ArtifactLoadError(GlanceException):
message = _("Cannot load artifact '%(name)s'")
class InvalidArtifactTypePropertyDefinition(Invalid):
message = _("Invalid property definition")
class ArtifactNonMatchingTypeName(ArtifactLoadError):
message = _(
"Plugin name '%(plugin)s' should match artifact typename '%(name)s'")
class InvalidArtifactTypeDefinition(Invalid):
message = _("Invalid type definition")
class InvalidArtifactPropertyValue(Invalid):
message = _("Property '%(name)s' may not have value '%(val)s': %(msg)s")
def __init__(self, message=None, *args, **kwargs):
super(InvalidArtifactPropertyValue, self).__init__(message, *args,
**kwargs)
self.name = kwargs.get('name')
self.value = kwargs.get('val')
class ArtifactPluginNotFound(NotFound):
message = _("No plugin for '%(name)s' has been loaded")
class UnknownArtifactType(NotFound):
message = _("Artifact type with name '%(name)s' and version '%(version)s' "
"is not known")
class ArtifactInvalidStateTransition(Invalid):
message = _("Artifact state cannot be changed from %(curr)s to %(to)s")

View File

View File

View File

@ -0,0 +1,5 @@
from v1 import artifact as art1
from v2 import artifact as art2
MY_ARTIFACT = [art1.MyArtifact, art2.MyArtifact]

View File

@ -0,0 +1,29 @@
# Copyright 2011-2012 OpenStack Foundation
# 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 glance.common.artifacts import definitions
class BaseArtifact(definitions.ArtifactType):
__type_version__ = "1.0"
prop1 = definitions.String()
prop2 = definitions.Integer()
int_list = definitions.Array(item_type=definitions.Integer(max_value=10,
min_value=1))
depends_on = definitions.ArtifactReference(type_name='MyArtifact')
references = definitions.ArtifactReferenceList()
image_file = definitions.BinaryObject()
screenshots = definitions.BinaryObjectList()

View File

@ -0,0 +1,25 @@
[metadata]
name = artifact
version = 0.0.1
description = A sample plugin for artifact loading
author = Inessa Vasilevskaya
author-email = ivasilevskaya@mirantis.com
classifier =
Development Status :: 3 - Alpha
License :: OSI Approved :: Apache Software License
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.2
Programming Language :: Python :: 3.3
Intended Audience :: Developers
Environment :: Console
[global]
setup-hooks =
pbr.hooks.setup_hook
[entry_points]
glance.artifacts.types =
MyArtifact = glance.contrib.plugins.artifacts_sample:MY_ARTIFACT

View File

@ -0,0 +1,20 @@
# Copyright 2011-2012 OpenStack Foundation
# 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 setuptools
# all other params will be taken from setup.cfg
setuptools.setup(packages=setuptools.find_packages(),
setup_requires=['pbr'], pbr=True)

View File

@ -0,0 +1,21 @@
# Copyright 2011-2012 OpenStack Foundation
# 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 glance.contrib.plugins.artifacts_sample import base
class MyArtifact(base.BaseArtifact):
__type_version__ = "1.0.1"

View File

@ -0,0 +1,23 @@
# Copyright 2011-2012 OpenStack Foundation
# 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 glance.common.artifacts import definitions
from glance.contrib.plugins.artifacts_sample import base
class MyArtifact(base.BaseArtifact):
__type_version__ = "2.0"
depends_on = definitions.ArtifactReference(type_name="MyArtifact")

View File

@ -0,0 +1 @@
python-glanceclient

View File

@ -0,0 +1,25 @@
[metadata]
name = image_artifact_plugin
version = 2.0
description = An artifact plugin for Imaging functionality
author = Alexander Tivelkov
author-email = ativelkov@mirantis.com
classifier =
Development Status :: 3 - Alpha
License :: OSI Approved :: Apache Software License
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.2
Programming Language :: Python :: 3.3
Intended Audience :: Developers
Environment :: Console
[global]
setup-hooks =
pbr.hooks.setup_hook
[entry_points]
glance.artifacts.types =
Image = glance.contrib.plugins.image_artifact.version_selector:versions

View File

@ -0,0 +1,20 @@
# Copyright 2011-2012 OpenStack Foundation
# 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 setuptools
# all other params will be taken from setup.cfg
setuptools.setup(packages=setuptools.find_packages(),
setup_requires=['pbr'], pbr=True)

View File

@ -0,0 +1,36 @@
# 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 glance.common.artifacts import definitions
class ImageAsAnArtifact(definitions.ArtifactType):
__type_name__ = 'Image'
__endpoint__ = 'images'
file = definitions.BinaryObject(required=True)
disk_format = definitions.String(allowed_values=['ami', 'ari', 'aki',
'vhd', 'vmdk', 'raw',
'qcow2', 'vdi', 'iso'],
required=True,
mutable=False)
container_format = definitions.String(allowed_values=['ami', 'ari',
'aki', 'bare',
'ovf', 'ova'],
required=True,
mutable=False)
min_disk = definitions.Integer(min_value=0, default=0)
min_ram = definitions.Integer(min_value=0, default=0)
virtual_size = definitions.Integer(min_value=0)

View File

@ -0,0 +1,27 @@
# 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 glance.common.artifacts import definitions
import glance.contrib.plugins.image_artifact.v1.image as v1
class ImageAsAnArtifact(v1.ImageAsAnArtifact):
__type_version__ = '1.1'
icons = definitions.BinaryObjectList()
similar_images = (definitions.
ArtifactReferenceList(references=definitions.
ArtifactReference('Image')))

View File

@ -0,0 +1,75 @@
# 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 glance.common.artifacts import definitions
from glance.common import exception
import glance.contrib.plugins.image_artifact.v1_1.image as v1_1
import glanceclient
from glance import i18n
_ = i18n._
class ImageAsAnArtifact(v1_1.ImageAsAnArtifact):
__type_version__ = '2.0'
file = definitions.BinaryObject(required=False)
legacy_image_id = definitions.String(required=False, mutable=False,
pattern=R'[0-9a-f]{8}-[0-9a-f]{4}'
R'-4[0-9a-f]{3}-[89ab]'
R'[0-9a-f]{3}-[0-9a-f]{12}')
def __pre_publish__(self, context, *args, **kwargs):
super(ImageAsAnArtifact, self).__pre_publish__(*args, **kwargs)
if self.file is None and self.legacy_image_id is None:
raise exception.InvalidArtifactPropertyValue(
message=_("Either a file or a legacy_image_id has to be "
"specified")
)
if self.file is not None and self.legacy_image_id is not None:
raise exception.InvalidArtifactPropertyValue(
message=_("Both file and legacy_image_id may not be "
"specified at the same time"))
if self.legacy_image_id:
glance_endpoint = next(service['endpoints'][0]['publicURL']
for service in context.service_catalog
if service['name'] == 'glance')
try:
client = glanceclient.Client(version=2,
endpoint=glance_endpoint,
token=context.auth_token)
legacy_image = client.images.get(self.legacy_image_id)
except Exception:
raise exception.InvalidArtifactPropertyValue(
message=_('Unable to get legacy image')
)
if legacy_image is not None:
self.file = definitions.Blob(size=legacy_image.size,
locations=[
{
"status": "active",
"value":
legacy_image.direct_url
}],
checksum=legacy_image.checksum,
item_key=legacy_image.id)
else:
raise exception.InvalidArtifactPropertyValue(
message=_("Legacy image was not found")
)

View File

@ -0,0 +1,19 @@
# 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 v1 import image as v1
from v1_1 import image as v1_1
from v2 import image as v2
versions = [v1.ImageAsAnArtifact, v1_1.ImageAsAnArtifact, v2.ImageAsAnArtifact]

View File

@ -14,6 +14,8 @@
import datetime
import mock
from glance.common.artifacts import declarative
import glance.common.artifacts.definitions as defs
from glance.common.artifacts import serialization
@ -1072,15 +1074,16 @@ class TestSerialization(test_utils.BaseTestCase):
]
}
}
plugins_dict = {'SerTestType': [SerTestType],
'ArtifactType': [defs.ArtifactType]}
art = serialization.deserialize_from_db(db_dict,
{
'SerTestType': {
'1.0': SerTestType},
'ArtifactType': {
'1.0':
defs.ArtifactType}
})
def _retrieve_plugin(name, version):
return next((p for p in plugins_dict.get(name, [])
if version and p.version == version),
plugins_dict.get(name, [None])[0])
plugins = mock.Mock()
plugins.get_class_by_typename = _retrieve_plugin
art = serialization.deserialize_from_db(db_dict, plugins)
self.assertEqual('123', art.id)
self.assertEqual('11.2', art.version)
self.assertIsNone(art.description)

View File

@ -0,0 +1,156 @@
# 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 os
import mock
import pkg_resources
from glance.common.artifacts import loader
from glance.common import exception
from glance.contrib.plugins.artifacts_sample.v1 import artifact as art1
from glance.contrib.plugins.artifacts_sample.v2 import artifact as art2
from glance.tests import utils
class MyArtifactDuplicate(art1.MyArtifact):
__type_version__ = '1.0.1'
__type_name__ = 'MyArtifact'
class MyArtifactOk(art1.MyArtifact):
__type_version__ = '1.0.2'
__type_name__ = 'MyArtifact'
class TestArtifactsLoader(utils.BaseTestCase):
def setUp(self):
self.path = 'glance.contrib.plugins.artifacts_sample'
self._setup_loader(['MyArtifact=%s.v1.artifact:MyArtifact' %
self.path])
super(TestArtifactsLoader, self).setUp()
def _setup_loader(self, artifacts):
self.loader = None
mock_this = 'stevedore.extension.ExtensionManager._find_entry_points'
with mock.patch(mock_this) as fep:
fep.return_value = [
pkg_resources.EntryPoint.parse(art) for art in artifacts]
self.loader = loader.ArtifactsPluginLoader(
'glance.artifacts.types')
def test_load(self):
"""
Plugins can be loaded as entrypoint=sigle plugin and
entrypoint=[a, list, of, plugins]
"""
# single version
self.assertEqual(1, len(self.loader.mgr.extensions))
self.assertEqual(art1.MyArtifact,
self.loader.get_class_by_endpoint('myartifact'))
# entrypoint = [a, list]
path = os.path.splitext(__file__)[0].replace('/', '.')
self._setup_loader([
'MyArtifact=%s:MyArtifactOk' % path,
'MyArtifact=%s.v2.artifact:MyArtifact' % self.path,
'MyArtifact=%s.v1.artifact:MyArtifact' % self.path]),
self.assertEqual(3, len(self.loader.mgr.extensions))
# returns the plugin with the latest version
self.assertEqual(art2.MyArtifact,
self.loader.get_class_by_endpoint('myartifact'))
self.assertEqual(art1.MyArtifact,
self.loader.get_class_by_endpoint('myartifact',
'1.0.1'))
def test_basic_loader_func(self):
"""Test public methods of PluginLoader class here"""
# type_version 2 == 2.0 == 2.0.0
self._setup_loader(
['MyArtifact=%s.v2.artifact:MyArtifact' % self.path])
self.assertEqual(art2.MyArtifact,
self.loader.get_class_by_endpoint('myartifact'))
self.assertEqual(art2.MyArtifact,
self.loader.get_class_by_endpoint('myartifact',
'2.0'))
self.assertEqual(art2.MyArtifact,
self.loader.get_class_by_endpoint('myartifact',
'2.0.0'))
self.assertEqual(art2.MyArtifact,
self.loader.get_class_by_endpoint('myartifact',
'2'))
# now make sure that get_class_by_typename works as well
self.assertEqual(art2.MyArtifact,
self.loader.get_class_by_typename('MyArtifact'))
self.assertEqual(art2.MyArtifact,
self.loader.get_class_by_typename('MyArtifact', '2'))
def test_config_validation(self):
"""
Plugins can be loaded on certain conditions:
* entry point name == type_name
* no plugin with the same type_name and version has been already
loaded
"""
path = 'glance.contrib.plugins.artifacts_sample'
# here artifacts specific validation is checked
self.assertRaises(exception.ArtifactNonMatchingTypeName,
self._setup_loader,
['non_matching_name=%s.v1.artifact:MyArtifact' %
path])
# make sure this call is ok
self._setup_loader(['MyArtifact=%s.v1.artifact:MyArtifact' % path])
art_type = self.loader.get_class_by_endpoint('myartifact')
self.assertEqual('MyArtifact', art_type.metadata.type_name)
self.assertEqual('1.0.1', art_type.metadata.type_version)
# now try to add duplicate artifact with the same type_name and
# type_version as already exists
bad_art_path = os.path.splitext(__file__)[0].replace('/', '.')
self.assertEqual(art_type.metadata.type_version,
MyArtifactDuplicate.metadata.type_version)
self.assertEqual(art_type.metadata.type_name,
MyArtifactDuplicate.metadata.type_name)
# should raise an exception as (name, version) is not unique
self.assertRaises(
exception.ArtifactDuplicateNameTypeVersion, self._setup_loader,
['MyArtifact=%s.v1.artifact:MyArtifact' % path,
'MyArtifact=%s:MyArtifactDuplicate' % bad_art_path])
# two artifacts with the same name but different versions coexist fine
self.assertEqual('MyArtifact', MyArtifactOk.metadata.type_name)
self.assertNotEqual(art_type.metadata.type_version,
MyArtifactOk.metadata.type_version)
self._setup_loader(['MyArtifact=%s.v1.artifact:MyArtifact' % path,
'MyArtifact=%s:MyArtifactOk' % bad_art_path])
def test_check_function(self):
"""
A test to show that plugin-load specific options in artifacts.conf
are correctly processed:
* no plugins can be loaded if load_enabled = False
* if available_plugins list is given only plugins specified can be
be loaded
"""
self.config(load_enabled=False)
self.assertRaises(exception.ArtifactLoadError,
self._setup_loader,
['MyArtifact=%s.v1.artifact:MyArtifact' % self.path])
self.config(load_enabled=True, available_plugins=['MyArtifact-1.0.2'])
self.assertRaises(exception.ArtifactLoadError,
self._setup_loader,
['MyArtifact=%s.v1.artifact:MyArtifact' % self.path])
path = os.path.splitext(__file__)[0].replace('/', '.')
self._setup_loader(['MyArtifact=%s:MyArtifactOk' % path])
# make sure that plugin_map has the expected plugin
self.assertEqual(MyArtifactOk,
self.loader.get_class_by_endpoint('myartifact',
'1.0.2'))