diff options
Diffstat (limited to 'glance')
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')) |