REST API layer for Artifact Repository
Adds REST API layer for Artifact Repository, binds all other changes together. Artifact Repository is run as a part of Glance api. Requests are routed to /v3/artifacts endpoint on the api port. API version in version selector is marked as experimental. Registers artifacts service and a sample Artifact Type in Glance entry points config. Implements-blueprint: artifact-repository FastTrack Co-Authored-By: Inessa Vasilevskaya <ivasilevskaya@mirantis.com> Co-Authored-By: Mike Fedosin <mfedosin@mirantis.com> Co-Authored-By: Alexander Tivelkov <ativelkov@mirantis.com> Change-Id: Ib6a0d2482208a37aa343a747dfe5f63f9080dd04
This commit is contained in:
parent
35e35a17bd
commit
3cdd5bba7c
@ -39,6 +39,7 @@ paste.composite_factory = glance.api:root_app_factory
|
||||
/: apiversions
|
||||
/v1: apiv1app
|
||||
/v2: apiv2app
|
||||
/v3: apiv3app
|
||||
|
||||
[app:apiversions]
|
||||
paste.app_factory = glance.api.versions:create_resource
|
||||
@ -49,6 +50,9 @@ paste.app_factory = glance.api.v1.router:API.factory
|
||||
[app:apiv2app]
|
||||
paste.app_factory = glance.api.v2.router:API.factory
|
||||
|
||||
[app:apiv3app]
|
||||
paste.app_factory = glance.api.v3.router:API.factory
|
||||
|
||||
[filter:versionnegotiation]
|
||||
paste.filter_factory = glance.api.middleware.version_negotiation:VersionNegotiationFilter.factory
|
||||
|
||||
|
@ -24,4 +24,6 @@ def root_app_factory(loader, global_conf, **local_conf):
|
||||
del local_conf['/v1']
|
||||
if not CONF.enable_v2_api:
|
||||
del local_conf['/v2']
|
||||
if not CONF.enable_v3_api:
|
||||
del local_conf['/v3']
|
||||
return paste.urlmap.urlmap_factory(loader, global_conf, **local_conf)
|
||||
|
@ -86,6 +86,8 @@ class VersionNegotiationFilter(wsgi.Middleware):
|
||||
major_version = 1
|
||||
elif subject in ('v2', 'v2.0', 'v2.1', 'v2.2') and CONF.enable_v2_api:
|
||||
major_version = 2
|
||||
elif subject in ('v3', 'v3.0') and CONF.enable_v3_api:
|
||||
major_version = 3
|
||||
else:
|
||||
raise ValueError()
|
||||
|
||||
|
0
glance/api/v3/__init__.py
Normal file
0
glance/api/v3/__init__.py
Normal file
701
glance/api/v3/artifacts.py
Normal file
701
glance/api/v3/artifacts.py
Normal file
@ -0,0 +1,701 @@
|
||||
# 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 sys
|
||||
|
||||
import glance_store
|
||||
import jsonschema
|
||||
from oslo.config import cfg
|
||||
from oslo.serialization import jsonutils as json
|
||||
from oslo.utils import excutils
|
||||
import semantic_version
|
||||
import six
|
||||
import webob.exc
|
||||
|
||||
from glance.artifacts import gateway
|
||||
from glance.artifacts import Showlevel
|
||||
from glance.common.artifacts import loader
|
||||
from glance.common.artifacts import serialization
|
||||
from glance.common import exception
|
||||
from glance.common import jsonpatchvalidator
|
||||
from glance.common import utils
|
||||
from glance.common import wsgi
|
||||
import glance.db
|
||||
from glance import i18n
|
||||
from oslo_log import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
_LE = i18n._LE
|
||||
_ = i18n._
|
||||
|
||||
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
os.pardir,
|
||||
os.pardir))
|
||||
if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.import_group("profiler", "glance.common.wsgi")
|
||||
|
||||
|
||||
class ArtifactsController(object):
|
||||
def __init__(self, db_api=None, store_api=None, plugins=None):
|
||||
self.db_api = db_api or glance.db.get_api()
|
||||
self.store_api = store_api or glance_store
|
||||
self.plugins = plugins or loader.ArtifactsPluginLoader(
|
||||
'glance.artifacts.types')
|
||||
self.gateway = gateway.Gateway(self.db_api,
|
||||
self.store_api, self.plugins)
|
||||
|
||||
@staticmethod
|
||||
def _do_update_op(artifact, change):
|
||||
"""Call corresponding method of the updater proxy.
|
||||
|
||||
Here 'change' is a typical jsonpatch request dict:
|
||||
* 'path' - a json-pointer string;
|
||||
* 'op' - one of the allowed operation types;
|
||||
* 'value' - value to set (omitted when op = remove)
|
||||
"""
|
||||
update_op = getattr(artifact, change['op'])
|
||||
update_op(change['path'], change.get('value'))
|
||||
return artifact
|
||||
|
||||
@staticmethod
|
||||
def _get_artifact_with_dependencies(repo, art_id,
|
||||
type_name=None, type_version=None):
|
||||
"""Retrieves an artifact with dependencies from db by its id.
|
||||
|
||||
Show level is direct (only direct dependencies are shown).
|
||||
"""
|
||||
return repo.get(art_id, show_level=Showlevel.DIRECT,
|
||||
type_name=type_name, type_version=type_version)
|
||||
|
||||
def show(self, req, type_name, type_version,
|
||||
show_level=Showlevel.TRANSITIVE, **kwargs):
|
||||
"""Retrieves one artifact by id with its dependencies"""
|
||||
artifact_repo = self.gateway.get_artifact_repo(req.context)
|
||||
try:
|
||||
art_id = kwargs.get('id')
|
||||
artifact = artifact_repo.get(art_id, type_name=type_name,
|
||||
type_version=type_version,
|
||||
show_level=show_level)
|
||||
return artifact
|
||||
except exception.ArtifactNotFound as e:
|
||||
raise webob.exc.HTTPNotFound(explanation=e.msg)
|
||||
|
||||
def list(self, req, type_name, type_version, state, **kwargs):
|
||||
"""Retrieves a list of artifacts that match some params"""
|
||||
artifact_repo = self.gateway.get_artifact_repo(req.context)
|
||||
filters = kwargs.pop('filters', {})
|
||||
|
||||
filters.update(type_name={'value': type_name},
|
||||
state={'value': state})
|
||||
if type_version is not None:
|
||||
filters['type_version'] = {'value': type_version}
|
||||
if 'version' in filters and filters['version']['value'] == 'latest':
|
||||
if 'name' in filters:
|
||||
filters['version']['value'] = self._get_latest_version(
|
||||
req, filters['name']['value'], type_name, type_version)
|
||||
else:
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
'Filtering by version without specifying a name is not'
|
||||
' supported.')
|
||||
|
||||
return artifact_repo.list(filters=filters,
|
||||
show_level=Showlevel.BASIC,
|
||||
**kwargs)
|
||||
|
||||
def _get_latest_version(self, req, name, type_name, type_version=None,
|
||||
state='creating'):
|
||||
artifact_repo = self.gateway.get_artifact_repo(req.context)
|
||||
filters = dict(name={"value": name},
|
||||
type_name={"value": type_name},
|
||||
state={"value": state})
|
||||
if type_version is not None:
|
||||
filters["type_version"] = {"value": type_version}
|
||||
result = artifact_repo.list(filters=filters,
|
||||
show_level=Showlevel.NONE,
|
||||
sort_keys=['version'])
|
||||
if len(result):
|
||||
return result[0].version
|
||||
|
||||
msg = "No artifacts have been found"
|
||||
raise exception.ArtifactNotFound(message=msg)
|
||||
|
||||
@utils.mutating
|
||||
def create(self, req, artifact_type, artifact_data, **kwargs):
|
||||
try:
|
||||
artifact_factory = self.gateway.get_artifact_type_factory(
|
||||
req.context, artifact_type)
|
||||
new_artifact = artifact_factory.new_artifact(**artifact_data)
|
||||
artifact_repo = self.gateway.get_artifact_repo(req.context)
|
||||
artifact_repo.add(new_artifact)
|
||||
# retrieve artifact from db
|
||||
return self._get_artifact_with_dependencies(artifact_repo,
|
||||
new_artifact.id)
|
||||
except TypeError as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e)
|
||||
except exception.ArtifactNotFound as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
except exception.DuplicateLocation as dup:
|
||||
raise webob.exc.HTTPBadRequest(explanation=dup.msg)
|
||||
except exception.Forbidden as e:
|
||||
raise webob.exc.HTTPForbidden(explanation=e.msg)
|
||||
except exception.InvalidParameterValue as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
except exception.LimitExceeded as e:
|
||||
raise webob.exc.HTTPRequestEntityTooLarge(
|
||||
explanation=e.msg, request=req, content_type='text/plain')
|
||||
except exception.Duplicate as dupex:
|
||||
raise webob.exc.HTTPConflict(explanation=dupex.msg)
|
||||
except exception.Invalid as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
|
||||
@utils.mutating
|
||||
def update_property(self, req, id, type_name, type_version, path, data,
|
||||
**kwargs):
|
||||
"""Updates a single property specified by request url."""
|
||||
artifact_repo = self.gateway.get_artifact_repo(req.context)
|
||||
try:
|
||||
artifact = self._get_artifact_with_dependencies(artifact_repo, id,
|
||||
type_name,
|
||||
type_version)
|
||||
# use updater mixin to perform updates: generate update path
|
||||
if req.method == "PUT":
|
||||
# replaces existing value or creates a new one
|
||||
if getattr(artifact, kwargs["attr"]):
|
||||
artifact.replace(path=path, value=data)
|
||||
else:
|
||||
artifact.add(path=path, value=data)
|
||||
else:
|
||||
# append to an existing value or create a new one
|
||||
artifact.add(path=path, value=data)
|
||||
artifact_repo.save(artifact)
|
||||
return self._get_artifact_with_dependencies(artifact_repo, id)
|
||||
except (exception.InvalidArtifactPropertyValue,
|
||||
exception.ArtifactInvalidProperty,
|
||||
exception.InvalidJsonPatchPath) as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
except exception.NotFound as e:
|
||||
raise webob.exc.HTTPNotFound(explanation=e.msg)
|
||||
|
||||
@utils.mutating
|
||||
def update(self, req, id, type_name, type_version, changes, **kwargs):
|
||||
"""Performs an update via json patch request"""
|
||||
artifact_repo = self.gateway.get_artifact_repo(req.context)
|
||||
try:
|
||||
artifact = self._get_artifact_with_dependencies(artifact_repo, id,
|
||||
type_name,
|
||||
type_version)
|
||||
updated = artifact
|
||||
for change in changes:
|
||||
updated = self._do_update_op(updated, change)
|
||||
artifact_repo.save(updated)
|
||||
return self._get_artifact_with_dependencies(artifact_repo, id)
|
||||
except (exception.InvalidArtifactPropertyValue,
|
||||
exception.InvalidJsonPatchPath,
|
||||
exception.InvalidParameterValue) as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
except exception.NotFound as e:
|
||||
raise webob.exc.HTTPNotFound(explanation=e.msg)
|
||||
except exception.Forbidden as e:
|
||||
raise webob.exc.HTTPForbidden(explanation=e.msg)
|
||||
except exception.StorageQuotaFull as e:
|
||||
msg = (_("Denying attempt to upload artifact because it exceeds "
|
||||
"the quota: %s") % utils.exception_to_str(e))
|
||||
raise webob.exc.HTTPRequestEntityTooLarge(
|
||||
explanation=msg, request=req, content_type='text/plain')
|
||||
except exception.Invalid as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
except exception.LimitExceeded as e:
|
||||
raise webob.exc.HTTPRequestEntityTooLarge(
|
||||
explanation=e.msg, request=req, content_type='text/plain')
|
||||
|
||||
@utils.mutating
|
||||
def delete(self, req, id, type_name, type_version, **kwargs):
|
||||
artifact_repo = self.gateway.get_artifact_repo(req.context)
|
||||
try:
|
||||
artifact = self._get_artifact_with_dependencies(
|
||||
artifact_repo, id, type_name=type_name,
|
||||
type_version=type_version)
|
||||
artifact_repo.remove(artifact)
|
||||
except exception.Invalid as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
except exception.Forbidden as e:
|
||||
raise webob.exc.HTTPForbidden(explanation=e.msg)
|
||||
except exception.NotFound as e:
|
||||
msg = (_("Failed to find artifact %(artifact_id)s to delete") %
|
||||
{'artifact_id': id})
|
||||
raise webob.exc.HTTPNotFound(explanation=msg)
|
||||
except exception.InUseByStore as e:
|
||||
msg = (_("Artifact %s could not be deleted "
|
||||
"because it is in use: %s") % (id, e.msg)) # noqa
|
||||
raise webob.exc.HTTPConflict(explanation=msg)
|
||||
|
||||
@utils.mutating
|
||||
def publish(self, req, id, type_name, type_version, **kwargs):
|
||||
artifact_repo = self.gateway.get_artifact_repo(req.context)
|
||||
try:
|
||||
artifact = self._get_artifact_with_dependencies(
|
||||
artifact_repo, id, type_name=type_name,
|
||||
type_version=type_version)
|
||||
return artifact_repo.publish(artifact, context=req.context)
|
||||
except exception.Forbidden as e:
|
||||
raise webob.exc.HTTPForbidden(explanation=e.msg)
|
||||
except exception.NotFound as e:
|
||||
raise webob.exc.HTTPNotFound(explanation=e.msg)
|
||||
except exception.Invalid as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
|
||||
def _upload_list_property(self, method, blob_list, index, data, size):
|
||||
if method == 'PUT' and not index and len(blob_list) > 0:
|
||||
# PUT replaces everything, so PUT to non-empty collection is
|
||||
# forbidden
|
||||
raise webob.exc.HTTPMethodNotAllowed(
|
||||
explanation=_("Unable to PUT to non-empty collection"))
|
||||
if index is not None and index > len(blob_list):
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
explanation=_("Index is out of range"))
|
||||
if index is None:
|
||||
# both POST and PUT create a new blob list
|
||||
blob_list.append((data, size))
|
||||
elif method == 'POST':
|
||||
blob_list.insert(index, (data, size))
|
||||
else:
|
||||
blob_list[index] = (data, size)
|
||||
|
||||
@utils.mutating
|
||||
def upload(self, req, id, type_name, type_version, attr, size, data,
|
||||
index, **kwargs):
|
||||
artifact_repo = self.gateway.get_artifact_repo(req.context)
|
||||
try:
|
||||
artifact = self._get_artifact_with_dependencies(artifact_repo,
|
||||
id,
|
||||
type_name,
|
||||
type_version)
|
||||
blob_prop = artifact.metadata.attributes.blobs.get(attr)
|
||||
if blob_prop is None:
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
explanation=_("Not a blob property '%s'") % attr)
|
||||
if isinstance(blob_prop, list):
|
||||
blob_list = getattr(artifact, attr)
|
||||
self._upload_list_property(req.method, blob_list,
|
||||
index, data, size)
|
||||
else:
|
||||
if index is not None:
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
explanation=_("Not a list property '%s'") % attr)
|
||||
setattr(artifact, attr, (data, size))
|
||||
artifact_repo.save(artifact)
|
||||
return artifact
|
||||
|
||||
except exception.Forbidden as e:
|
||||
raise webob.exc.HTTPForbidden(explanation=e.msg)
|
||||
except exception.NotFound as e:
|
||||
raise webob.exc.HTTPNotFound(explanation=e.msg)
|
||||
except exception.Invalid as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
except Exception as e:
|
||||
# TODO(mfedosin): add more exception handlers here
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE("Failed to upload image data due to "
|
||||
"internal error"))
|
||||
self._restore(artifact_repo, artifact)
|
||||
|
||||
def download(self, req, id, type_name, type_version, attr, index,
|
||||
**kwargs):
|
||||
artifact_repo = self.gateway.get_artifact_repo(req.context)
|
||||
try:
|
||||
artifact = artifact_repo.get(id, type_name, type_version)
|
||||
if attr in artifact.metadata.attributes.blobs:
|
||||
if isinstance(artifact.metadata.attributes.blobs[attr], list):
|
||||
if index is None:
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
explanation=_("Index is required"))
|
||||
blob_list = getattr(artifact, attr)
|
||||
try:
|
||||
return blob_list[index]
|
||||
except IndexError as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.message)
|
||||
else:
|
||||
if index is not None:
|
||||
raise webob.exc.HTTPBadRequest(_("Not a list "
|
||||
"property"))
|
||||
return getattr(artifact, attr)
|
||||
else:
|
||||
message = _("Not a downloadable entity")
|
||||
raise webob.exc.HTTPBadRequest(explanation=message)
|
||||
except exception.Forbidden as e:
|
||||
raise webob.exc.HTTPForbidden(explanation=e.msg)
|
||||
except exception.NotFound as e:
|
||||
raise webob.exc.HTTPNotFound(explanation=e.msg)
|
||||
except exception.Invalid as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
|
||||
def _restore(self, artifact_repo, artifact):
|
||||
"""Restore the artifact to queued status.
|
||||
|
||||
:param artifact_repo: The instance of ArtifactRepo
|
||||
:param artifact: The artifact will be restored
|
||||
"""
|
||||
try:
|
||||
if artifact_repo and artifact:
|
||||
artifact.state = 'creating'
|
||||
artifact_repo.save(artifact)
|
||||
except Exception as e:
|
||||
msg = (_LE("Unable to restore artifact %(artifact_id)s: %(e)s") %
|
||||
{'artifact_id': artifact.id,
|
||||
'e': utils.exception_to_str(e)})
|
||||
LOG.exception(msg)
|
||||
|
||||
|
||||
class RequestDeserializer(wsgi.JSONRequestDeserializer,
|
||||
jsonpatchvalidator.JsonPatchValidatorMixin):
|
||||
_available_sort_keys = ('name', 'status', 'container_format',
|
||||
'disk_format', 'size', 'id', 'created_at',
|
||||
'updated_at', 'version')
|
||||
_default_sort_dir = 'desc'
|
||||
|
||||
_max_limit_number = 1000
|
||||
|
||||
def __init__(self, schema=None, plugins=None):
|
||||
super(RequestDeserializer, self).__init__(
|
||||
methods_allowed=["replace", "remove", "add"])
|
||||
self.plugins = plugins or loader.ArtifactsPluginLoader(
|
||||
'glance.artifacts.types')
|
||||
|
||||
def _validate_show_level(self, show_level):
|
||||
try:
|
||||
return Showlevel.from_str(show_level.strip().lower())
|
||||
except exception.ArtifactUnsupportedShowLevel as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.message)
|
||||
|
||||
def show(self, req):
|
||||
res = self._process_type_from_request(req, True)
|
||||
params = req.params.copy()
|
||||
show_level = params.pop('show_level', None)
|
||||
if show_level is not None:
|
||||
res['show_level'] = self._validate_show_level(show_level)
|
||||
return res
|
||||
|
||||
def _get_request_body(self, req):
|
||||
output = super(RequestDeserializer, self).default(req)
|
||||
if 'body' not in output:
|
||||
msg = _('Body expected in request.')
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
return output['body']
|
||||
|
||||
def validate_body(self, request):
|
||||
try:
|
||||
body = self._get_request_body(request)
|
||||
return super(RequestDeserializer, self).validate_body(body)
|
||||
except exception.JsonPatchException as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e)
|
||||
|
||||
def default(self, request):
|
||||
return self._process_type_from_request(request)
|
||||
|
||||
def _check_type_version(self, type_version):
|
||||
try:
|
||||
semantic_version.Version(type_version, partial=True)
|
||||
except ValueError as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e)
|
||||
|
||||
def _process_type_from_request(self, req,
|
||||
allow_implicit_version=False):
|
||||
try:
|
||||
type_name = req.urlvars.get('type_name')
|
||||
type_version = req.urlvars.get('type_version')
|
||||
if type_version is not None:
|
||||
self._check_type_version(type_version)
|
||||
# Even if the type_version is not specified and
|
||||
# 'allow_implicit_version' is False, this call is still needed to
|
||||
# ensure that at least one version of this type exists.
|
||||
artifact_type = self.plugins.get_class_by_endpoint(type_name,
|
||||
type_version)
|
||||
res = {
|
||||
'type_name': artifact_type.metadata.type_name,
|
||||
'type_version':
|
||||
artifact_type.metadata.type_version
|
||||
if type_version is not None else None
|
||||
}
|
||||
if allow_implicit_version:
|
||||
res['artifact_type'] = artifact_type
|
||||
return res
|
||||
|
||||
except exception.ArtifactPluginNotFound as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||
|
||||
def create(self, req):
|
||||
res = self._process_type_from_request(req, True)
|
||||
res["artifact_data"] = self._get_request_body(req)
|
||||
return res
|
||||
|
||||
def update(self, req):
|
||||
res = self._process_type_from_request(req)
|
||||
res["changes"] = self.validate_body(req)
|
||||
return res
|
||||
|
||||
def update_property(self, req):
|
||||
"""Data is expected in form {'data': ...}"""
|
||||
res = self._process_type_from_request(req)
|
||||
data_schema = {
|
||||
"type": "object",
|
||||
"properties": {"data": {}},
|
||||
"required": ["data"],
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"}
|
||||
try:
|
||||
json_body = json.loads(req.body)
|
||||
jsonschema.validate(json_body, data_schema)
|
||||
# TODO(ivasilevskaya):
|
||||
# by now the deepest nesting level == 1 (ex. some_list/3),
|
||||
# has to be fixed for dict properties
|
||||
attr = req.urlvars["attr"]
|
||||
path_left = req.urlvars["path_left"]
|
||||
path = (attr if not path_left
|
||||
else "%(attr)s/%(path_left)s" % {'attr': attr,
|
||||
'path_left': path_left})
|
||||
res.update(data=json_body["data"], path=path)
|
||||
return res
|
||||
except (ValueError, jsonschema.ValidationError) as e:
|
||||
msg = _("Invalid json body: %s") % e.message
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
def upload(self, req):
|
||||
res = self._process_type_from_request(req)
|
||||
index = req.urlvars.get('path_left')
|
||||
try:
|
||||
# for blobs only one level of indexing is supported
|
||||
# (ex. bloblist/0)
|
||||
if index is not None:
|
||||
index = int(index)
|
||||
except ValueError:
|
||||
msg = _("Only list indexes are allowed for blob lists")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
artifact_size = req.content_length or None
|
||||
res.update(size=artifact_size, data=req.body_file,
|
||||
index=index)
|
||||
return res
|
||||
|
||||
def download(self, req):
|
||||
res = self._process_type_from_request(req)
|
||||
index = req.urlvars.get('index')
|
||||
if index is not None:
|
||||
index = int(index)
|
||||
res.update(index=index)
|
||||
return res
|
||||
|
||||
def _validate_limit(self, limit):
|
||||
if limit is None:
|
||||
return self._max_limit_number
|
||||
try:
|
||||
limit = int(limit)
|
||||
except ValueError:
|
||||
msg = _("Limit param must be an integer")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
if limit < 0:
|
||||
msg = _("Limit param must be positive")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
if limit > self._max_limit_number:
|
||||
msg = _("Limit param"
|
||||
" must not be higher than %d") % self._max_limit_number
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
return limit
|
||||
|
||||
def _validate_sort_key(self, sort_key, artifact_type, type_version=None):
|
||||
if sort_key in self._available_sort_keys:
|
||||
return sort_key, None
|
||||
elif type_version is None:
|
||||
msg = _('Invalid sort key: %(sort_key)s. '
|
||||
'If type version is not set it must be one of'
|
||||
' the following: %(available)s.') % \
|
||||
{'sort_key': sort_key,
|
||||
'available': ', '.join(self._available_sort_keys)}
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
prop_type = artifact_type.metadata.attributes.all.get(sort_key)
|
||||
if prop_type is None or prop_type.DB_TYPE not in ['string',
|
||||
'numeric',
|
||||
'int',
|
||||
'bool']:
|
||||
msg = _('Invalid sort key: %(sort_key)s. '
|
||||
'You cannot sort by this property') % \
|
||||
{'sort_key': sort_key}
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
return sort_key, prop_type.DB_TYPE
|
||||
|
||||
def _validate_sort_dir(self, sort_dir):
|
||||
if sort_dir not in ['asc', 'desc']:
|
||||
msg = _('Invalid sort direction: %s') % sort_dir
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
return sort_dir
|
||||
|
||||
def _get_sorting_params(self, params, artifact_type, type_version=None):
|
||||
|
||||
sort_keys = []
|
||||
sort_dirs = []
|
||||
|
||||
if 'sort' in params:
|
||||
for sort_param in params.pop('sort').strip().split(','):
|
||||
key, _sep, dir = sort_param.partition(':')
|
||||
if not dir:
|
||||
dir = self._default_sort_dir
|
||||
sort_keys.append(self._validate_sort_key(key.strip(),
|
||||
artifact_type,
|
||||
type_version))
|
||||
sort_dirs.append(self._validate_sort_dir(dir.strip()))
|
||||
|
||||
if not sort_keys:
|
||||
sort_keys = [('created_at', None)]
|
||||
if not sort_dirs:
|
||||
sort_dirs = [self._default_sort_dir]
|
||||
|
||||
return sort_keys, sort_dirs
|
||||
|
||||
def _bring_to_type(self, type_name, value):
|
||||
mapper = {'int': int,
|
||||
'string': str,
|
||||
'text': str,
|
||||
'bool': bool,
|
||||
'numeric': float}
|
||||
return mapper[type_name](value)
|
||||
|
||||
def _get_filters(self, artifact_type, params):
|
||||
filters = dict()
|
||||
for filter, value in params.items():
|
||||
value = value.strip()
|
||||
prop_type = artifact_type.metadata.attributes.all.get(filter)
|
||||
if prop_type.DB_TYPE is not None:
|
||||
str_type = prop_type.DB_TYPE
|
||||
elif isinstance(prop_type, list):
|
||||
if not isinstance(prop_type.item_type, list):
|
||||
str_type = prop_type.item_type.DB_TYPE
|
||||
else:
|
||||
raise webob.exc.HTTPBadRequest('Filtering by tuple-like'
|
||||
' fields is not supported')
|
||||
elif isinstance(prop_type, dict):
|
||||
filters['name'] = filter + '.' + value
|
||||
continue
|
||||
else:
|
||||
raise webob.exc.HTTPBadRequest('Filtering by this property '
|
||||
'is not supported')
|
||||
substr1, _sep, substr2 = value.partition(':')
|
||||
if not _sep:
|
||||
op = 'IN' if isinstance(prop_type, list) else 'EQ'
|
||||
filters[filter] = dict(operator=op,
|
||||
value=self._bring_to_type(str_type,
|
||||
substr1),
|
||||
type=str_type)
|
||||
else:
|
||||
op = substr1.strip().upper()
|
||||
filters[filter] = dict(operator=op,
|
||||
value=self._bring_to_type(str_type,
|
||||
substr2),
|
||||
type=str_type)
|
||||
return filters
|
||||
|
||||
def list(self, req):
|
||||
res = self._process_type_from_request(req, True)
|
||||
params = req.params.copy()
|
||||
show_level = params.pop('show_level', None)
|
||||
if show_level is not None:
|
||||
res['show_level'] = self._validate_show_level(show_level.strip())
|
||||
|
||||
limit = params.pop('limit', None)
|
||||
marker = params.pop('marker', None)
|
||||
|
||||
tags = []
|
||||
while 'tag' in params:
|
||||
tags.append(params.pop('tag').strip())
|
||||
|
||||
query_params = dict()
|
||||
|
||||
query_params['sort_keys'], query_params['sort_dirs'] = \
|
||||
self._get_sorting_params(params, res['artifact_type'],
|
||||
res['type_version'])
|
||||
|
||||
if marker is not None:
|
||||
query_params['marker'] = marker
|
||||
|
||||
query_params['limit'] = self._validate_limit(limit)
|
||||
|
||||
if tags:
|
||||
query_params['filters']['tags'] = {'value': tags}
|
||||
|
||||
query_params['filters'] = self._get_filters(res['artifact_type'],
|
||||
params)
|
||||
|
||||
query_params['type_name'] = res['artifact_type'].metadata.type_name
|
||||
|
||||
return query_params
|
||||
|
||||
|
||||
class ResponseSerializer(wsgi.JSONResponseSerializer):
|
||||
# TODO(ivasilevskaya): ideally this should be autogenerated/loaded
|
||||
ARTIFACTS_ENDPOINT = '/v3/artifacts'
|
||||
fields = ['id', 'name', 'version', 'type_name', 'type_version',
|
||||
'visibility', 'state', 'owner', 'scope', 'created_at',
|
||||
'updated_at', 'tags', 'dependencies', 'blobs', 'properties']
|
||||
|
||||
def __init__(self, schema=None):
|
||||
super(ResponseSerializer, self).__init__()
|
||||
|
||||
def default(self, response, res):
|
||||
artifact = serialization.serialize_for_client(
|
||||
res, show_level=Showlevel.DIRECT)
|
||||
body = json.dumps(artifact, ensure_ascii=False)
|
||||
response.unicode_body = six.text_type(body)
|
||||
response.content_type = 'application/json'
|
||||
|
||||
def create(self, response, artifact):
|
||||
response.status_int = 201
|
||||
self.default(response, artifact)
|
||||
response.location = (
|
||||
'%(root_url)s/%(type_name)s/v%(type_version)s/%(id)s' % dict(
|
||||
root_url=ResponseSerializer.ARTIFACTS_ENDPOINT,
|
||||
type_name=artifact.metadata.endpoint,
|
||||
type_version=artifact.metadata.type_version,
|
||||
id=artifact.id))
|
||||
|
||||
def list(self, response, res):
|
||||
artifacts_list = [
|
||||
serialization.serialize_for_client(a, show_level=Showlevel.NONE)
|
||||
for a in res]
|
||||
body = json.dumps(artifacts_list, ensure_ascii=False)
|
||||
response.unicode_body = six.text_type(body)
|
||||
response.content_type = 'application/json'
|
||||
|
||||
def delete(self, response, result):
|
||||
response.status_int = 204
|
||||
|
||||
def download(self, response, blob):
|
||||
response.headers['Content-Type'] = 'application/octet-stream'
|
||||
response.app_iter = iter(blob.data_stream)
|
||||
if blob.checksum:
|
||||
response.headers['Content-MD5'] = blob.checksum
|
||||
response.headers['Content-Length'] = str(blob.size)
|
||||
|
||||
|
||||
def create_resource():
|
||||
"""Images resource factory method"""
|
||||
plugins = loader.ArtifactsPluginLoader('glance.artifacts.types')
|
||||
deserializer = RequestDeserializer(plugins=plugins)
|
||||
serializer = ResponseSerializer()
|
||||
controller = ArtifactsController(plugins=plugins)
|
||||
return wsgi.Resource(controller, deserializer, serializer)
|
87
glance/api/v3/router.py
Normal file
87
glance/api/v3/router.py
Normal file
@ -0,0 +1,87 @@
|
||||
# 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.
|
||||
|
||||
from glance.api.v3 import artifacts
|
||||
from glance.common import wsgi
|
||||
|
||||
|
||||
UUID_REGEX = (
|
||||
R'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}')
|
||||
|
||||
|
||||
class API(wsgi.Router):
|
||||
|
||||
def _get_artifacts_resource(self):
|
||||
if not self.artifacts_resource:
|
||||
self.artifacts_resource = artifacts.create_resource()
|
||||
return self.artifacts_resource
|
||||
|
||||
def __init__(self, mapper):
|
||||
self.artifacts_resource = None
|
||||
artifacts_resource = self._get_artifacts_resource()
|
||||
|
||||
def _check_json_content_type(environ, result):
|
||||
return "application/json" in environ["CONTENT_TYPE"]
|
||||
|
||||
def _check_octet_stream_content_type(environ, result):
|
||||
return "application/octet-stream" in environ["CONTENT_TYPE"]
|
||||
|
||||
def connect_routes(m, read_only):
|
||||
with m.submapper(resource_name="artifact_operations",
|
||||
path_prefix="/{id}",
|
||||
requirements={'id': UUID_REGEX}) as art:
|
||||
art.show()
|
||||
if not read_only:
|
||||
art.delete()
|
||||
art.action('update', method='PATCH')
|
||||
art.link('publish', method='POST')
|
||||
|
||||
def connect_attr_action(attr):
|
||||
if not read_only:
|
||||
attr.action("upload", conditions={
|
||||
'method': ["POST", "PUT"],
|
||||
'function': _check_octet_stream_content_type})
|
||||
attr.action("update_property",
|
||||
conditions={
|
||||
'method': ["POST", "PUT"],
|
||||
'function': _check_json_content_type})
|
||||
attr.link("download", method="GET")
|
||||
|
||||
attr_map = art.submapper(resource_name="attr_operations",
|
||||
path_prefix="/{attr}", path_left=None)
|
||||
attr_items = art.submapper(
|
||||
resource_name="attr_item_ops",
|
||||
path_prefix="/{attr}/{path_left:.*}")
|
||||
connect_attr_action(attr_map)
|
||||
connect_attr_action(attr_items)
|
||||
|
||||
m.connect("", action='list', conditions={'method': 'GET'},
|
||||
state='active')
|
||||
m.connect("/drafts", action='list', conditions={'method': 'GET'},
|
||||
state='creating')
|
||||
if not read_only:
|
||||
m.connect("/drafts", action='create',
|
||||
conditions={'method': 'POST'})
|
||||
|
||||
versioned = mapper.submapper(path_prefix='/artifacts/{type_name}/'
|
||||
'v{type_version}',
|
||||
controller=artifacts_resource)
|
||||
|
||||
non_versioned = mapper.submapper(path_prefix='/artifacts/{type_name}',
|
||||
type_version=None,
|
||||
controller=artifacts_resource)
|
||||
connect_routes(versioned, False)
|
||||
connect_routes(non_versioned, True)
|
||||
|
||||
super(API, self).__init__(mapper)
|
@ -56,6 +56,9 @@ class Controller(object):
|
||||
}
|
||||
|
||||
version_objs = []
|
||||
if CONF.enable_v3_api:
|
||||
version_objs.append(
|
||||
build_version_object(3.0, 'v3', 'EXPERIMENTAL'))
|
||||
if CONF.enable_v2_api:
|
||||
version_objs.extend([
|
||||
build_version_object(2.3, 'v2', 'CURRENT'),
|
||||
|
@ -12,8 +12,11 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
|
||||
import six
|
||||
|
||||
from glance import artifacts as ga
|
||||
from glance.common.artifacts import declarative
|
||||
from glance.common.artifacts import definitions
|
||||
from glance.common import exception
|
||||
@ -262,3 +265,66 @@ def deserialize_from_db(db_dict, plugins):
|
||||
artifact_properties, plugins)
|
||||
|
||||
return artifact_type(**artifact_properties)
|
||||
|
||||
|
||||
def _process_blobs_for_client(artifact, result):
|
||||
"""Processes artifact's blobs: adds download links and pretty-printed data.
|
||||
|
||||
The result is stored in 'result' dict.
|
||||
"""
|
||||
def build_uri(blob_attr, position=None):
|
||||
"""A helper func to build download uri"""
|
||||
template = "/artifacts/%(type)s/v%(version)s/%(id)s/%(prop)s/download"
|
||||
format_dict = {
|
||||
"type": artifact.metadata.endpoint,
|
||||
"version": artifact.type_version,
|
||||
"id": artifact.id,
|
||||
"prop": blob_attr.name
|
||||
}
|
||||
if position is not None:
|
||||
template = "/artifacts/%(type)s/v%(version)s/" \
|
||||
"%(id)s/%(prop)s/%(position)s/download"
|
||||
format_dict["position"] = position
|
||||
|
||||
return template % format_dict
|
||||
|
||||
for blob_attr in artifact.metadata.attributes.blobs.values():
|
||||
value = blob_attr.get_value(artifact)
|
||||
if value is None:
|
||||
result[blob_attr.name] = None
|
||||
elif isinstance(value, collections.Iterable):
|
||||
res_list = []
|
||||
for pos, blob in enumerate(value):
|
||||
blob_dict = blob.to_dict()
|
||||
blob_dict["download_link"] = build_uri(blob_attr, pos)
|
||||
res_list.append(blob_dict)
|
||||
result[blob_attr.name] = res_list
|
||||
else:
|
||||
result[blob_attr.name] = value.to_dict()
|
||||
result[blob_attr.name]["download_link"] = build_uri(blob_attr)
|
||||
|
||||
|
||||
def serialize_for_client(artifact, show_level=ga.Showlevel.NONE):
|
||||
# use serialize_for_db and modify some fields
|
||||
# (like properties, show only value, not type)
|
||||
result = {}
|
||||
|
||||
for prop in artifact.metadata.attributes.properties.values():
|
||||
result[prop.name] = prop.get_value(artifact)
|
||||
|
||||
if show_level > ga.Showlevel.NONE:
|
||||
for dep in artifact.metadata.attributes.dependencies.values():
|
||||
inner_show_level = (ga.Showlevel.DIRECT
|
||||
if show_level == ga.Showlevel.DIRECT
|
||||
else ga.Showlevel.NONE)
|
||||
value = dep.get_value(artifact)
|
||||
if value is None:
|
||||
result[dep.name] = None
|
||||
elif isinstance(value, list):
|
||||
result[dep.name] = [serialize_for_client(v, inner_show_level)
|
||||
for v in value]
|
||||
else:
|
||||
result[dep.name] = serialize_for_client(value,
|
||||
inner_show_level)
|
||||
_process_blobs_for_client(artifact, result)
|
||||
return result
|
||||
|
@ -150,6 +150,8 @@ common_opts = [
|
||||
help=_("Deploy the v1 OpenStack Images API.")),
|
||||
cfg.BoolOpt('enable_v2_api', default=True,
|
||||
help=_("Deploy the v2 OpenStack Images API.")),
|
||||
cfg.BoolOpt('enable_v3_api', default=True,
|
||||
help=_("Deploy the v3 OpenStack Objects API.")),
|
||||
cfg.BoolOpt('enable_v1_registry', default=True,
|
||||
help=_("Deploy the v1 OpenStack Registry API.")),
|
||||
cfg.BoolOpt('enable_v2_registry', default=True,
|
||||
|
@ -75,6 +75,7 @@ class Server(object):
|
||||
self.property_protection_file = ''
|
||||
self.enable_v1_api = True
|
||||
self.enable_v2_api = True
|
||||
self.enable_v3_api = True
|
||||
self.enable_v1_registry = True
|
||||
self.enable_v2_registry = True
|
||||
self.needs_database = False
|
||||
@ -340,6 +341,7 @@ show_multiple_locations = %(show_multiple_locations)s
|
||||
user_storage_quota = %(user_storage_quota)s
|
||||
enable_v1_api = %(enable_v1_api)s
|
||||
enable_v2_api = %(enable_v2_api)s
|
||||
enable_v3_api = %(enable_v3_api)s
|
||||
lock_path = %(lock_path)s
|
||||
property_protection_file = %(property_protection_file)s
|
||||
property_protection_rule_format = %(property_protection_rule_format)s
|
||||
@ -386,6 +388,7 @@ paste.composite_factory = glance.api:root_app_factory
|
||||
/: apiversions
|
||||
/v1: apiv1app
|
||||
/v2: apiv2app
|
||||
/v3: apiv3app
|
||||
|
||||
[app:apiversions]
|
||||
paste.app_factory = glance.api.versions:create_resource
|
||||
@ -396,6 +399,9 @@ paste.app_factory = glance.api.v1.router:API.factory
|
||||
[app:apiv2app]
|
||||
paste.app_factory = glance.api.v2.router:API.factory
|
||||
|
||||
[app:apiv3app]
|
||||
paste.app_factory = glance.api.v3.router:API.factory
|
||||
|
||||
[filter:versionnegotiation]
|
||||
paste.filter_factory =
|
||||
glance.api.middleware.version_negotiation:VersionNegotiationFilter.factory
|
||||
|
0
glance/tests/functional/artifacts/__init__.py
Normal file
0
glance/tests/functional/artifacts/__init__.py
Normal file
1116
glance/tests/functional/artifacts/test_artifacts.py
Normal file
1116
glance/tests/functional/artifacts/test_artifacts.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -31,6 +31,11 @@ class TestApiVersions(functional.FunctionalTest):
|
||||
|
||||
url = 'http://127.0.0.1:%d/v%%s/' % self.api_port
|
||||
versions = {'versions': [
|
||||
{
|
||||
'status': 'EXPERIMENTAL',
|
||||
'id': 'v3.0',
|
||||
'links': [{'href': url % '3', "rel": "self"}],
|
||||
},
|
||||
{
|
||||
'id': 'v2.3',
|
||||
'status': 'CURRENT',
|
||||
@ -78,6 +83,11 @@ class TestApiVersions(functional.FunctionalTest):
|
||||
|
||||
url = 'http://127.0.0.1:%d/v%%s/' % self.api_port
|
||||
versions = {'versions': [
|
||||
{
|
||||
'status': 'EXPERIMENTAL',
|
||||
'id': 'v3.0',
|
||||
'links': [{'href': url % '3', "rel": "self"}],
|
||||
},
|
||||
{
|
||||
'id': 'v2.3',
|
||||
'status': 'CURRENT',
|
||||
@ -111,6 +121,7 @@ class TestApiVersions(functional.FunctionalTest):
|
||||
def test_v1_api_configuration(self):
|
||||
self.api_server.enable_v1_api = True
|
||||
self.api_server.enable_v2_api = False
|
||||
self.api_server.enable_v3_api = False
|
||||
self.start_servers(**self.__dict__.copy())
|
||||
|
||||
url = 'http://127.0.0.1:%d/v%%s/' % self.api_port
|
||||
@ -143,6 +154,11 @@ class TestApiPaths(functional.FunctionalTest):
|
||||
|
||||
url = 'http://127.0.0.1:%d/v%%s/' % self.api_port
|
||||
versions = {'versions': [
|
||||
{
|
||||
'status': 'EXPERIMENTAL',
|
||||
'id': 'v3.0',
|
||||
'links': [{'href': url % '3', "rel": "self"}],
|
||||
},
|
||||
{
|
||||
'id': 'v2.3',
|
||||
'status': 'CURRENT',
|
||||
|
@ -78,6 +78,7 @@ class OptsTestCase(utils.BaseTestCase):
|
||||
'user_storage_quota',
|
||||
'enable_v1_api',
|
||||
'enable_v2_api',
|
||||
'enable_v3_api',
|
||||
'enable_v1_registry',
|
||||
'enable_v2_registry',
|
||||
'pydev_worker_debug_host',
|
||||
@ -166,6 +167,7 @@ class OptsTestCase(utils.BaseTestCase):
|
||||
'user_storage_quota',
|
||||
'enable_v1_api',
|
||||
'enable_v2_api',
|
||||
'enable_v3_api',
|
||||
'enable_v1_registry',
|
||||
'enable_v2_registry',
|
||||
'pydev_worker_debug_host',
|
||||
@ -211,6 +213,7 @@ class OptsTestCase(utils.BaseTestCase):
|
||||
'user_storage_quota',
|
||||
'enable_v1_api',
|
||||
'enable_v2_api',
|
||||
'enable_v3_api',
|
||||
'enable_v1_registry',
|
||||
'enable_v2_registry',
|
||||
'pydev_worker_debug_host',
|
||||
@ -260,6 +263,7 @@ class OptsTestCase(utils.BaseTestCase):
|
||||
'user_storage_quota',
|
||||
'enable_v1_api',
|
||||
'enable_v2_api',
|
||||
'enable_v3_api',
|
||||
'enable_v1_registry',
|
||||
'enable_v2_registry',
|
||||
'pydev_worker_debug_host',
|
||||
|
@ -34,6 +34,12 @@ class VersionsTest(base.IsolatedUnitTest):
|
||||
self.assertEqual('application/json', res.content_type)
|
||||
results = jsonutils.loads(res.body)['versions']
|
||||
expected = [
|
||||
{
|
||||
'status': 'EXPERIMENTAL',
|
||||
'id': 'v3.0',
|
||||
'links': [{'href': 'http://127.0.0.1:9292/v3/',
|
||||
'rel': 'self'}],
|
||||
},
|
||||
{
|
||||
'id': 'v2.3',
|
||||
'status': 'CURRENT',
|
||||
@ -83,6 +89,12 @@ class VersionsTest(base.IsolatedUnitTest):
|
||||
self.assertEqual('application/json', res.content_type)
|
||||
results = jsonutils.loads(res.body)['versions']
|
||||
expected = [
|
||||
{
|
||||
'status': 'EXPERIMENTAL',
|
||||
'id': 'v3.0',
|
||||
'links': [{'href': 'https://example.com:9292/v3/',
|
||||
'rel': 'self'}],
|
||||
},
|
||||
{
|
||||
'id': 'v2.3',
|
||||
'status': 'CURRENT',
|
||||
@ -170,12 +182,22 @@ class VersionNegotiationTest(base.IsolatedUnitTest):
|
||||
self.middleware.process_request(request)
|
||||
self.assertEqual('/v2/images', request.path_info)
|
||||
|
||||
def test_request_url_v3(self):
|
||||
request = webob.Request.blank('/v3/artifacts')
|
||||
self.middleware.process_request(request)
|
||||
self.assertEqual('/v3/artifacts', request.path_info)
|
||||
|
||||
def test_request_url_v3_0(self):
|
||||
request = webob.Request.blank('/v3.0/artifacts')
|
||||
self.middleware.process_request(request)
|
||||
self.assertEqual('/v3/artifacts', request.path_info)
|
||||
|
||||
def test_request_url_v2_3_unsupported(self):
|
||||
request = webob.Request.blank('/v2.3/images')
|
||||
resp = self.middleware.process_request(request)
|
||||
self.assertIsInstance(resp, versions.Controller)
|
||||
|
||||
def test_request_url_v3_unsupported(self):
|
||||
request = webob.Request.blank('/v3/images')
|
||||
def test_request_url_v4_unsupported(self):
|
||||
request = webob.Request.blank('/v4/images')
|
||||
resp = self.middleware.process_request(request)
|
||||
self.assertIsInstance(resp, versions.Controller)
|
||||
|
@ -20,6 +20,7 @@ classifier =
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
glance-api = glance.cmd.api:main
|
||||
glance-artifacts = glance.cmd.artifacts:main
|
||||
glance-cache-prefetcher = glance.cmd.cache_prefetcher:main
|
||||
glance-cache-pruner = glance.cmd.cache_pruner:main
|
||||
glance-cache-manage = glance.cmd.cache_manage:main
|
||||
@ -47,6 +48,8 @@ glance.database.metadata_backend =
|
||||
glance.search.index_backend =
|
||||
image = glance.search.plugins.images:ImageIndex
|
||||
metadef = glance.search.plugins.metadefs:MetadefIndex
|
||||
glance.artifacts.types =
|
||||
MyArtifact = glance.contrib.plugins.artifacts_sample:MY_ARTIFACT
|
||||
|
||||
glance.flows =
|
||||
import = glance.async.flows.base_import:get_flow
|
||||
|
Loading…
Reference in New Issue
Block a user