summaryrefslogtreecommitdiff
path: root/glance
diff options
context:
space:
mode:
Diffstat (limited to 'glance')
-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'))