summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorInessa Vasilevskaya <ivasilevskaya@mirantis.com>2014-11-13 21:07:15 +0400
committerMike Fedosin <mfedosin@mirantis.com>2015-03-30 20:51:10 +0300
commit8e4b2dad0b71f060b6b0c5114cb76283c4ee367e (patch)
tree8ec3099baefefaf7679f059213b77b8171065450
parentb66f3904c8ddf0c3c68b05c14b16298ec5c7fec4 (diff)
downloadglance-8e4b2dad0b71f060b6b0c5114cb76283c4ee367e.tar.gz
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
-rw-r--r--glance/common/artifacts/loader.py195
-rw-r--r--glance/common/artifacts/serialization.py17
-rw-r--r--glance/common/exception.py45
-rw-r--r--glance/contrib/__init__.py0
-rw-r--r--glance/contrib/plugins/__init__.py0
-rw-r--r--glance/contrib/plugins/artifacts_sample/__init__.py5
-rw-r--r--glance/contrib/plugins/artifacts_sample/base.py29
-rw-r--r--glance/contrib/plugins/artifacts_sample/setup.cfg25
-rw-r--r--glance/contrib/plugins/artifacts_sample/setup.py20
-rw-r--r--glance/contrib/plugins/artifacts_sample/v1/__init__.py0
-rw-r--r--glance/contrib/plugins/artifacts_sample/v1/artifact.py21
-rw-r--r--glance/contrib/plugins/artifacts_sample/v2/__init__.py0
-rw-r--r--glance/contrib/plugins/artifacts_sample/v2/artifact.py23
-rw-r--r--glance/contrib/plugins/image_artifact/__init__.py0
-rw-r--r--glance/contrib/plugins/image_artifact/requirements.txt1
-rw-r--r--glance/contrib/plugins/image_artifact/setup.cfg25
-rw-r--r--glance/contrib/plugins/image_artifact/setup.py20
-rw-r--r--glance/contrib/plugins/image_artifact/v1/__init__.py0
-rw-r--r--glance/contrib/plugins/image_artifact/v1/image.py36
-rw-r--r--glance/contrib/plugins/image_artifact/v1_1/__init__.py0
-rw-r--r--glance/contrib/plugins/image_artifact/v1_1/image.py27
-rw-r--r--glance/contrib/plugins/image_artifact/v2/__init__.py0
-rw-r--r--glance/contrib/plugins/image_artifact/v2/image.py75
-rw-r--r--glance/contrib/plugins/image_artifact/version_selector.py19
-rw-r--r--glance/tests/unit/test_artifact_type_definition_framework.py21
-rw-r--r--glance/tests/unit/test_artifacts_plugin_loader.py156
26 files changed, 726 insertions, 34 deletions
diff --git a/glance/common/artifacts/loader.py b/glance/common/artifacts/loader.py
new file mode 100644
index 000000000..6c876ae22
--- /dev/null
+++ b/glance/common/artifacts/loader.py
@@ -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)
diff --git a/glance/common/artifacts/serialization.py b/glance/common/artifacts/serialization.py
index 9712a9c8b..384cbc6c3 100644
--- a/glance/common/artifacts/serialization.py
+++ b/glance/common/artifacts/serialization.py
@@ -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)
diff --git a/glance/common/exception.py b/glance/common/exception.py
index 4b57b41c2..2e7350e28 100644
--- a/glance/common/exception.py
+++ b/glance/common/exception.py
@@ -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")
diff --git a/glance/contrib/__init__.py b/glance/contrib/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/glance/contrib/__init__.py
diff --git a/glance/contrib/plugins/__init__.py b/glance/contrib/plugins/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/glance/contrib/plugins/__init__.py
diff --git a/glance/contrib/plugins/artifacts_sample/__init__.py b/glance/contrib/plugins/artifacts_sample/__init__.py
new file mode 100644
index 000000000..f730c4393
--- /dev/null
+++ b/glance/contrib/plugins/artifacts_sample/__init__.py
@@ -0,0 +1,5 @@
+from v1 import artifact as art1
+from v2 import artifact as art2
+
+
+MY_ARTIFACT = [art1.MyArtifact, art2.MyArtifact]
diff --git a/glance/contrib/plugins/artifacts_sample/base.py b/glance/contrib/plugins/artifacts_sample/base.py
new file mode 100644
index 000000000..2763ec534
--- /dev/null
+++ b/glance/contrib/plugins/artifacts_sample/base.py
@@ -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()
diff --git a/glance/contrib/plugins/artifacts_sample/setup.cfg b/glance/contrib/plugins/artifacts_sample/setup.cfg
new file mode 100644
index 000000000..7d5234ae7
--- /dev/null
+++ b/glance/contrib/plugins/artifacts_sample/setup.cfg
@@ -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
diff --git a/glance/contrib/plugins/artifacts_sample/setup.py b/glance/contrib/plugins/artifacts_sample/setup.py
new file mode 100644
index 000000000..2a3ea51e7
--- /dev/null
+++ b/glance/contrib/plugins/artifacts_sample/setup.py
@@ -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)
diff --git a/glance/contrib/plugins/artifacts_sample/v1/__init__.py b/glance/contrib/plugins/artifacts_sample/v1/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/glance/contrib/plugins/artifacts_sample/v1/__init__.py
diff --git a/glance/contrib/plugins/artifacts_sample/v1/artifact.py b/glance/contrib/plugins/artifacts_sample/v1/artifact.py
new file mode 100644
index 000000000..f224edd30
--- /dev/null
+++ b/glance/contrib/plugins/artifacts_sample/v1/artifact.py
@@ -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"
diff --git a/glance/contrib/plugins/artifacts_sample/v2/__init__.py b/glance/contrib/plugins/artifacts_sample/v2/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/glance/contrib/plugins/artifacts_sample/v2/__init__.py
diff --git a/glance/contrib/plugins/artifacts_sample/v2/artifact.py b/glance/contrib/plugins/artifacts_sample/v2/artifact.py
new file mode 100644
index 000000000..b331f80d5
--- /dev/null
+++ b/glance/contrib/plugins/artifacts_sample/v2/artifact.py
@@ -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")
diff --git a/glance/contrib/plugins/image_artifact/__init__.py b/glance/contrib/plugins/image_artifact/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/glance/contrib/plugins/image_artifact/__init__.py
diff --git a/glance/contrib/plugins/image_artifact/requirements.txt b/glance/contrib/plugins/image_artifact/requirements.txt
new file mode 100644
index 000000000..5cee777d4
--- /dev/null
+++ b/glance/contrib/plugins/image_artifact/requirements.txt
@@ -0,0 +1 @@
+python-glanceclient
diff --git a/glance/contrib/plugins/image_artifact/setup.cfg b/glance/contrib/plugins/image_artifact/setup.cfg
new file mode 100644
index 000000000..38253c792
--- /dev/null
+++ b/glance/contrib/plugins/image_artifact/setup.cfg
@@ -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
diff --git a/glance/contrib/plugins/image_artifact/setup.py b/glance/contrib/plugins/image_artifact/setup.py
new file mode 100644
index 000000000..2a3ea51e7
--- /dev/null
+++ b/glance/contrib/plugins/image_artifact/setup.py
@@ -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)
diff --git a/glance/contrib/plugins/image_artifact/v1/__init__.py b/glance/contrib/plugins/image_artifact/v1/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/glance/contrib/plugins/image_artifact/v1/__init__.py
diff --git a/glance/contrib/plugins/image_artifact/v1/image.py b/glance/contrib/plugins/image_artifact/v1/image.py
new file mode 100644
index 000000000..176c4b9a6
--- /dev/null
+++ b/glance/contrib/plugins/image_artifact/v1/image.py
@@ -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)
diff --git a/glance/contrib/plugins/image_artifact/v1_1/__init__.py b/glance/contrib/plugins/image_artifact/v1_1/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/glance/contrib/plugins/image_artifact/v1_1/__init__.py
diff --git a/glance/contrib/plugins/image_artifact/v1_1/image.py b/glance/contrib/plugins/image_artifact/v1_1/image.py
new file mode 100644
index 000000000..fc41dae7a
--- /dev/null
+++ b/glance/contrib/plugins/image_artifact/v1_1/image.py
@@ -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')))
diff --git a/glance/contrib/plugins/image_artifact/v2/__init__.py b/glance/contrib/plugins/image_artifact/v2/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/glance/contrib/plugins/image_artifact/v2/__init__.py
diff --git a/glance/contrib/plugins/image_artifact/v2/image.py b/glance/contrib/plugins/image_artifact/v2/image.py
new file mode 100644
index 000000000..cf6002153
--- /dev/null
+++ b/glance/contrib/plugins/image_artifact/v2/image.py
@@ -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")
+ )
diff --git a/glance/contrib/plugins/image_artifact/version_selector.py b/glance/contrib/plugins/image_artifact/version_selector.py
new file mode 100644
index 000000000..afbe30345
--- /dev/null
+++ b/glance/contrib/plugins/image_artifact/version_selector.py
@@ -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]
diff --git a/glance/tests/unit/test_artifact_type_definition_framework.py b/glance/tests/unit/test_artifact_type_definition_framework.py
index a0302b8d4..a4084e8dd 100644
--- a/glance/tests/unit/test_artifact_type_definition_framework.py
+++ b/glance/tests/unit/test_artifact_type_definition_framework.py
@@ -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):
]
}
}
-
- art = serialization.deserialize_from_db(db_dict,
- {
- 'SerTestType': {
- '1.0': SerTestType},
- 'ArtifactType': {
- '1.0':
- defs.ArtifactType}
- })
+ plugins_dict = {'SerTestType': [SerTestType],
+ 'ArtifactType': [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)
diff --git a/glance/tests/unit/test_artifacts_plugin_loader.py b/glance/tests/unit/test_artifacts_plugin_loader.py
new file mode 100644
index 000000000..6c3cfcd7e
--- /dev/null
+++ b/glance/tests/unit/test_artifacts_plugin_loader.py
@@ -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'))