diff options
author | Inessa Vasilevskaya <ivasilevskaya@mirantis.com> | 2014-11-13 21:07:15 +0400 |
---|---|---|
committer | Mike Fedosin <mfedosin@mirantis.com> | 2015-03-30 20:51:10 +0300 |
commit | 8e4b2dad0b71f060b6b0c5114cb76283c4ee367e (patch) | |
tree | 8ec3099baefefaf7679f059213b77b8171065450 | |
parent | b66f3904c8ddf0c3c68b05c14b16298ec5c7fec4 (diff) | |
download | glance-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
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')) |