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:
parent
b66f3904c8
commit
8e4b2dad0b
195
glance/common/artifacts/loader.py
Normal file
195
glance/common/artifacts/loader.py
Normal 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)
|
@ -194,7 +194,7 @@ def _deserialize_blobs(artifact_type, blobs_from_db, artifact_properties):
|
|||||||
|
|
||||||
|
|
||||||
def _deserialize_dependencies(artifact_type, deps_from_db,
|
def _deserialize_dependencies(artifact_type, deps_from_db,
|
||||||
artifact_properties, type_dictionary):
|
artifact_properties, plugins):
|
||||||
"""Retrieves dependencies from database"""
|
"""Retrieves dependencies from database"""
|
||||||
for dep_name, dep_value in six.iteritems(deps_from_db):
|
for dep_name, dep_value in six.iteritems(deps_from_db):
|
||||||
if not dep_value:
|
if not dep_value:
|
||||||
@ -204,9 +204,9 @@ def _deserialize_dependencies(artifact_type, deps_from_db,
|
|||||||
declarative.ListAttributeDefinition):
|
declarative.ListAttributeDefinition):
|
||||||
val = []
|
val = []
|
||||||
for v in dep_value:
|
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:
|
elif len(dep_value) == 1:
|
||||||
val = deserialize_from_db(dep_value[0], type_dictionary)
|
val = deserialize_from_db(dep_value[0], plugins)
|
||||||
else:
|
else:
|
||||||
raise exception.InvalidArtifactPropertyValue(
|
raise exception.InvalidArtifactPropertyValue(
|
||||||
message=_('Relation %(name)s may not have multiple values'),
|
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
|
artifact_properties[dep_name] = val
|
||||||
|
|
||||||
|
|
||||||
def deserialize_from_db(db_dict, type_dictionary):
|
def deserialize_from_db(db_dict, plugins):
|
||||||
artifact_properties = {}
|
artifact_properties = {}
|
||||||
type_name = None
|
type_name = None
|
||||||
type_version = None
|
type_version = None
|
||||||
@ -228,10 +228,9 @@ def deserialize_from_db(db_dict, type_dictionary):
|
|||||||
else:
|
else:
|
||||||
artifact_properties[prop_name] = prop_value
|
artifact_properties[prop_name] = prop_value
|
||||||
|
|
||||||
if type_name and type_version and (type_version in
|
try:
|
||||||
type_dictionary.get(type_name, [])):
|
artifact_type = plugins.get_class_by_typename(type_name, type_version)
|
||||||
artifact_type = type_dictionary[type_name][type_version]
|
except exception.ArtifactPluginNotFound:
|
||||||
else:
|
|
||||||
raise exception.UnknownArtifactType(name=type_name,
|
raise exception.UnknownArtifactType(name=type_name,
|
||||||
version=type_version)
|
version=type_version)
|
||||||
|
|
||||||
@ -260,6 +259,6 @@ def deserialize_from_db(db_dict, type_dictionary):
|
|||||||
|
|
||||||
dependencies = db_dict.pop('dependencies', {})
|
dependencies = db_dict.pop('dependencies', {})
|
||||||
_deserialize_dependencies(artifact_type, dependencies,
|
_deserialize_dependencies(artifact_type, dependencies,
|
||||||
artifact_properties, type_dictionary)
|
artifact_properties, plugins)
|
||||||
|
|
||||||
return artifact_type(**artifact_properties)
|
return artifact_type(**artifact_properties)
|
||||||
|
@ -452,6 +452,24 @@ class InvalidVersion(Invalid):
|
|||||||
message = _("Version is invalid: %(reason)s")
|
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):
|
class ArtifactNotFound(NotFound):
|
||||||
message = _("Artifact with id=%(id)s was not found")
|
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")
|
message = _("Cannot use this parameter with the operator %(op)s")
|
||||||
|
|
||||||
|
|
||||||
class ArtifactInvalidStateTransition(Invalid):
|
class ArtifactLoadError(GlanceException):
|
||||||
message = _("Artifact state cannot be changed from %(curr)s to %(to)s")
|
message = _("Cannot load artifact '%(name)s'")
|
||||||
|
|
||||||
|
|
||||||
class InvalidArtifactTypePropertyDefinition(Invalid):
|
class ArtifactNonMatchingTypeName(ArtifactLoadError):
|
||||||
message = _("Invalid property definition")
|
message = _(
|
||||||
|
"Plugin name '%(plugin)s' should match artifact typename '%(name)s'")
|
||||||
|
|
||||||
|
|
||||||
class InvalidArtifactTypeDefinition(Invalid):
|
class ArtifactPluginNotFound(NotFound):
|
||||||
message = _("Invalid type definition")
|
message = _("No plugin for '%(name)s' has been loaded")
|
||||||
|
|
||||||
|
|
||||||
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 UnknownArtifactType(NotFound):
|
class UnknownArtifactType(NotFound):
|
||||||
message = _("Artifact type with name '%(name)s' and version '%(version)s' "
|
message = _("Artifact type with name '%(name)s' and version '%(version)s' "
|
||||||
"is not known")
|
"is not known")
|
||||||
|
|
||||||
|
|
||||||
|
class ArtifactInvalidStateTransition(Invalid):
|
||||||
|
message = _("Artifact state cannot be changed from %(curr)s to %(to)s")
|
||||||
|
0
glance/contrib/__init__.py
Normal file
0
glance/contrib/__init__.py
Normal file
0
glance/contrib/plugins/__init__.py
Normal file
0
glance/contrib/plugins/__init__.py
Normal file
5
glance/contrib/plugins/artifacts_sample/__init__.py
Normal file
5
glance/contrib/plugins/artifacts_sample/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from v1 import artifact as art1
|
||||||
|
from v2 import artifact as art2
|
||||||
|
|
||||||
|
|
||||||
|
MY_ARTIFACT = [art1.MyArtifact, art2.MyArtifact]
|
29
glance/contrib/plugins/artifacts_sample/base.py
Normal file
29
glance/contrib/plugins/artifacts_sample/base.py
Normal 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()
|
25
glance/contrib/plugins/artifacts_sample/setup.cfg
Normal file
25
glance/contrib/plugins/artifacts_sample/setup.cfg
Normal 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
|
20
glance/contrib/plugins/artifacts_sample/setup.py
Normal file
20
glance/contrib/plugins/artifacts_sample/setup.py
Normal 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)
|
21
glance/contrib/plugins/artifacts_sample/v1/artifact.py
Normal file
21
glance/contrib/plugins/artifacts_sample/v1/artifact.py
Normal 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"
|
23
glance/contrib/plugins/artifacts_sample/v2/artifact.py
Normal file
23
glance/contrib/plugins/artifacts_sample/v2/artifact.py
Normal 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")
|
0
glance/contrib/plugins/image_artifact/__init__.py
Normal file
0
glance/contrib/plugins/image_artifact/__init__.py
Normal file
1
glance/contrib/plugins/image_artifact/requirements.txt
Normal file
1
glance/contrib/plugins/image_artifact/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
python-glanceclient
|
25
glance/contrib/plugins/image_artifact/setup.cfg
Normal file
25
glance/contrib/plugins/image_artifact/setup.cfg
Normal 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
|
20
glance/contrib/plugins/image_artifact/setup.py
Normal file
20
glance/contrib/plugins/image_artifact/setup.py
Normal 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)
|
36
glance/contrib/plugins/image_artifact/v1/image.py
Normal file
36
glance/contrib/plugins/image_artifact/v1/image.py
Normal 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)
|
27
glance/contrib/plugins/image_artifact/v1_1/image.py
Normal file
27
glance/contrib/plugins/image_artifact/v1_1/image.py
Normal 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')))
|
75
glance/contrib/plugins/image_artifact/v2/image.py
Normal file
75
glance/contrib/plugins/image_artifact/v2/image.py
Normal 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")
|
||||||
|
)
|
19
glance/contrib/plugins/image_artifact/version_selector.py
Normal file
19
glance/contrib/plugins/image_artifact/version_selector.py
Normal 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]
|
@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
from glance.common.artifacts import declarative
|
from glance.common.artifacts import declarative
|
||||||
import glance.common.artifacts.definitions as defs
|
import glance.common.artifacts.definitions as defs
|
||||||
from glance.common.artifacts import serialization
|
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,
|
def _retrieve_plugin(name, version):
|
||||||
{
|
return next((p for p in plugins_dict.get(name, [])
|
||||||
'SerTestType': {
|
if version and p.version == version),
|
||||||
'1.0': SerTestType},
|
plugins_dict.get(name, [None])[0])
|
||||||
'ArtifactType': {
|
plugins = mock.Mock()
|
||||||
'1.0':
|
plugins.get_class_by_typename = _retrieve_plugin
|
||||||
defs.ArtifactType}
|
art = serialization.deserialize_from_db(db_dict, plugins)
|
||||||
})
|
|
||||||
self.assertEqual('123', art.id)
|
self.assertEqual('123', art.id)
|
||||||
self.assertEqual('11.2', art.version)
|
self.assertEqual('11.2', art.version)
|
||||||
self.assertIsNone(art.description)
|
self.assertIsNone(art.description)
|
||||||
|
156
glance/tests/unit/test_artifacts_plugin_loader.py
Normal file
156
glance/tests/unit/test_artifacts_plugin_loader.py
Normal 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'))
|
Loading…
Reference in New Issue
Block a user