Cleanup remove api v1 and registry code
Change-Id: I86a3cbf4374bc2b083ccd86f75b88490b305eaab
This commit is contained in:
parent
3f6e80cdc9
commit
3068096199
@ -37,15 +37,11 @@ pipeline = cors healthcheck http_proxy_to_wsgi versionnegotiation osprofiler con
|
||||
[composite:rootapp]
|
||||
paste.composite_factory = glance.api:root_app_factory
|
||||
/: apiversions
|
||||
/v1: apiv1app
|
||||
/v2: apiv2app
|
||||
|
||||
[app:apiversions]
|
||||
paste.app_factory = glance.api.versions:create_resource
|
||||
|
||||
[app:apiv1app]
|
||||
paste.app_factory = glance.api.v1.router:API.factory
|
||||
|
||||
[app:apiv2app]
|
||||
paste.app_factory = glance.api.v2.router:API.factory
|
||||
|
||||
|
@ -1,129 +0,0 @@
|
||||
# Copyright 2011 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.
|
||||
|
||||
"""
|
||||
Controller for Image Cache Management API
|
||||
"""
|
||||
|
||||
from oslo_log import log as logging
|
||||
import webob.exc
|
||||
|
||||
from glance.api import policy
|
||||
from glance.api.v1 import controller
|
||||
from glance.common import exception
|
||||
from glance.common import wsgi
|
||||
from glance import image_cache
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Controller(controller.BaseController):
|
||||
"""
|
||||
A controller for managing cached images.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.cache = image_cache.ImageCache()
|
||||
self.policy = policy.Enforcer()
|
||||
|
||||
def _enforce(self, req):
|
||||
"""Authorize request against 'manage_image_cache' policy"""
|
||||
try:
|
||||
self.policy.enforce(req.context, 'manage_image_cache', {})
|
||||
except exception.Forbidden:
|
||||
LOG.debug("User not permitted to manage the image cache")
|
||||
raise webob.exc.HTTPForbidden()
|
||||
|
||||
def get_cached_images(self, req):
|
||||
"""
|
||||
GET /cached_images
|
||||
|
||||
Returns a mapping of records about cached images.
|
||||
"""
|
||||
self._enforce(req)
|
||||
images = self.cache.get_cached_images()
|
||||
return dict(cached_images=images)
|
||||
|
||||
def delete_cached_image(self, req, image_id):
|
||||
"""
|
||||
DELETE /cached_images/<IMAGE_ID>
|
||||
|
||||
Removes an image from the cache.
|
||||
"""
|
||||
self._enforce(req)
|
||||
self.cache.delete_cached_image(image_id)
|
||||
|
||||
def delete_cached_images(self, req):
|
||||
"""
|
||||
DELETE /cached_images - Clear all active cached images
|
||||
|
||||
Removes all images from the cache.
|
||||
"""
|
||||
self._enforce(req)
|
||||
return dict(num_deleted=self.cache.delete_all_cached_images())
|
||||
|
||||
def get_queued_images(self, req):
|
||||
"""
|
||||
GET /queued_images
|
||||
|
||||
Returns a mapping of records about queued images.
|
||||
"""
|
||||
self._enforce(req)
|
||||
images = self.cache.get_queued_images()
|
||||
return dict(queued_images=images)
|
||||
|
||||
def queue_image(self, req, image_id):
|
||||
"""
|
||||
PUT /queued_images/<IMAGE_ID>
|
||||
|
||||
Queues an image for caching. We do not check to see if
|
||||
the image is in the registry here. That is done by the
|
||||
prefetcher...
|
||||
"""
|
||||
self._enforce(req)
|
||||
self.cache.queue_image(image_id)
|
||||
|
||||
def delete_queued_image(self, req, image_id):
|
||||
"""
|
||||
DELETE /queued_images/<IMAGE_ID>
|
||||
|
||||
Removes an image from the cache.
|
||||
"""
|
||||
self._enforce(req)
|
||||
self.cache.delete_queued_image(image_id)
|
||||
|
||||
def delete_queued_images(self, req):
|
||||
"""
|
||||
DELETE /queued_images - Clear all active queued images
|
||||
|
||||
Removes all images from the cache.
|
||||
"""
|
||||
self._enforce(req)
|
||||
return dict(num_deleted=self.cache.delete_all_queued_images())
|
||||
|
||||
|
||||
class CachedImageDeserializer(wsgi.JSONRequestDeserializer):
|
||||
pass
|
||||
|
||||
|
||||
class CachedImageSerializer(wsgi.JSONResponseSerializer):
|
||||
pass
|
||||
|
||||
|
||||
def create_resource():
|
||||
"""Cached Images resource factory method"""
|
||||
deserializer = CachedImageDeserializer()
|
||||
serializer = CachedImageSerializer()
|
||||
return wsgi.Resource(Controller(), deserializer, serializer)
|
@ -1,26 +0,0 @@
|
||||
# Copyright 2011 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.
|
||||
|
||||
SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
|
||||
'min_ram', 'min_disk', 'size_min', 'size_max',
|
||||
'is_public', 'changes-since', 'protected']
|
||||
|
||||
SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir')
|
||||
|
||||
# Metadata which only an admin can change once the image is active
|
||||
ACTIVE_IMMUTABLE = ('size', 'checksum')
|
||||
|
||||
# Metadata which cannot be changed (irrespective of the current image state)
|
||||
IMMUTABLE = ('status', 'id')
|
@ -1,96 +0,0 @@
|
||||
# Copyright 2011 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 glance_store as store
|
||||
from oslo_log import log as logging
|
||||
import webob.exc
|
||||
|
||||
from glance.common import exception
|
||||
from glance.i18n import _
|
||||
import glance.registry.client.v1.api as registry
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseController(object):
|
||||
def get_image_meta_or_404(self, request, image_id):
|
||||
"""
|
||||
Grabs the image metadata for an image with a supplied
|
||||
identifier or raises an HTTPNotFound (404) response
|
||||
|
||||
:param request: The WSGI/Webob Request object
|
||||
:param image_id: The opaque image identifier
|
||||
|
||||
:raises HTTPNotFound: if image does not exist
|
||||
"""
|
||||
context = request.context
|
||||
try:
|
||||
return registry.get_image_metadata(context, image_id)
|
||||
except exception.NotFound:
|
||||
LOG.debug("Image with identifier %s not found", image_id)
|
||||
msg = _("Image with identifier %s not found") % image_id
|
||||
raise webob.exc.HTTPNotFound(
|
||||
msg, request=request, content_type='text/plain')
|
||||
except exception.Forbidden:
|
||||
LOG.debug("Forbidden image access")
|
||||
raise webob.exc.HTTPForbidden(_("Forbidden image access"),
|
||||
request=request,
|
||||
content_type='text/plain')
|
||||
|
||||
def get_active_image_meta_or_error(self, request, image_id):
|
||||
"""
|
||||
Same as get_image_meta_or_404 except that it will raise a 403 if the
|
||||
image is deactivated or 404 if the image is otherwise not 'active'.
|
||||
"""
|
||||
image = self.get_image_meta_or_404(request, image_id)
|
||||
if image['status'] == 'deactivated':
|
||||
LOG.debug("Image %s is deactivated", image_id)
|
||||
msg = _("Image %s is deactivated") % image_id
|
||||
raise webob.exc.HTTPForbidden(
|
||||
msg, request=request, content_type='text/plain')
|
||||
if image['status'] != 'active':
|
||||
LOG.debug("Image %s is not active", image_id)
|
||||
msg = _("Image %s is not active") % image_id
|
||||
raise webob.exc.HTTPNotFound(
|
||||
msg, request=request, content_type='text/plain')
|
||||
return image
|
||||
|
||||
def update_store_acls(self, req, image_id, location_uri, public=False):
|
||||
if location_uri:
|
||||
try:
|
||||
read_tenants = []
|
||||
write_tenants = []
|
||||
members = registry.get_image_members(req.context, image_id)
|
||||
if members:
|
||||
for member in members:
|
||||
if member['can_share']:
|
||||
write_tenants.append(member['member_id'])
|
||||
else:
|
||||
read_tenants.append(member['member_id'])
|
||||
store.set_acls(location_uri, public=public,
|
||||
read_tenants=read_tenants,
|
||||
write_tenants=write_tenants,
|
||||
context=req.context)
|
||||
except store.UnknownScheme:
|
||||
msg = _("Store for image_id not found: %s") % image_id
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg,
|
||||
request=req,
|
||||
content_type='text/plain')
|
||||
except store.NotFound:
|
||||
msg = _("Data for image_id not found: %s") % image_id
|
||||
raise webob.exc.HTTPNotFound(explanation=msg,
|
||||
request=req,
|
||||
content_type='text/plain')
|
@ -1,40 +0,0 @@
|
||||
# Copyright 2012, Piston Cloud Computing, Inc.
|
||||
# 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.
|
||||
|
||||
|
||||
def validate(filter, value):
|
||||
return FILTER_FUNCTIONS.get(filter, lambda v: True)(value)
|
||||
|
||||
|
||||
def validate_int_in_range(min=0, max=None):
|
||||
def _validator(v):
|
||||
try:
|
||||
if max is None:
|
||||
return min <= int(v)
|
||||
return min <= int(v) <= max
|
||||
except ValueError:
|
||||
return False
|
||||
return _validator
|
||||
|
||||
|
||||
def validate_boolean(v):
|
||||
return v.lower() in ('none', 'true', 'false', '1', '0')
|
||||
|
||||
|
||||
FILTER_FUNCTIONS = {'size_max': validate_int_in_range(), # build validator
|
||||
'size_min': validate_int_in_range(), # build validator
|
||||
'min_ram': validate_int_in_range(), # build validator
|
||||
'protected': validate_boolean,
|
||||
'is_public': validate_boolean, }
|
@ -1,4 +1,4 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2020 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -13,19 +13,21 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from glance.common import wsgi
|
||||
|
||||
|
||||
class API(wsgi.Router):
|
||||
def init(mapper):
|
||||
reject_resource = wsgi.Resource(wsgi.RejectMethodController())
|
||||
mapper.connect("/v1", controller=reject_resource,
|
||||
action="reject")
|
||||
|
||||
"""WSGI router for Glance v1 API requests."""
|
||||
|
||||
class API(wsgi.Router):
|
||||
"""WSGI entry point for satisfy grenade."""
|
||||
|
||||
def __init__(self, mapper):
|
||||
reject_method_resource = wsgi.Resource(wsgi.RejectMethodController())
|
||||
mapper = mapper or wsgi.APIMapper()
|
||||
|
||||
mapper.connect("/",
|
||||
controller=reject_method_resource,
|
||||
action="reject")
|
||||
init(mapper)
|
||||
|
||||
super(API, self).__init__(mapper)
|
||||
|
@ -1,293 +0,0 @@
|
||||
# Copyright 2013 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 glance_store as store_api
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import excutils
|
||||
import webob.exc
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import store_utils
|
||||
from glance.common import utils
|
||||
import glance.db
|
||||
from glance.i18n import _, _LE, _LI
|
||||
import glance.registry.client.v1.api as registry
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def initiate_deletion(req, location_data, id):
|
||||
"""
|
||||
Deletes image data from the location of backend store.
|
||||
|
||||
:param req: The WSGI/Webob Request object
|
||||
:param location_data: Location to the image data in a data store
|
||||
:param id: Opaque image identifier
|
||||
"""
|
||||
store_utils.delete_image_location_from_backend(req.context,
|
||||
id, location_data)
|
||||
|
||||
|
||||
def _kill(req, image_id, from_state):
|
||||
"""
|
||||
Marks the image status to `killed`.
|
||||
|
||||
:param req: The WSGI/Webob Request object
|
||||
:param image_id: Opaque image identifier
|
||||
:param from_state: Permitted current status for transition to 'killed'
|
||||
"""
|
||||
# TODO(dosaboy): http://docs.openstack.org/developer/glance/statuses.html
|
||||
# needs updating to reflect the fact that queued->killed and saving->killed
|
||||
# are both allowed.
|
||||
registry.update_image_metadata(req.context, image_id,
|
||||
{'status': 'killed'},
|
||||
from_state=from_state)
|
||||
|
||||
|
||||
def safe_kill(req, image_id, from_state):
|
||||
"""
|
||||
Mark image killed without raising exceptions if it fails.
|
||||
|
||||
Since _kill is meant to be called from exceptions handlers, it should
|
||||
not raise itself, rather it should just log its error.
|
||||
|
||||
:param req: The WSGI/Webob Request object
|
||||
:param image_id: Opaque image identifier
|
||||
:param from_state: Permitted current status for transition to 'killed'
|
||||
"""
|
||||
try:
|
||||
_kill(req, image_id, from_state)
|
||||
except Exception:
|
||||
LOG.exception(_LE("Unable to kill image %(id)s: "), {'id': image_id})
|
||||
|
||||
|
||||
def upload_data_to_store(req, image_meta, image_data, store, notifier):
|
||||
"""
|
||||
Upload image data to specified store.
|
||||
|
||||
Upload image data to the store and cleans up on error.
|
||||
"""
|
||||
image_id = image_meta['id']
|
||||
|
||||
db_api = glance.db.get_api(v1_mode=True)
|
||||
image_size = image_meta.get('size')
|
||||
|
||||
try:
|
||||
# By default image_data will be passed as CooperativeReader object.
|
||||
# But if 'user_storage_quota' is enabled and 'remaining' is not None
|
||||
# then it will be passed as object of LimitingReader to
|
||||
# 'store_add_to_backend' method.
|
||||
image_data = utils.CooperativeReader(image_data)
|
||||
|
||||
remaining = glance.api.common.check_quota(
|
||||
req.context, image_size, db_api, image_id=image_id)
|
||||
if remaining is not None:
|
||||
image_data = utils.LimitingReader(image_data, remaining)
|
||||
|
||||
(uri,
|
||||
size,
|
||||
checksum,
|
||||
location_metadata) = store_api.store_add_to_backend(
|
||||
image_meta['id'],
|
||||
image_data,
|
||||
image_meta['size'],
|
||||
store,
|
||||
context=req.context)
|
||||
|
||||
location_data = {'url': uri,
|
||||
'metadata': location_metadata,
|
||||
'status': 'active'}
|
||||
|
||||
try:
|
||||
# recheck the quota in case there were simultaneous uploads that
|
||||
# did not provide the size
|
||||
glance.api.common.check_quota(
|
||||
req.context, size, db_api, image_id=image_id)
|
||||
except exception.StorageQuotaFull:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.info(_LI('Cleaning up %s after exceeding '
|
||||
'the quota'), image_id)
|
||||
store_utils.safe_delete_from_backend(
|
||||
req.context, image_meta['id'], location_data)
|
||||
|
||||
def _kill_mismatched(image_meta, attr, actual):
|
||||
supplied = image_meta.get(attr)
|
||||
if supplied and supplied != actual:
|
||||
msg = (_("Supplied %(attr)s (%(supplied)s) and "
|
||||
"%(attr)s generated from uploaded image "
|
||||
"(%(actual)s) did not match. Setting image "
|
||||
"status to 'killed'.") % {'attr': attr,
|
||||
'supplied': supplied,
|
||||
'actual': actual})
|
||||
LOG.error(msg)
|
||||
safe_kill(req, image_id, 'saving')
|
||||
initiate_deletion(req, location_data, image_id)
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg,
|
||||
content_type="text/plain",
|
||||
request=req)
|
||||
|
||||
# Verify any supplied size/checksum value matches size/checksum
|
||||
# returned from store when adding image
|
||||
_kill_mismatched(image_meta, 'size', size)
|
||||
_kill_mismatched(image_meta, 'checksum', checksum)
|
||||
|
||||
# Update the database with the checksum returned
|
||||
# from the backend store
|
||||
LOG.debug("Updating image %(image_id)s data. "
|
||||
"Checksum set to %(checksum)s, size set "
|
||||
"to %(size)d", {'image_id': image_id,
|
||||
'checksum': checksum,
|
||||
'size': size})
|
||||
update_data = {'checksum': checksum,
|
||||
'size': size}
|
||||
try:
|
||||
try:
|
||||
state = 'saving'
|
||||
image_meta = registry.update_image_metadata(req.context,
|
||||
image_id,
|
||||
update_data,
|
||||
from_state=state)
|
||||
except exception.Duplicate:
|
||||
image = registry.get_image_metadata(req.context, image_id)
|
||||
if image['status'] == 'deleted':
|
||||
raise exception.ImageNotFound()
|
||||
else:
|
||||
raise
|
||||
except exception.NotAuthenticated as e:
|
||||
# Delete image data due to possible token expiration.
|
||||
LOG.debug("Authentication error - the token may have "
|
||||
"expired during file upload. Deleting image data for "
|
||||
" %s", image_id)
|
||||
initiate_deletion(req, location_data, image_id)
|
||||
raise webob.exc.HTTPUnauthorized(explanation=e.msg, request=req)
|
||||
except exception.ImageNotFound:
|
||||
msg = _("Image %s could not be found after upload. The image may"
|
||||
" have been deleted during the upload.") % image_id
|
||||
LOG.info(msg)
|
||||
|
||||
# NOTE(jculp): we need to clean up the datastore if an image
|
||||
# resource is deleted while the image data is being uploaded
|
||||
#
|
||||
# We get "location_data" from above call to store.add(), any
|
||||
# exceptions that occur there handle this same issue internally,
|
||||
# Since this is store-agnostic, should apply to all stores.
|
||||
initiate_deletion(req, location_data, image_id)
|
||||
raise webob.exc.HTTPPreconditionFailed(explanation=msg,
|
||||
request=req,
|
||||
content_type='text/plain')
|
||||
|
||||
except store_api.StoreAddDisabled:
|
||||
msg = _("Error in store configuration. Adding images to store "
|
||||
"is disabled.")
|
||||
LOG.exception(msg)
|
||||
safe_kill(req, image_id, 'saving')
|
||||
notifier.error('image.upload', msg)
|
||||
raise webob.exc.HTTPGone(explanation=msg, request=req,
|
||||
content_type='text/plain')
|
||||
|
||||
except (store_api.Duplicate, exception.Duplicate) as e:
|
||||
msg = (_("Attempt to upload duplicate image: %s") %
|
||||
encodeutils.exception_to_unicode(e))
|
||||
LOG.warn(msg)
|
||||
# NOTE(dosaboy): do not delete the image since it is likely that this
|
||||
# conflict is a result of another concurrent upload that will be
|
||||
# successful.
|
||||
notifier.error('image.upload', msg)
|
||||
raise webob.exc.HTTPConflict(explanation=msg,
|
||||
request=req,
|
||||
content_type="text/plain")
|
||||
|
||||
except exception.Forbidden as e:
|
||||
msg = (_("Forbidden upload attempt: %s") %
|
||||
encodeutils.exception_to_unicode(e))
|
||||
LOG.warn(msg)
|
||||
safe_kill(req, image_id, 'saving')
|
||||
notifier.error('image.upload', msg)
|
||||
raise webob.exc.HTTPForbidden(explanation=msg,
|
||||
request=req,
|
||||
content_type="text/plain")
|
||||
|
||||
except store_api.StorageFull as e:
|
||||
msg = (_("Image storage media is full: %s") %
|
||||
encodeutils.exception_to_unicode(e))
|
||||
LOG.error(msg)
|
||||
safe_kill(req, image_id, 'saving')
|
||||
notifier.error('image.upload', msg)
|
||||
raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg,
|
||||
request=req,
|
||||
content_type='text/plain')
|
||||
|
||||
except store_api.StorageWriteDenied as e:
|
||||
msg = (_("Insufficient permissions on image storage media: %s") %
|
||||
encodeutils.exception_to_unicode(e))
|
||||
LOG.error(msg)
|
||||
safe_kill(req, image_id, 'saving')
|
||||
notifier.error('image.upload', msg)
|
||||
raise webob.exc.HTTPServiceUnavailable(explanation=msg,
|
||||
request=req,
|
||||
content_type='text/plain')
|
||||
|
||||
except exception.ImageSizeLimitExceeded:
|
||||
msg = (_("Denying attempt to upload image larger than %d bytes.")
|
||||
% CONF.image_size_cap)
|
||||
LOG.warn(msg)
|
||||
safe_kill(req, image_id, 'saving')
|
||||
notifier.error('image.upload', msg)
|
||||
raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg,
|
||||
request=req,
|
||||
content_type='text/plain')
|
||||
|
||||
except exception.StorageQuotaFull as e:
|
||||
msg = (_("Denying attempt to upload image because it exceeds the "
|
||||
"quota: %s") % encodeutils.exception_to_unicode(e))
|
||||
LOG.warn(msg)
|
||||
safe_kill(req, image_id, 'saving')
|
||||
notifier.error('image.upload', msg)
|
||||
raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg,
|
||||
request=req,
|
||||
content_type='text/plain')
|
||||
|
||||
except webob.exc.HTTPError:
|
||||
# NOTE(bcwaldon): Ideally, we would just call 'raise' here,
|
||||
# but something in the above function calls is affecting the
|
||||
# exception context and we must explicitly re-raise the
|
||||
# caught exception.
|
||||
msg = _LE("Received HTTP error while uploading image %s") % image_id
|
||||
notifier.error('image.upload', msg)
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(msg)
|
||||
safe_kill(req, image_id, 'saving')
|
||||
|
||||
except (ValueError, IOError):
|
||||
msg = _("Client disconnected before sending all data to backend")
|
||||
LOG.warn(msg)
|
||||
safe_kill(req, image_id, 'saving')
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg,
|
||||
content_type="text/plain",
|
||||
request=req)
|
||||
|
||||
except Exception:
|
||||
msg = _("Failed to upload image %s") % image_id
|
||||
LOG.exception(msg)
|
||||
safe_kill(req, image_id, 'saving')
|
||||
notifier.error('image.upload', msg)
|
||||
raise webob.exc.HTTPInternalServerError(explanation=msg,
|
||||
request=req,
|
||||
content_type='text/plain')
|
||||
|
||||
return image_meta, location_data
|
@ -159,16 +159,6 @@ Related Options:
|
||||
""")),
|
||||
]
|
||||
|
||||
_DEPRECATE_GLANCE_V1_MSG = _('The Images (Glance) version 1 API has been '
|
||||
'DEPRECATED in the Newton release and will be '
|
||||
'removed on or after Pike release, following '
|
||||
'the standard OpenStack deprecation policy. '
|
||||
'Hence, the configuration options specific to '
|
||||
'the Images (Glance) v1 API are hereby '
|
||||
'deprecated and subject to removal. Operators '
|
||||
'are advised to deploy the Images (Glance) v2 '
|
||||
'API.')
|
||||
|
||||
common_opts = [
|
||||
cfg.BoolOpt('allow_additional_image_properties', default=True,
|
||||
deprecated_for_removal=True,
|
||||
|
@ -1,302 +0,0 @@
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
RPC Controller
|
||||
"""
|
||||
import datetime
|
||||
import traceback
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import encodeutils
|
||||
import oslo_utils.importutils as imp
|
||||
import six
|
||||
from webob import exc
|
||||
|
||||
from glance.common import client
|
||||
from glance.common import exception
|
||||
from glance.common import timeutils
|
||||
from glance.common import wsgi
|
||||
from glance.i18n import _, _LE
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
rpc_opts = [
|
||||
cfg.ListOpt('allowed_rpc_exception_modules',
|
||||
default=['glance.common.exception',
|
||||
'builtins',
|
||||
'exceptions',
|
||||
],
|
||||
help=_("""
|
||||
List of allowed exception modules to handle RPC exceptions.
|
||||
|
||||
Provide a comma separated list of modules whose exceptions are
|
||||
permitted to be recreated upon receiving exception data via an RPC
|
||||
call made to Glance. The default list includes
|
||||
``glance.common.exception``, ``builtins``, and ``exceptions``.
|
||||
|
||||
The RPC protocol permits interaction with Glance via calls across a
|
||||
network or within the same system. Including a list of exception
|
||||
namespaces with this option enables RPC to propagate the exceptions
|
||||
back to the users.
|
||||
|
||||
Possible values:
|
||||
* A comma separated list of valid exception modules
|
||||
|
||||
Related options:
|
||||
* None
|
||||
""")),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(rpc_opts)
|
||||
|
||||
|
||||
class RPCJSONSerializer(wsgi.JSONResponseSerializer):
|
||||
|
||||
@staticmethod
|
||||
def _to_primitive(_type, _value):
|
||||
return {"_type": _type, "_value": _value}
|
||||
|
||||
def _sanitizer(self, obj):
|
||||
if isinstance(obj, datetime.datetime):
|
||||
return self._to_primitive("datetime",
|
||||
obj.isoformat())
|
||||
|
||||
return super(RPCJSONSerializer, self)._sanitizer(obj)
|
||||
|
||||
|
||||
class RPCJSONDeserializer(wsgi.JSONRequestDeserializer):
|
||||
|
||||
@staticmethod
|
||||
def _to_datetime(obj):
|
||||
return timeutils.normalize_time(timeutils.parse_isotime(obj))
|
||||
|
||||
def _sanitizer(self, obj):
|
||||
try:
|
||||
_type, _value = obj["_type"], obj["_value"]
|
||||
return getattr(self, "_to_" + _type)(_value)
|
||||
except (KeyError, AttributeError):
|
||||
return obj
|
||||
|
||||
|
||||
class Controller(object):
|
||||
"""
|
||||
Base RPCController.
|
||||
|
||||
This is the base controller for RPC based APIs. Commands
|
||||
handled by this controller respect the following form:
|
||||
|
||||
::
|
||||
|
||||
[{
|
||||
'command': 'method_name',
|
||||
'kwargs': {...}
|
||||
}]
|
||||
|
||||
The controller is capable of processing more than one command
|
||||
per request and will always return a list of results.
|
||||
|
||||
:param bool raise_exc: Specifies whether to raise
|
||||
exceptions instead of "serializing" them.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, raise_exc=False):
|
||||
self._registered = {}
|
||||
self.raise_exc = raise_exc
|
||||
|
||||
def register(self, resource, filtered=None, excluded=None, refiner=None):
|
||||
"""
|
||||
Exports methods through the RPC Api.
|
||||
|
||||
:param resource: Resource's instance to register.
|
||||
:param filtered: List of methods that *can* be registered. Read
|
||||
as "Method must be in this list".
|
||||
:param excluded: List of methods to exclude.
|
||||
:param refiner: Callable to use as filter for methods.
|
||||
|
||||
:raises TypeError: If refiner is not callable.
|
||||
|
||||
"""
|
||||
|
||||
funcs = [x for x in dir(resource) if not x.startswith("_")]
|
||||
|
||||
if filtered:
|
||||
funcs = [f for f in funcs if f in filtered]
|
||||
|
||||
if excluded:
|
||||
funcs = [f for f in funcs if f not in excluded]
|
||||
|
||||
if refiner:
|
||||
funcs = filter(refiner, funcs)
|
||||
|
||||
for name in funcs:
|
||||
meth = getattr(resource, name)
|
||||
|
||||
if not callable(meth):
|
||||
continue
|
||||
|
||||
self._registered[name] = meth
|
||||
|
||||
def __call__(self, req, body):
|
||||
"""
|
||||
Executes the command
|
||||
"""
|
||||
|
||||
if not isinstance(body, list):
|
||||
msg = _("Request must be a list of commands")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
def validate(cmd):
|
||||
if not isinstance(cmd, dict):
|
||||
msg = _("Bad Command: %s") % str(cmd)
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
command, kwargs = cmd.get("command"), cmd.get("kwargs")
|
||||
|
||||
if (not command or not isinstance(command, six.string_types) or
|
||||
(kwargs and not isinstance(kwargs, dict))):
|
||||
msg = _("Wrong command structure: %s") % (str(cmd))
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
method = self._registered.get(command)
|
||||
if not method:
|
||||
# Just raise 404 if the user tries to
|
||||
# access a private method. No need for
|
||||
# 403 here since logically the command
|
||||
# is not registered to the rpc dispatcher
|
||||
raise exc.HTTPNotFound(explanation=_("Command not found"))
|
||||
|
||||
return True
|
||||
|
||||
# If more than one command were sent then they might
|
||||
# be intended to be executed sequentially, that for,
|
||||
# lets first verify they're all valid before executing
|
||||
# them.
|
||||
commands = filter(validate, body)
|
||||
|
||||
results = []
|
||||
for cmd in commands:
|
||||
# kwargs is not required
|
||||
command, kwargs = cmd["command"], cmd.get("kwargs", {})
|
||||
method = self._registered[command]
|
||||
try:
|
||||
result = method(req.context, **kwargs)
|
||||
except Exception as e:
|
||||
if self.raise_exc:
|
||||
raise
|
||||
|
||||
cls, val = e.__class__, encodeutils.exception_to_unicode(e)
|
||||
msg = (_LE("RPC Call Error: %(val)s\n%(tb)s") %
|
||||
dict(val=val, tb=traceback.format_exc()))
|
||||
LOG.error(msg)
|
||||
|
||||
# NOTE(flaper87): Don't propagate all exceptions
|
||||
# but the ones allowed by the user.
|
||||
module = cls.__module__
|
||||
if module not in CONF.allowed_rpc_exception_modules:
|
||||
cls = exception.RPCError
|
||||
val = encodeutils.exception_to_unicode(
|
||||
exception.RPCError(cls=cls, val=val))
|
||||
|
||||
cls_path = "%s.%s" % (cls.__module__, cls.__name__)
|
||||
result = {"_error": {"cls": cls_path, "val": val}}
|
||||
results.append(result)
|
||||
return results
|
||||
|
||||
|
||||
class RPCClient(client.BaseClient):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._serializer = RPCJSONSerializer()
|
||||
self._deserializer = RPCJSONDeserializer()
|
||||
|
||||
self.raise_exc = kwargs.pop("raise_exc", True)
|
||||
self.base_path = kwargs.pop("base_path", '/rpc')
|
||||
super(RPCClient, self).__init__(*args, **kwargs)
|
||||
|
||||
@client.handle_unauthenticated
|
||||
def bulk_request(self, commands):
|
||||
"""
|
||||
Execute multiple commands in a single request.
|
||||
|
||||
:param commands: List of commands to send. Commands
|
||||
must respect the following form
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
'command': 'method_name',
|
||||
'kwargs': method_kwargs
|
||||
}
|
||||
|
||||
"""
|
||||
body = self._serializer.to_json(commands)
|
||||
response = super(RPCClient, self).do_request('POST',
|
||||
self.base_path,
|
||||
body)
|
||||
return self._deserializer.from_json(response.read())
|
||||
|
||||
def do_request(self, method, **kwargs):
|
||||
"""
|
||||
Simple do_request override. This method serializes
|
||||
the outgoing body and builds the command that will
|
||||
be sent.
|
||||
|
||||
:param method: The remote python method to call
|
||||
:param kwargs: Dynamic parameters that will be
|
||||
passed to the remote method.
|
||||
"""
|
||||
content = self.bulk_request([{'command': method,
|
||||
'kwargs': kwargs}])
|
||||
|
||||
# NOTE(flaper87): Return the first result if
|
||||
# a single command was executed.
|
||||
content = content[0]
|
||||
|
||||
# NOTE(flaper87): Check if content is an error
|
||||
# and re-raise it if raise_exc is True. Before
|
||||
# checking if content contains the '_error' key,
|
||||
# verify if it is an instance of dict - since the
|
||||
# RPC call may have returned something different.
|
||||
if self.raise_exc and (isinstance(content, dict)
|
||||
and '_error' in content):
|
||||
error = content['_error']
|
||||
try:
|
||||
exc_cls = imp.import_class(error['cls'])
|
||||
raise exc_cls(error['val'])
|
||||
except ImportError:
|
||||
# NOTE(flaper87): The exception
|
||||
# class couldn't be imported, using
|
||||
# a generic exception.
|
||||
raise exception.RPCError(**error)
|
||||
return content
|
||||
|
||||
def __getattr__(self, item):
|
||||
"""
|
||||
This method returns a method_proxy that
|
||||
will execute the rpc call in the registry
|
||||
service.
|
||||
"""
|
||||
if item.startswith('_'):
|
||||
raise AttributeError(item)
|
||||
|
||||
def method_proxy(**kw):
|
||||
return self.do_request(item, **kw)
|
||||
|
||||
return method_proxy
|
@ -1,546 +0,0 @@
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# Copyright 2015 Mirantis, Inc.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
This is the Registry's Driver API.
|
||||
|
||||
This API relies on the registry RPC client (version >= 2). The functions bellow
|
||||
work as a proxy for the database back-end configured in the registry service,
|
||||
which means that everything returned by that back-end will be also returned by
|
||||
this API.
|
||||
|
||||
|
||||
This API exists for supporting deployments not willing to put database
|
||||
credentials in glance-api. Those deployments can rely on this registry driver
|
||||
that will talk to a remote registry service, which will then access the
|
||||
database back-end.
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
from glance.db import utils as db_utils
|
||||
from glance.registry.client.v2 import api
|
||||
|
||||
|
||||
def configure():
|
||||
api.configure_registry_client()
|
||||
|
||||
|
||||
def _get_client(func):
|
||||
"""Injects a client instance to the each function
|
||||
|
||||
This decorator creates an instance of the Registry
|
||||
client and passes it as an argument to each function
|
||||
in this API.
|
||||
"""
|
||||
@functools.wraps(func)
|
||||
def wrapper(context, *args, **kwargs):
|
||||
client = api.get_registry_client(context)
|
||||
return func(client, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_create(client, values, v1_mode=False):
|
||||
"""Create an image from the values dictionary."""
|
||||
return client.image_create(values=values, v1_mode=v1_mode)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_update(client, image_id, values, purge_props=False, from_state=None,
|
||||
v1_mode=False):
|
||||
"""
|
||||
Set the given properties on an image and update it.
|
||||
|
||||
:raises ImageNotFound: if image does not exist.
|
||||
"""
|
||||
return client.image_update(values=values,
|
||||
image_id=image_id,
|
||||
purge_props=purge_props,
|
||||
from_state=from_state,
|
||||
v1_mode=v1_mode)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_destroy(client, image_id):
|
||||
"""Destroy the image or raise if it does not exist."""
|
||||
return client.image_destroy(image_id=image_id)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_get(client, image_id, force_show_deleted=False, v1_mode=False):
|
||||
return client.image_get(image_id=image_id,
|
||||
force_show_deleted=force_show_deleted,
|
||||
v1_mode=v1_mode)
|
||||
|
||||
|
||||
def is_image_visible(context, image, status=None):
|
||||
"""Return True if the image is visible in this context."""
|
||||
return db_utils.is_image_visible(context, image, image_member_find, status)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_get_all(client, filters=None, marker=None, limit=None,
|
||||
sort_key=None, sort_dir=None,
|
||||
member_status='accepted', is_public=None,
|
||||
admin_as_user=False, return_tag=False, v1_mode=False):
|
||||
"""
|
||||
Get all images that match zero or more filters.
|
||||
|
||||
:param filters: dict of filter keys and values. If a 'properties'
|
||||
key is present, it is treated as a dict of key/value
|
||||
filters on the image properties attribute
|
||||
:param marker: image id after which to start page
|
||||
:param limit: maximum number of images to return
|
||||
:param sort_key: image attribute by which results should be sorted
|
||||
:param sort_dir: direction in which results should be sorted (asc, desc)
|
||||
:param member_status: only return shared images that have this membership
|
||||
status
|
||||
:param is_public: If true, return only public images. If false, return
|
||||
only private and shared images.
|
||||
:param admin_as_user: For backwards compatibility. If true, then return to
|
||||
an admin the equivalent set of images which it would see
|
||||
if it were a regular user
|
||||
:param return_tag: To indicates whether image entry in result includes it
|
||||
relevant tag entries. This could improve upper-layer
|
||||
query performance, to prevent using separated calls
|
||||
:param v1_mode: If true, mutates the 'visibility' value of each image
|
||||
into the v1-compatible field 'is_public'
|
||||
"""
|
||||
sort_key = ['created_at'] if not sort_key else sort_key
|
||||
sort_dir = ['desc'] if not sort_dir else sort_dir
|
||||
return client.image_get_all(filters=filters, marker=marker, limit=limit,
|
||||
sort_key=sort_key, sort_dir=sort_dir,
|
||||
member_status=member_status,
|
||||
is_public=is_public,
|
||||
admin_as_user=admin_as_user,
|
||||
return_tag=return_tag,
|
||||
v1_mode=v1_mode)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_property_create(client, values, session=None):
|
||||
"""Create an ImageProperty object"""
|
||||
return client.image_property_create(values=values)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_property_delete(client, prop_ref, image_ref, session=None):
|
||||
"""
|
||||
Used internally by _image_property_create and image_property_update
|
||||
"""
|
||||
return client.image_property_delete(prop_ref=prop_ref, image_ref=image_ref)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_member_create(client, values, session=None):
|
||||
"""Create an ImageMember object"""
|
||||
return client.image_member_create(values=values)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_member_update(client, memb_id, values):
|
||||
"""Update an ImageMember object"""
|
||||
return client.image_member_update(memb_id=memb_id, values=values)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_member_delete(client, memb_id, session=None):
|
||||
"""Delete an ImageMember object"""
|
||||
client.image_member_delete(memb_id=memb_id)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_member_find(client, image_id=None, member=None, status=None,
|
||||
include_deleted=False):
|
||||
"""Find all members that meet the given criteria.
|
||||
|
||||
Note, currently include_deleted should be true only when create a new
|
||||
image membership, as there may be a deleted image membership between
|
||||
the same image and tenant, the membership will be reused in this case.
|
||||
It should be false in other cases.
|
||||
|
||||
:param image_id: identifier of image entity
|
||||
:param member: tenant to which membership has been granted
|
||||
:include_deleted: A boolean indicating whether the result should include
|
||||
the deleted record of image member
|
||||
"""
|
||||
return client.image_member_find(image_id=image_id,
|
||||
member=member,
|
||||
status=status,
|
||||
include_deleted=include_deleted)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_member_count(client, image_id):
|
||||
"""Return the number of image members for this image
|
||||
|
||||
:param image_id: identifier of image entity
|
||||
"""
|
||||
return client.image_member_count(image_id=image_id)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_tag_set_all(client, image_id, tags):
|
||||
client.image_tag_set_all(image_id=image_id, tags=tags)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_tag_create(client, image_id, value, session=None):
|
||||
"""Create an image tag."""
|
||||
return client.image_tag_create(image_id=image_id, value=value)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_tag_delete(client, image_id, value, session=None):
|
||||
"""Delete an image tag."""
|
||||
client.image_tag_delete(image_id=image_id, value=value)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_tag_get_all(client, image_id, session=None):
|
||||
"""Get a list of tags for a specific image."""
|
||||
return client.image_tag_get_all(image_id=image_id)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_location_delete(client, image_id, location_id, status, session=None):
|
||||
"""Delete an image location."""
|
||||
client.image_location_delete(image_id=image_id, location_id=location_id,
|
||||
status=status)
|
||||
|
||||
|
||||
@_get_client
|
||||
def image_location_update(client, image_id, location, session=None):
|
||||
"""Update image location."""
|
||||
client.image_location_update(image_id=image_id, location=location)
|
||||
|
||||
|
||||
@_get_client
|
||||
def user_get_storage_usage(client, owner_id, image_id=None, session=None):
|
||||
return client.user_get_storage_usage(owner_id=owner_id, image_id=image_id)
|
||||
|
||||
|
||||
@_get_client
|
||||
def task_get(client, task_id, session=None, force_show_deleted=False):
|
||||
"""Get a single task object
|
||||
:returns: task dictionary
|
||||
"""
|
||||
return client.task_get(task_id=task_id, session=session,
|
||||
force_show_deleted=force_show_deleted)
|
||||
|
||||
|
||||
@_get_client
|
||||
def task_get_all(client, filters=None, marker=None, limit=None,
|
||||
sort_key='created_at', sort_dir='desc', admin_as_user=False):
|
||||
"""Get all tasks that match zero or more filters.
|
||||
|
||||
:param filters: dict of filter keys and values.
|
||||
:param marker: task id after which to start page
|
||||
:param limit: maximum number of tasks to return
|
||||
:param sort_key: task attribute by which results should be sorted
|
||||
:param sort_dir: direction in which results should be sorted (asc, desc)
|
||||
:param admin_as_user: For backwards compatibility. If true, then return to
|
||||
an admin the equivalent set of tasks which it would see
|
||||
if it were a regular user
|
||||
:returns: tasks set
|
||||
"""
|
||||
return client.task_get_all(filters=filters, marker=marker, limit=limit,
|
||||
sort_key=sort_key, sort_dir=sort_dir,
|
||||
admin_as_user=admin_as_user)
|
||||
|
||||
|
||||
@_get_client
|
||||
def task_create(client, values, session=None):
|
||||
"""Create a task object"""
|
||||
return client.task_create(values=values, session=session)
|
||||
|
||||
|
||||
@_get_client
|
||||
def task_delete(client, task_id, session=None):
|
||||
"""Delete a task object"""
|
||||
return client.task_delete(task_id=task_id, session=session)
|
||||
|
||||
|
||||
@_get_client
|
||||
def task_update(client, task_id, values, session=None):
|
||||
return client.task_update(task_id=task_id, values=values, session=session)
|
||||
|
||||
|
||||
# Metadef
|
||||
@_get_client
|
||||
def metadef_namespace_get_all(
|
||||
client, marker=None, limit=None, sort_key='created_at',
|
||||
sort_dir=None, filters=None, session=None):
|
||||
return client.metadef_namespace_get_all(
|
||||
marker=marker, limit=limit,
|
||||
sort_key=sort_key, sort_dir=sort_dir, filters=filters)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_namespace_get(client, namespace_name, session=None):
|
||||
return client.metadef_namespace_get(namespace_name=namespace_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_namespace_create(client, values, session=None):
|
||||
return client.metadef_namespace_create(values=values)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_namespace_update(
|
||||
client, namespace_id, namespace_dict,
|
||||
session=None):
|
||||
return client.metadef_namespace_update(
|
||||
namespace_id=namespace_id, namespace_dict=namespace_dict)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_namespace_delete(client, namespace_name, session=None):
|
||||
return client.metadef_namespace_delete(
|
||||
namespace_name=namespace_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_object_get_all(client, namespace_name, session=None):
|
||||
return client.metadef_object_get_all(
|
||||
namespace_name=namespace_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_object_get(
|
||||
client,
|
||||
namespace_name, object_name, session=None):
|
||||
return client.metadef_object_get(
|
||||
namespace_name=namespace_name, object_name=object_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_object_create(
|
||||
client,
|
||||
namespace_name, object_dict, session=None):
|
||||
return client.metadef_object_create(
|
||||
namespace_name=namespace_name, object_dict=object_dict)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_object_update(
|
||||
client,
|
||||
namespace_name, object_id,
|
||||
object_dict, session=None):
|
||||
return client.metadef_object_update(
|
||||
namespace_name=namespace_name, object_id=object_id,
|
||||
object_dict=object_dict)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_object_delete(
|
||||
client,
|
||||
namespace_name, object_name,
|
||||
session=None):
|
||||
return client.metadef_object_delete(
|
||||
namespace_name=namespace_name, object_name=object_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_object_delete_namespace_content(
|
||||
client,
|
||||
namespace_name, session=None):
|
||||
return client.metadef_object_delete_namespace_content(
|
||||
namespace_name=namespace_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_object_count(
|
||||
client,
|
||||
namespace_name, session=None):
|
||||
return client.metadef_object_count(
|
||||
namespace_name=namespace_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_property_get_all(
|
||||
client,
|
||||
namespace_name, session=None):
|
||||
return client.metadef_property_get_all(
|
||||
namespace_name=namespace_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_property_get(
|
||||
client,
|
||||
namespace_name, property_name,
|
||||
session=None):
|
||||
return client.metadef_property_get(
|
||||
namespace_name=namespace_name, property_name=property_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_property_create(
|
||||
client,
|
||||
namespace_name, property_dict,
|
||||
session=None):
|
||||
return client.metadef_property_create(
|
||||
namespace_name=namespace_name, property_dict=property_dict)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_property_update(
|
||||
client,
|
||||
namespace_name, property_id,
|
||||
property_dict, session=None):
|
||||
return client.metadef_property_update(
|
||||
namespace_name=namespace_name, property_id=property_id,
|
||||
property_dict=property_dict)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_property_delete(
|
||||
client,
|
||||
namespace_name, property_name,
|
||||
session=None):
|
||||
return client.metadef_property_delete(
|
||||
namespace_name=namespace_name, property_name=property_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_property_delete_namespace_content(
|
||||
client,
|
||||
namespace_name, session=None):
|
||||
return client.metadef_property_delete_namespace_content(
|
||||
namespace_name=namespace_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_property_count(
|
||||
client,
|
||||
namespace_name, session=None):
|
||||
return client.metadef_property_count(
|
||||
namespace_name=namespace_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_resource_type_create(client, values, session=None):
|
||||
return client.metadef_resource_type_create(values=values)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_resource_type_get(
|
||||
client,
|
||||
resource_type_name, session=None):
|
||||
return client.metadef_resource_type_get(
|
||||
resource_type_name=resource_type_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_resource_type_get_all(client, session=None):
|
||||
return client.metadef_resource_type_get_all()
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_resource_type_delete(
|
||||
client,
|
||||
resource_type_name, session=None):
|
||||
return client.metadef_resource_type_delete(
|
||||
resource_type_name=resource_type_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_resource_type_association_get(
|
||||
client,
|
||||
namespace_name, resource_type_name,
|
||||
session=None):
|
||||
return client.metadef_resource_type_association_get(
|
||||
namespace_name=namespace_name, resource_type_name=resource_type_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_resource_type_association_create(
|
||||
client,
|
||||
namespace_name, values, session=None):
|
||||
return client.metadef_resource_type_association_create(
|
||||
namespace_name=namespace_name, values=values)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_resource_type_association_delete(
|
||||
client,
|
||||
namespace_name, resource_type_name, session=None):
|
||||
return client.metadef_resource_type_association_delete(
|
||||
namespace_name=namespace_name, resource_type_name=resource_type_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_resource_type_association_get_all_by_namespace(
|
||||
client,
|
||||
namespace_name, session=None):
|
||||
return client.metadef_resource_type_association_get_all_by_namespace(
|
||||
namespace_name=namespace_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_tag_get_all(client, namespace_name, filters=None, marker=None,
|
||||
limit=None, sort_key='created_at', sort_dir=None,
|
||||
session=None):
|
||||
return client.metadef_tag_get_all(
|
||||
namespace_name=namespace_name, filters=filters, marker=marker,
|
||||
limit=limit, sort_key=sort_key, sort_dir=sort_dir, session=session)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_tag_get(client, namespace_name, name, session=None):
|
||||
return client.metadef_tag_get(
|
||||
namespace_name=namespace_name, name=name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_tag_create(
|
||||
client, namespace_name, tag_dict, session=None):
|
||||
return client.metadef_tag_create(
|
||||
namespace_name=namespace_name, tag_dict=tag_dict)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_tag_create_tags(
|
||||
client, namespace_name, tag_list, session=None):
|
||||
return client.metadef_tag_create_tags(
|
||||
namespace_name=namespace_name, tag_list=tag_list)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_tag_update(
|
||||
client, namespace_name, id, tag_dict, session=None):
|
||||
return client.metadef_tag_update(
|
||||
namespace_name=namespace_name, id=id, tag_dict=tag_dict)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_tag_delete(
|
||||
client, namespace_name, name, session=None):
|
||||
return client.metadef_tag_delete(
|
||||
namespace_name=namespace_name, name=name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_tag_delete_namespace_content(
|
||||
client, namespace_name, session=None):
|
||||
return client.metadef_tag_delete_namespace_content(
|
||||
namespace_name=namespace_name)
|
||||
|
||||
|
||||
@_get_client
|
||||
def metadef_tag_count(client, namespace_name, session=None):
|
||||
return client.metadef_tag_count(namespace_name=namespace_name)
|
@ -89,7 +89,6 @@ def no_translate_debug_logs(logical_line, filename):
|
||||
"glance/domain",
|
||||
"glance/image_cache",
|
||||
"glance/quota",
|
||||
"glance/registry",
|
||||
"glance/store",
|
||||
"glance/tests",
|
||||
]
|
||||
|
@ -36,7 +36,6 @@ import glance.common.config
|
||||
import glance.common.location_strategy
|
||||
import glance.common.location_strategy.store_type
|
||||
import glance.common.property_utils
|
||||
import glance.common.rpc
|
||||
import glance.common.wsgi
|
||||
import glance.image_cache
|
||||
import glance.image_cache.drivers.sqlite
|
||||
@ -51,7 +50,6 @@ _api_opts = [
|
||||
glance.common.config.common_opts,
|
||||
glance.common.location_strategy.location_strategy_opts,
|
||||
glance.common.property_utils.property_opts,
|
||||
glance.common.rpc.rpc_opts,
|
||||
glance.common.wsgi.bind_opts,
|
||||
glance.common.wsgi.eventlet_opts,
|
||||
glance.common.wsgi.socket_opts,
|
||||
|
@ -1,68 +0,0 @@
|
||||
# Copyright 2010-2011 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.
|
||||
|
||||
"""
|
||||
Registry API
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from glance.i18n import _
|
||||
|
||||
|
||||
registry_addr_opts = [
|
||||
cfg.HostAddressOpt('registry_host',
|
||||
default='0.0.0.0',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_since="Queens",
|
||||
deprecated_reason=_("""
|
||||
Glance registry service is deprecated for removal.
|
||||
|
||||
More information can be found from the spec:
|
||||
http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html
|
||||
"""),
|
||||
help=_("""
|
||||
Address the registry server is hosted on.
|
||||
|
||||
Possible values:
|
||||
* A valid IP or hostname
|
||||
|
||||
Related options:
|
||||
* None
|
||||
|
||||
""")),
|
||||
cfg.PortOpt('registry_port', default=9191,
|
||||
deprecated_for_removal=True,
|
||||
deprecated_since="Queens",
|
||||
deprecated_reason=_("""
|
||||
Glance registry service is deprecated for removal.
|
||||
|
||||
More information can be found from the spec:
|
||||
http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html
|
||||
"""),
|
||||
help=_("""
|
||||
Port the registry server is listening on.
|
||||
|
||||
Possible values:
|
||||
* A valid port number
|
||||
|
||||
Related options:
|
||||
* None
|
||||
|
||||
""")),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(registry_addr_opts)
|
@ -1,91 +0,0 @@
|
||||
# Copyright 2010-2011 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 import wsgi
|
||||
from glance.registry.api.v1 import images
|
||||
from glance.registry.api.v1 import members
|
||||
|
||||
|
||||
def init(mapper):
|
||||
images_resource = images.create_resource()
|
||||
|
||||
mapper.connect("/",
|
||||
controller=images_resource,
|
||||
action="index")
|
||||
mapper.connect("/images",
|
||||
controller=images_resource,
|
||||
action="index",
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect("/images",
|
||||
controller=images_resource,
|
||||
action="create",
|
||||
conditions={'method': ['POST']})
|
||||
mapper.connect("/images/detail",
|
||||
controller=images_resource,
|
||||
action="detail",
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect("/images/{id}",
|
||||
controller=images_resource,
|
||||
action="show",
|
||||
conditions=dict(method=["GET"]))
|
||||
mapper.connect("/images/{id}",
|
||||
controller=images_resource,
|
||||
action="update",
|
||||
conditions=dict(method=["PUT"]))
|
||||
mapper.connect("/images/{id}",
|
||||
controller=images_resource,
|
||||
action="delete",
|
||||
conditions=dict(method=["DELETE"]))
|
||||
|
||||
members_resource = members.create_resource()
|
||||
|
||||
mapper.connect("/images/{image_id}/members",
|
||||
controller=members_resource,
|
||||
action="index",
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect("/images/{image_id}/members",
|
||||
controller=members_resource,
|
||||
action="create",
|
||||
conditions={'method': ['POST']})
|
||||
mapper.connect("/images/{image_id}/members",
|
||||
controller=members_resource,
|
||||
action="update_all",
|
||||
conditions=dict(method=["PUT"]))
|
||||
mapper.connect("/images/{image_id}/members/{id}",
|
||||
controller=members_resource,
|
||||
action="show",
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect("/images/{image_id}/members/{id}",
|
||||
controller=members_resource,
|
||||
action="update",
|
||||
conditions={'method': ['PUT']})
|
||||
mapper.connect("/images/{image_id}/members/{id}",
|
||||
controller=members_resource,
|
||||
action="delete",
|
||||
conditions={'method': ['DELETE']})
|
||||
mapper.connect("/shared-images/{id}",
|
||||
controller=members_resource,
|
||||
action="index_shared_images")
|
||||
|
||||
|
||||
class API(wsgi.Router):
|
||||
"""WSGI entry point for all Registry requests."""
|
||||
|
||||
def __init__(self, mapper):
|
||||
mapper = mapper or wsgi.APIMapper()
|
||||
|
||||
init(mapper)
|
||||
|
||||
super(API, self).__init__(mapper)
|
@ -1,569 +0,0 @@
|
||||
# Copyright 2010-2011 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.
|
||||
|
||||
"""
|
||||
Reference implementation registry server WSGI controller
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import strutils
|
||||
from oslo_utils import uuidutils
|
||||
from webob import exc
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import timeutils
|
||||
from glance.common import utils
|
||||
from glance.common import wsgi
|
||||
import glance.db
|
||||
from glance.i18n import _, _LE, _LI, _LW
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
DISPLAY_FIELDS_IN_INDEX = ['id', 'name', 'size',
|
||||
'disk_format', 'container_format',
|
||||
'checksum']
|
||||
|
||||
SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
|
||||
'min_ram', 'min_disk', 'size_min', 'size_max',
|
||||
'changes-since', 'protected']
|
||||
|
||||
SUPPORTED_SORT_KEYS = ('name', 'status', 'container_format', 'disk_format',
|
||||
'size', 'id', 'created_at', 'updated_at')
|
||||
|
||||
SUPPORTED_SORT_DIRS = ('asc', 'desc')
|
||||
|
||||
SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir')
|
||||
|
||||
|
||||
def _normalize_image_location_for_db(image_data):
|
||||
"""
|
||||
This function takes the legacy locations field and the newly added
|
||||
location_data field from the image_data values dictionary which flows
|
||||
over the wire between the registry and API servers and converts it
|
||||
into the location_data format only which is then consumable by the
|
||||
Image object.
|
||||
|
||||
:param image_data: a dict of values representing information in the image
|
||||
:returns: a new image data dict
|
||||
"""
|
||||
if 'locations' not in image_data and 'location_data' not in image_data:
|
||||
image_data['locations'] = None
|
||||
return image_data
|
||||
|
||||
locations = image_data.pop('locations', [])
|
||||
location_data = image_data.pop('location_data', [])
|
||||
|
||||
location_data_dict = {}
|
||||
for l in locations:
|
||||
location_data_dict[l] = {}
|
||||
for l in location_data:
|
||||
location_data_dict[l['url']] = {'metadata': l['metadata'],
|
||||
'status': l['status'],
|
||||
# Note(zhiyan): New location has no ID.
|
||||
'id': l['id'] if 'id' in l else None}
|
||||
|
||||
# NOTE(jbresnah) preserve original order. tests assume original order,
|
||||
# should that be defined functionality
|
||||
ordered_keys = locations[:]
|
||||
for ld in location_data:
|
||||
if ld['url'] not in ordered_keys:
|
||||
ordered_keys.append(ld['url'])
|
||||
|
||||
location_data = []
|
||||
for loc in ordered_keys:
|
||||
data = location_data_dict[loc]
|
||||
if data:
|
||||
location_data.append({'url': loc,
|
||||
'metadata': data['metadata'],
|
||||
'status': data['status'],
|
||||
'id': data['id']})
|
||||
else:
|
||||
location_data.append({'url': loc,
|
||||
'metadata': {},
|
||||
'status': 'active',
|
||||
'id': None})
|
||||
|
||||
image_data['locations'] = location_data
|
||||
return image_data
|
||||
|
||||
|
||||
class Controller(object):
|
||||
|
||||
def __init__(self):
|
||||
self.db_api = glance.db.get_api()
|
||||
|
||||
def _get_images(self, context, filters, **params):
|
||||
"""Get images, wrapping in exception if necessary."""
|
||||
# NOTE(markwash): for backwards compatibility, is_public=True for
|
||||
# admins actually means "treat me as if I'm not an admin and show me
|
||||
# all my images"
|
||||
if context.is_admin and params.get('is_public') is True:
|
||||
params['admin_as_user'] = True
|
||||
del params['is_public']
|
||||
try:
|
||||
return self.db_api.image_get_all(context, filters=filters,
|
||||
v1_mode=True, **params)
|
||||
except exception.ImageNotFound:
|
||||
LOG.warn(_LW("Invalid marker. Image %(id)s could not be "
|
||||
"found."), {'id': params.get('marker')})
|
||||
msg = _("Invalid marker. Image could not be found.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
except exception.Forbidden:
|
||||
LOG.warn(_LW("Access denied to image %(id)s but returning "
|
||||
"'not found'"), {'id': params.get('marker')})
|
||||
msg = _("Invalid marker. Image could not be found.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
except Exception:
|
||||
LOG.exception(_LE("Unable to get images"))
|
||||
raise
|
||||
|
||||
def index(self, req):
|
||||
"""Return a basic filtered list of public, non-deleted images
|
||||
|
||||
:param req: the Request object coming from the wsgi layer
|
||||
:returns: a mapping of the following form
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
dict(images=[image_list])
|
||||
|
||||
Where image_list is a sequence of mappings
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
'id': <ID>,
|
||||
'name': <NAME>,
|
||||
'size': <SIZE>,
|
||||
'disk_format': <DISK_FORMAT>,
|
||||
'container_format': <CONTAINER_FORMAT>,
|
||||
'checksum': <CHECKSUM>
|
||||
}
|
||||
|
||||
"""
|
||||
params = self._get_query_params(req)
|
||||
images = self._get_images(req.context, **params)
|
||||
|
||||
results = []
|
||||
for image in images:
|
||||
result = {}
|
||||
for field in DISPLAY_FIELDS_IN_INDEX:
|
||||
result[field] = image[field]
|
||||
results.append(result)
|
||||
|
||||
LOG.debug("Returning image list")
|
||||
return dict(images=results)
|
||||
|
||||
def detail(self, req):
|
||||
"""Return a filtered list of public, non-deleted images in detail
|
||||
|
||||
:param req: the Request object coming from the wsgi layer
|
||||
:returns: a mapping of the following form
|
||||
|
||||
::
|
||||
|
||||
{'images':
|
||||
[{
|
||||
'id': <ID>,
|
||||
'name': <NAME>,
|
||||
'size': <SIZE>,
|
||||
'disk_format': <DISK_FORMAT>,
|
||||
'container_format': <CONTAINER_FORMAT>,
|
||||
'checksum': <CHECKSUM>,
|
||||
'min_disk': <MIN_DISK>,
|
||||
'min_ram': <MIN_RAM>,
|
||||
'store': <STORE>,
|
||||
'status': <STATUS>,
|
||||
'created_at': <TIMESTAMP>,
|
||||
'updated_at': <TIMESTAMP>,
|
||||
'deleted_at': <TIMESTAMP>|<NONE>,
|
||||
'properties': {'distro': 'Ubuntu 10.04 LTS', {...}}
|
||||
}, {...}]
|
||||
}
|
||||
|
||||
"""
|
||||
params = self._get_query_params(req)
|
||||
|
||||
images = self._get_images(req.context, **params)
|
||||
image_dicts = [make_image_dict(i) for i in images]
|
||||
LOG.debug("Returning detailed image list")
|
||||
return dict(images=image_dicts)
|
||||
|
||||
def _get_query_params(self, req):
|
||||
"""Extract necessary query parameters from http request.
|
||||
|
||||
:param req: the Request object coming from the wsgi layer
|
||||
:returns: dictionary of filters to apply to list of images
|
||||
"""
|
||||
params = {
|
||||
'filters': self._get_filters(req),
|
||||
'limit': self._get_limit(req),
|
||||
'sort_key': [self._get_sort_key(req)],
|
||||
'sort_dir': [self._get_sort_dir(req)],
|
||||
'marker': self._get_marker(req),
|
||||
}
|
||||
|
||||
if req.context.is_admin:
|
||||
# Only admin gets to look for non-public images
|
||||
params['is_public'] = self._get_is_public(req)
|
||||
|
||||
# need to coy items because the params is modified in the loop body
|
||||
items = list(params.items())
|
||||
for key, value in items:
|
||||
if value is None:
|
||||
del params[key]
|
||||
|
||||
# Fix for LP Bug #1132294
|
||||
# Ensure all shared images are returned in v1
|
||||
params['member_status'] = 'all'
|
||||
return params
|
||||
|
||||
def _get_filters(self, req):
|
||||
"""Return a dictionary of query param filters from the request
|
||||
|
||||
:param req: the Request object coming from the wsgi layer
|
||||
:returns: a dict of key/value filters
|
||||
"""
|
||||
filters = {}
|
||||
properties = {}
|
||||
|
||||
for param in req.params:
|
||||
if param in SUPPORTED_FILTERS:
|
||||
filters[param] = req.params.get(param)
|
||||
if param.startswith('property-'):
|
||||
_param = param[9:]
|
||||
properties[_param] = req.params.get(param)
|
||||
|
||||
if 'changes-since' in filters:
|
||||
isotime = filters['changes-since']
|
||||
try:
|
||||
filters['changes-since'] = timeutils.parse_isotime(isotime)
|
||||
except ValueError:
|
||||
raise exc.HTTPBadRequest(_("Unrecognized changes-since value"))
|
||||
|
||||
if 'protected' in filters:
|
||||
value = self._get_bool(filters['protected'])
|
||||
if value is None:
|
||||
raise exc.HTTPBadRequest(_("protected must be True, or "
|
||||
"False"))
|
||||
|
||||
filters['protected'] = value
|
||||
|
||||
# only allow admins to filter on 'deleted'
|
||||
if req.context.is_admin:
|
||||
deleted_filter = self._parse_deleted_filter(req)
|
||||
if deleted_filter is not None:
|
||||
filters['deleted'] = deleted_filter
|
||||
elif 'changes-since' not in filters:
|
||||
filters['deleted'] = False
|
||||
elif 'changes-since' not in filters:
|
||||
filters['deleted'] = False
|
||||
|
||||
if properties:
|
||||
filters['properties'] = properties
|
||||
|
||||
return filters
|
||||
|
||||
def _get_limit(self, req):
|
||||
"""Parse a limit query param into something usable."""
|
||||
try:
|
||||
limit = int(req.params.get('limit', CONF.limit_param_default))
|
||||
except ValueError:
|
||||
raise exc.HTTPBadRequest(_("limit param must be an integer"))
|
||||
|
||||
if limit < 0:
|
||||
raise exc.HTTPBadRequest(_("limit param must be positive"))
|
||||
|
||||
return min(CONF.api_limit_max, limit)
|
||||
|
||||
def _get_marker(self, req):
|
||||
"""Parse a marker query param into something usable."""
|
||||
marker = req.params.get('marker')
|
||||
|
||||
if marker and not uuidutils.is_uuid_like(marker):
|
||||
msg = _('Invalid marker format')
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
return marker
|
||||
|
||||
def _get_sort_key(self, req):
|
||||
"""Parse a sort key query param from the request object."""
|
||||
sort_key = req.params.get('sort_key', 'created_at')
|
||||
if sort_key is not None and sort_key not in SUPPORTED_SORT_KEYS:
|
||||
_keys = ', '.join(SUPPORTED_SORT_KEYS)
|
||||
msg = _("Unsupported sort_key. Acceptable values: %s") % (_keys,)
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
return sort_key
|
||||
|
||||
def _get_sort_dir(self, req):
|
||||
"""Parse a sort direction query param from the request object."""
|
||||
sort_dir = req.params.get('sort_dir', 'desc')
|
||||
if sort_dir is not None and sort_dir not in SUPPORTED_SORT_DIRS:
|
||||
_keys = ', '.join(SUPPORTED_SORT_DIRS)
|
||||
msg = _("Unsupported sort_dir. Acceptable values: %s") % (_keys,)
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
return sort_dir
|
||||
|
||||
def _get_bool(self, value):
|
||||
value = value.lower()
|
||||
if value == 'true' or value == '1':
|
||||
return True
|
||||
elif value == 'false' or value == '0':
|
||||
return False
|
||||
|
||||
return None
|
||||
|
||||
def _get_is_public(self, req):
|
||||
"""Parse is_public into something usable."""
|
||||
is_public = req.params.get('is_public')
|
||||
|
||||
if is_public is None:
|
||||
# NOTE(vish): This preserves the default value of showing only
|
||||
# public images.
|
||||
return True
|
||||
elif is_public.lower() == 'none':
|
||||
return None
|
||||
|
||||
value = self._get_bool(is_public)
|
||||
if value is None:
|
||||
raise exc.HTTPBadRequest(_("is_public must be None, True, or "
|
||||
"False"))
|
||||
|
||||
return value
|
||||
|
||||
def _parse_deleted_filter(self, req):
|
||||
"""Parse deleted into something usable."""
|
||||
deleted = req.params.get('deleted')
|
||||
if deleted is None:
|
||||
return None
|
||||
return strutils.bool_from_string(deleted)
|
||||
|
||||
def show(self, req, id):
|
||||
"""Return data about the given image id."""
|
||||
try:
|
||||
image = self.db_api.image_get(req.context, id, v1_mode=True)
|
||||
LOG.debug("Successfully retrieved image %(id)s", {'id': id})
|
||||
except exception.ImageNotFound:
|
||||
LOG.info(_LI("Image %(id)s not found"), {'id': id})
|
||||
raise exc.HTTPNotFound()
|
||||
except exception.Forbidden:
|
||||
# If it's private and doesn't belong to them, don't let on
|
||||
# that it exists
|
||||
LOG.info(_LI("Access denied to image %(id)s but returning"
|
||||
" 'not found'"), {'id': id})
|
||||
raise exc.HTTPNotFound()
|
||||
except Exception:
|
||||
LOG.exception(_LE("Unable to show image %s"), id)
|
||||
raise
|
||||
|
||||
return dict(image=make_image_dict(image))
|
||||
|
||||
@utils.mutating
|
||||
def delete(self, req, id):
|
||||
"""Deletes an existing image with the registry.
|
||||
|
||||
:param req: wsgi Request object
|
||||
:param id: The opaque internal identifier for the image
|
||||
|
||||
:returns: 200 if delete was successful, a fault if not. On
|
||||
success, the body contains the deleted image
|
||||
information as a mapping.
|
||||
"""
|
||||
try:
|
||||
deleted_image = self.db_api.image_destroy(req.context, id)
|
||||
LOG.info(_LI("Successfully deleted image %(id)s"), {'id': id})
|
||||
return dict(image=make_image_dict(deleted_image))
|
||||
except exception.ForbiddenPublicImage:
|
||||
LOG.info(_LI("Delete denied for public image %(id)s"), {'id': id})
|
||||
raise exc.HTTPForbidden()
|
||||
except exception.Forbidden:
|
||||
# If it's private and doesn't belong to them, don't let on
|
||||
# that it exists
|
||||
LOG.info(_LI("Access denied to image %(id)s but returning"
|
||||
" 'not found'"), {'id': id})
|
||||
return exc.HTTPNotFound()
|
||||
except exception.ImageNotFound:
|
||||
LOG.info(_LI("Image %(id)s not found"), {'id': id})
|
||||
return exc.HTTPNotFound()
|
||||
except Exception:
|
||||
LOG.exception(_LE("Unable to delete image %s"), id)
|
||||
raise
|
||||
|
||||
@utils.mutating
|
||||
def create(self, req, body):
|
||||
"""Registers a new image with the registry.
|
||||
|
||||
:param req: wsgi Request object
|
||||
:param body: Dictionary of information about the image
|
||||
|
||||
:returns: The newly-created image information as a mapping,
|
||||
which will include the newly-created image's internal id
|
||||
in the 'id' field
|
||||
"""
|
||||
image_data = body['image']
|
||||
|
||||
# Ensure the image has a status set
|
||||
image_data.setdefault('status', 'active')
|
||||
|
||||
# Set up the image owner
|
||||
if not req.context.is_admin or 'owner' not in image_data:
|
||||
image_data['owner'] = req.context.owner
|
||||
|
||||
image_id = image_data.get('id')
|
||||
if image_id and not uuidutils.is_uuid_like(image_id):
|
||||
LOG.info(_LI("Rejecting image creation request for invalid image "
|
||||
"id '%(bad_id)s'"), {'bad_id': image_id})
|
||||
msg = _("Invalid image id format")
|
||||
return exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
if 'location' in image_data:
|
||||
image_data['locations'] = [image_data.pop('location')]
|
||||
|
||||
try:
|
||||
image_data = _normalize_image_location_for_db(image_data)
|
||||
image_data = self.db_api.image_create(req.context, image_data,
|
||||
v1_mode=True)
|
||||
image_data = dict(image=make_image_dict(image_data))
|
||||
LOG.info(_LI("Successfully created image %(id)s"),
|
||||
{'id': image_data['image']['id']})
|
||||
return image_data
|
||||
except exception.Duplicate:
|
||||
msg = _("Image with identifier %s already exists!") % image_id
|
||||
LOG.warn(msg)
|
||||
return exc.HTTPConflict(msg)
|
||||
except exception.Invalid as e:
|
||||
msg = (_("Failed to add image metadata. "
|
||||
"Got error: %s") % encodeutils.exception_to_unicode(e))
|
||||
LOG.error(msg)
|
||||
return exc.HTTPBadRequest(msg)
|
||||
except Exception:
|
||||
LOG.exception(_LE("Unable to create image %s"), image_id)
|
||||
raise
|
||||
|
||||
@utils.mutating
|
||||
def update(self, req, id, body):
|
||||
"""Updates an existing image with the registry.
|
||||
|
||||
:param req: wsgi Request object
|
||||
:param body: Dictionary of information about the image
|
||||
:param id: The opaque internal identifier for the image
|
||||
|
||||
:returns: Returns the updated image information as a mapping,
|
||||
"""
|
||||
image_data = body['image']
|
||||
from_state = body.get('from_state')
|
||||
|
||||
# Prohibit modification of 'owner'
|
||||
if not req.context.is_admin and 'owner' in image_data:
|
||||
del image_data['owner']
|
||||
|
||||
if 'location' in image_data:
|
||||
image_data['locations'] = [image_data.pop('location')]
|
||||
|
||||
purge_props = req.headers.get("X-Glance-Registry-Purge-Props", "false")
|
||||
try:
|
||||
# These fields hold sensitive data, which should not be printed in
|
||||
# the logs.
|
||||
sensitive_fields = ['locations', 'location_data']
|
||||
LOG.debug("Updating image %(id)s with metadata: %(image_data)r",
|
||||
{'id': id,
|
||||
'image_data': {k: v for k, v in image_data.items()
|
||||
if k not in sensitive_fields}})
|
||||
image_data = _normalize_image_location_for_db(image_data)
|
||||
if purge_props == "true":
|
||||
purge_props = True
|
||||
else:
|
||||
purge_props = False
|
||||
|
||||
updated_image = self.db_api.image_update(req.context, id,
|
||||
image_data,
|
||||
purge_props=purge_props,
|
||||
from_state=from_state,
|
||||
v1_mode=True)
|
||||
|
||||
LOG.info(_LI("Updating metadata for image %(id)s"), {'id': id})
|
||||
return dict(image=make_image_dict(updated_image))
|
||||
except exception.Invalid as e:
|
||||
msg = (_("Failed to update image metadata. "
|
||||
"Got error: %s") % encodeutils.exception_to_unicode(e))
|
||||
LOG.error(msg)
|
||||
return exc.HTTPBadRequest(msg)
|
||||
except exception.ImageNotFound:
|
||||
LOG.info(_LI("Image %(id)s not found"), {'id': id})
|
||||
raise exc.HTTPNotFound(body='Image not found',
|
||||
request=req,
|
||||
content_type='text/plain')
|
||||
except exception.ForbiddenPublicImage:
|
||||
LOG.info(_LI("Update denied for public image %(id)s"), {'id': id})
|
||||
raise exc.HTTPForbidden()
|
||||
except exception.Forbidden:
|
||||
# If it's private and doesn't belong to them, don't let on
|
||||
# that it exists
|
||||
LOG.info(_LI("Access denied to image %(id)s but returning"
|
||||
" 'not found'"), {'id': id})
|
||||
raise exc.HTTPNotFound(body='Image not found',
|
||||
request=req,
|
||||
content_type='text/plain')
|
||||
except exception.Conflict as e:
|
||||
LOG.info(encodeutils.exception_to_unicode(e))
|
||||
raise exc.HTTPConflict(body='Image operation conflicts',
|
||||
request=req,
|
||||
content_type='text/plain')
|
||||
except Exception:
|
||||
LOG.exception(_LE("Unable to update image %s"), id)
|
||||
raise
|
||||
|
||||
|
||||
def _limit_locations(image):
|
||||
locations = image.pop('locations', [])
|
||||
image['location_data'] = locations
|
||||
image['location'] = None
|
||||
for loc in locations:
|
||||
if loc['status'] == 'active':
|
||||
image['location'] = loc['url']
|
||||
break
|
||||
|
||||
|
||||
def make_image_dict(image):
|
||||
"""Create a dict representation of an image which we can use to
|
||||
serialize the image.
|
||||
"""
|
||||
|
||||
def _fetch_attrs(d, attrs):
|
||||
return {a: d[a] for a in attrs if a in d.keys()}
|
||||
|
||||
# TODO(sirp): should this be a dict, or a list of dicts?
|
||||
# A plain dict is more convenient, but list of dicts would provide
|
||||
# access to created_at, etc
|
||||
properties = {p['name']: p['value'] for p in image['properties']
|
||||
if not p['deleted']}
|
||||
|
||||
image_dict = _fetch_attrs(image, glance.db.IMAGE_ATTRS)
|
||||
image_dict['properties'] = properties
|
||||
_limit_locations(image_dict)
|
||||
|
||||
return image_dict
|
||||
|
||||
|
||||
def create_resource():
|
||||
"""Images resource factory method."""
|
||||
deserializer = wsgi.JSONRequestDeserializer()
|
||||
serializer = wsgi.JSONResponseSerializer()
|
||||
return wsgi.Resource(Controller(), deserializer, serializer)
|
@ -1,366 +0,0 @@
|
||||
# Copyright 2010-2011 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 oslo_log import log as logging
|
||||
from oslo_utils import encodeutils
|
||||
import webob.exc
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
from glance.common import wsgi
|
||||
import glance.db
|
||||
from glance.i18n import _, _LI, _LW
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Controller(object):
|
||||
|
||||
def _check_can_access_image_members(self, context):
|
||||
if context.owner is None and not context.is_admin:
|
||||
raise webob.exc.HTTPUnauthorized(_("No authenticated user"))
|
||||
|
||||
def __init__(self):
|
||||
self.db_api = glance.db.get_api()
|
||||
|
||||
def is_image_sharable(self, context, image):
|
||||
"""Return True if the image can be shared to others in this context."""
|
||||
# Is admin == image sharable
|
||||
if context.is_admin:
|
||||
return True
|
||||
|
||||
# Only allow sharing if we have an owner
|
||||
if context.owner is None:
|
||||
return False
|
||||
|
||||
# If we own the image, we can share it
|
||||
if context.owner == image['owner']:
|
||||
return True
|
||||
|
||||
members = self.db_api.image_member_find(context,
|
||||
image_id=image['id'],
|
||||
member=context.owner)
|
||||
if members:
|
||||
return members[0]['can_share']
|
||||
|
||||
return False
|
||||
|
||||
def index(self, req, image_id):
|
||||
"""
|
||||
Get the members of an image.
|
||||
"""
|
||||
try:
|
||||
self.db_api.image_get(req.context, image_id, v1_mode=True)
|
||||
except exception.NotFound:
|
||||
msg = _("Image %(id)s not found") % {'id': image_id}
|
||||
LOG.warn(msg)
|
||||
raise webob.exc.HTTPNotFound(msg)
|
||||
except exception.Forbidden:
|
||||
# If it's private and doesn't belong to them, don't let on
|
||||
# that it exists
|
||||
msg = _LW("Access denied to image %(id)s but returning"
|
||||
" 'not found'") % {'id': image_id}
|
||||
LOG.warn(msg)
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
members = self.db_api.image_member_find(req.context, image_id=image_id)
|
||||
LOG.debug("Returning member list for image %(id)s", {'id': image_id})
|
||||
return dict(members=make_member_list(members,
|
||||
member_id='member',
|
||||
can_share='can_share'))
|
||||
|
||||
@utils.mutating
|
||||
def update_all(self, req, image_id, body):
|
||||
"""
|
||||
Replaces the members of the image with those specified in the
|
||||
body. The body is a dict with the following format::
|
||||
|
||||
{'memberships': [
|
||||
{'member_id': <MEMBER_ID>,
|
||||
['can_share': [True|False]]}, ...
|
||||
]}
|
||||
"""
|
||||
self._check_can_access_image_members(req.context)
|
||||
|
||||
# Make sure the image exists
|
||||
try:
|
||||
image = self.db_api.image_get(req.context, image_id, v1_mode=True)
|
||||
except exception.NotFound:
|
||||
msg = _("Image %(id)s not found") % {'id': image_id}
|
||||
LOG.warn(msg)
|
||||
raise webob.exc.HTTPNotFound(msg)
|
||||
except exception.Forbidden:
|
||||
# If it's private and doesn't belong to them, don't let on
|
||||
# that it exists
|
||||
msg = _LW("Access denied to image %(id)s but returning"
|
||||
" 'not found'") % {'id': image_id}
|
||||
LOG.warn(msg)
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
# Can they manipulate the membership?
|
||||
if not self.is_image_sharable(req.context, image):
|
||||
msg = (_LW("User lacks permission to share image %(id)s") %
|
||||
{'id': image_id})
|
||||
LOG.warn(msg)
|
||||
msg = _("No permission to share that image")
|
||||
raise webob.exc.HTTPForbidden(msg)
|
||||
|
||||
# Get the membership list
|
||||
try:
|
||||
memb_list = body['memberships']
|
||||
except Exception as e:
|
||||
# Malformed entity...
|
||||
msg = _LW("Invalid membership association specified for "
|
||||
"image %(id)s") % {'id': image_id}
|
||||
LOG.warn(msg)
|
||||
msg = (_("Invalid membership association: %s") %
|
||||
encodeutils.exception_to_unicode(e))
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
add = []
|
||||
existing = {}
|
||||
# Walk through the incoming memberships
|
||||
for memb in memb_list:
|
||||
try:
|
||||
datum = dict(image_id=image['id'],
|
||||
member=memb['member_id'],
|
||||
can_share=None)
|
||||
except Exception as e:
|
||||
# Malformed entity...
|
||||
msg = _LW("Invalid membership association specified for "
|
||||
"image %(id)s") % {'id': image_id}
|
||||
LOG.warn(msg)
|
||||
msg = (_("Invalid membership association: %s") %
|
||||
encodeutils.exception_to_unicode(e))
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
# Figure out what can_share should be
|
||||
if 'can_share' in memb:
|
||||
datum['can_share'] = bool(memb['can_share'])
|
||||
|
||||
# Try to find the corresponding membership
|
||||
members = self.db_api.image_member_find(req.context,
|
||||
image_id=datum['image_id'],
|
||||
member=datum['member'],
|
||||
include_deleted=True)
|
||||
try:
|
||||
member = members[0]
|
||||
except IndexError:
|
||||
# Default can_share
|
||||
datum['can_share'] = bool(datum['can_share'])
|
||||
add.append(datum)
|
||||
else:
|
||||
# Are we overriding can_share?
|
||||
if datum['can_share'] is None:
|
||||
datum['can_share'] = members[0]['can_share']
|
||||
|
||||
existing[member['id']] = {
|
||||
'values': datum,
|
||||
'membership': member,
|
||||
}
|
||||
|
||||
# We now have a filtered list of memberships to add and
|
||||
# memberships to modify. Let's start by walking through all
|
||||
# the existing image memberships...
|
||||
existing_members = self.db_api.image_member_find(req.context,
|
||||
image_id=image['id'],
|
||||
include_deleted=True)
|
||||
for member in existing_members:
|
||||
if member['id'] in existing:
|
||||
# Just update the membership in place
|
||||
update = existing[member['id']]['values']
|
||||
self.db_api.image_member_update(req.context,
|
||||
member['id'],
|
||||
update)
|
||||
else:
|
||||
if not member['deleted']:
|
||||
# Outdated one; needs to be deleted
|
||||
self.db_api.image_member_delete(req.context, member['id'])
|
||||
|
||||
# Now add the non-existent ones
|
||||
for memb in add:
|
||||
self.db_api.image_member_create(req.context, memb)
|
||||
|
||||
# Make an appropriate result
|
||||
LOG.info(_LI("Successfully updated memberships for image %(id)s"),
|
||||
{'id': image_id})
|
||||
return webob.exc.HTTPNoContent()
|
||||
|
||||
@utils.mutating
|
||||
def update(self, req, image_id, id, body=None):
|
||||
"""
|
||||
Adds a membership to the image, or updates an existing one.
|
||||
If a body is present, it is a dict with the following format::
|
||||
|
||||
{'member': {
|
||||
'can_share': [True|False]
|
||||
}}
|
||||
|
||||
If `can_share` is provided, the member's ability to share is
|
||||
set accordingly. If it is not provided, existing memberships
|
||||
remain unchanged and new memberships default to False.
|
||||
"""
|
||||
self._check_can_access_image_members(req.context)
|
||||
|
||||
# Make sure the image exists
|
||||
try:
|
||||
image = self.db_api.image_get(req.context, image_id, v1_mode=True)
|
||||
except exception.NotFound:
|
||||
msg = _("Image %(id)s not found") % {'id': image_id}
|
||||
LOG.warn(msg)
|
||||
raise webob.exc.HTTPNotFound(msg)
|
||||
except exception.Forbidden:
|
||||
# If it's private and doesn't belong to them, don't let on
|
||||
# that it exists
|
||||
msg = _LW("Access denied to image %(id)s but returning"
|
||||
" 'not found'") % {'id': image_id}
|
||||
LOG.warn(msg)
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
# Can they manipulate the membership?
|
||||
if not self.is_image_sharable(req.context, image):
|
||||
msg = (_LW("User lacks permission to share image %(id)s") %
|
||||
{'id': image_id})
|
||||
LOG.warn(msg)
|
||||
msg = _("No permission to share that image")
|
||||
raise webob.exc.HTTPForbidden(msg)
|
||||
|
||||
# Determine the applicable can_share value
|
||||
can_share = None
|
||||
if body:
|
||||
try:
|
||||
can_share = bool(body['member']['can_share'])
|
||||
except Exception as e:
|
||||
# Malformed entity...
|
||||
msg = _LW("Invalid membership association specified for "
|
||||
"image %(id)s") % {'id': image_id}
|
||||
LOG.warn(msg)
|
||||
msg = (_("Invalid membership association: %s") %
|
||||
encodeutils.exception_to_unicode(e))
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
# Look up an existing membership...
|
||||
members = self.db_api.image_member_find(req.context,
|
||||
image_id=image_id,
|
||||
member=id,
|
||||
include_deleted=True)
|
||||
if members:
|
||||
if can_share is not None:
|
||||
values = dict(can_share=can_share)
|
||||
self.db_api.image_member_update(req.context,
|
||||
members[0]['id'],
|
||||
values)
|
||||
else:
|
||||
values = dict(image_id=image['id'], member=id,
|
||||
can_share=bool(can_share))
|
||||
self.db_api.image_member_create(req.context, values)
|
||||
|
||||
LOG.info(_LI("Successfully updated a membership for image %(id)s"),
|
||||
{'id': image_id})
|
||||
return webob.exc.HTTPNoContent()
|
||||
|
||||
@utils.mutating
|
||||
def delete(self, req, image_id, id):
|
||||
"""
|
||||
Removes a membership from the image.
|
||||
"""
|
||||
self._check_can_access_image_members(req.context)
|
||||
|
||||
# Make sure the image exists
|
||||
try:
|
||||
image = self.db_api.image_get(req.context, image_id, v1_mode=True)
|
||||
except exception.NotFound:
|
||||
msg = _("Image %(id)s not found") % {'id': image_id}
|
||||
LOG.warn(msg)
|
||||
raise webob.exc.HTTPNotFound(msg)
|
||||
except exception.Forbidden:
|
||||
# If it's private and doesn't belong to them, don't let on
|
||||
# that it exists
|
||||
msg = _LW("Access denied to image %(id)s but returning"
|
||||
" 'not found'") % {'id': image_id}
|
||||
LOG.warn(msg)
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
# Can they manipulate the membership?
|
||||
if not self.is_image_sharable(req.context, image):
|
||||
msg = (_LW("User lacks permission to share image %(id)s") %
|
||||
{'id': image_id})
|
||||
LOG.warn(msg)
|
||||
msg = _("No permission to share that image")
|
||||
raise webob.exc.HTTPForbidden(msg)
|
||||
|
||||
# Look up an existing membership
|
||||
members = self.db_api.image_member_find(req.context,
|
||||
image_id=image_id,
|
||||
member=id)
|
||||
if members:
|
||||
self.db_api.image_member_delete(req.context, members[0]['id'])
|
||||
else:
|
||||
LOG.debug("%(id)s is not a member of image %(image_id)s",
|
||||
{'id': id, 'image_id': image_id})
|
||||
msg = _("Membership could not be found.")
|
||||
raise webob.exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
# Make an appropriate result
|
||||
LOG.info(_LI("Successfully deleted a membership from image %(id)s"),
|
||||
{'id': image_id})
|
||||
return webob.exc.HTTPNoContent()
|
||||
|
||||
def default(self, req, *args, **kwargs):
|
||||
"""This will cover the missing 'show' and 'create' actions"""
|
||||
LOG.debug("The method %s is not allowed for this resource",
|
||||
req.environ['REQUEST_METHOD'])
|
||||
raise webob.exc.HTTPMethodNotAllowed(
|
||||
headers=[('Allow', 'PUT, DELETE')])
|
||||
|
||||
def index_shared_images(self, req, id):
|
||||
"""
|
||||
Retrieves images shared with the given member.
|
||||
"""
|
||||
try:
|
||||
members = self.db_api.image_member_find(req.context, member=id)
|
||||
except exception.NotFound:
|
||||
msg = _LW("Member %(id)s not found") % {'id': id}
|
||||
LOG.warn(msg)
|
||||
msg = _("Membership could not be found.")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
LOG.debug("Returning list of images shared with member %(id)s",
|
||||
{'id': id})
|
||||
return dict(shared_images=make_member_list(members,
|
||||
image_id='image_id',
|
||||
can_share='can_share'))
|
||||
|
||||
|
||||
def make_member_list(members, **attr_map):
|
||||
"""
|
||||
Create a dict representation of a list of members which we can use
|
||||
to serialize the members list. Keyword arguments map the names of
|
||||
optional attributes to include to the database attribute.
|
||||
"""
|
||||
|
||||
def _fetch_memb(memb, attr_map):
|
||||
return {k: memb[v] for k, v in attr_map.items() if v in memb.keys()}
|
||||
|
||||
# Return the list of members with the given attribute mapping
|
||||
return [_fetch_memb(memb, attr_map) for memb in members]
|
||||
|
||||
|
||||
def create_resource():
|
||||
"""Image members resource factory method."""
|
||||
deserializer = wsgi.JSONRequestDeserializer()
|
||||
serializer = wsgi.JSONResponseSerializer()
|
||||
return wsgi.Resource(Controller(), deserializer, serializer)
|
@ -1,264 +0,0 @@
|
||||
# Copyright 2013 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 oslo_config import cfg
|
||||
|
||||
from glance.i18n import _
|
||||
|
||||
|
||||
registry_client_opts = [
|
||||
cfg.StrOpt('registry_client_protocol',
|
||||
default='http',
|
||||
choices=('http', 'https'),
|
||||
deprecated_for_removal=True,
|
||||
deprecated_since="Queens",
|
||||
deprecated_reason=_("""
|
||||
Glance registry service is deprecated for removal.
|
||||
|
||||
More information can be found from the spec:
|
||||
http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html
|
||||
"""),
|
||||
help=_("""
|
||||
Protocol to use for communication with the registry server.
|
||||
|
||||
Provide a string value representing the protocol to use for
|
||||
communication with the registry server. By default, this option is
|
||||
set to ``http`` and the connection is not secure.
|
||||
|
||||
This option can be set to ``https`` to establish a secure connection
|
||||
to the registry server. In this case, provide a key to use for the
|
||||
SSL connection using the ``registry_client_key_file`` option. Also
|
||||
include the CA file and cert file using the options
|
||||
``registry_client_ca_file`` and ``registry_client_cert_file``
|
||||
respectively.
|
||||
|
||||
Possible values:
|
||||
* http
|
||||
* https
|
||||
|
||||
Related options:
|
||||
* registry_client_key_file
|
||||
* registry_client_cert_file
|
||||
* registry_client_ca_file
|
||||
|
||||
""")),
|
||||
cfg.StrOpt('registry_client_key_file',
|
||||
sample_default='/etc/ssl/key/key-file.pem',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_since="Queens",
|
||||
deprecated_reason=_("""
|
||||
Glance registry service is deprecated for removal.
|
||||
|
||||
More information can be found from the spec:
|
||||
http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html
|
||||
"""),
|
||||
help=_("""
|
||||
Absolute path to the private key file.
|
||||
|
||||
Provide a string value representing a valid absolute path to the
|
||||
private key file to use for establishing a secure connection to
|
||||
the registry server.
|
||||
|
||||
NOTE: This option must be set if ``registry_client_protocol`` is
|
||||
set to ``https``. Alternatively, the GLANCE_CLIENT_KEY_FILE
|
||||
environment variable may be set to a filepath of the key file.
|
||||
|
||||
Possible values:
|
||||
* String value representing a valid absolute path to the key
|
||||
file.
|
||||
|
||||
Related options:
|
||||
* registry_client_protocol
|
||||
|
||||
""")),
|
||||
cfg.StrOpt('registry_client_cert_file',
|
||||
sample_default='/etc/ssl/certs/file.crt',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_since="Queens",
|
||||
deprecated_reason=_("""
|
||||
Glance registry service is deprecated for removal.
|
||||
|
||||
More information can be found from the spec:
|
||||
http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html
|
||||
"""),
|
||||
help=_("""
|
||||
Absolute path to the certificate file.
|
||||
|
||||
Provide a string value representing a valid absolute path to the
|
||||
certificate file to use for establishing a secure connection to
|
||||
the registry server.
|
||||
|
||||
NOTE: This option must be set if ``registry_client_protocol`` is
|
||||
set to ``https``. Alternatively, the GLANCE_CLIENT_CERT_FILE
|
||||
environment variable may be set to a filepath of the certificate
|
||||
file.
|
||||
|
||||
Possible values:
|
||||
* String value representing a valid absolute path to the
|
||||
certificate file.
|
||||
|
||||
Related options:
|
||||
* registry_client_protocol
|
||||
|
||||
""")),
|
||||
cfg.StrOpt('registry_client_ca_file',
|
||||
sample_default='/etc/ssl/cafile/file.ca',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_since="Queens",
|
||||
deprecated_reason=_("""
|
||||
Glance registry service is deprecated for removal.
|
||||
|
||||
More information can be found from the spec:
|
||||
http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html
|
||||
"""),
|
||||
help=_("""
|
||||
Absolute path to the Certificate Authority file.
|
||||
|
||||
Provide a string value representing a valid absolute path to the
|
||||
certificate authority file to use for establishing a secure
|
||||
connection to the registry server.
|
||||
|
||||
NOTE: This option must be set if ``registry_client_protocol`` is
|
||||
set to ``https``. Alternatively, the GLANCE_CLIENT_CA_FILE
|
||||
environment variable may be set to a filepath of the CA file.
|
||||
This option is ignored if the ``registry_client_insecure`` option
|
||||
is set to ``True``.
|
||||
|
||||
Possible values:
|
||||
* String value representing a valid absolute path to the CA
|
||||
file.
|
||||
|
||||
Related options:
|
||||
* registry_client_protocol
|
||||
* registry_client_insecure
|
||||
|
||||
""")),
|
||||
cfg.BoolOpt('registry_client_insecure',
|
||||
default=False,
|
||||
deprecated_for_removal=True,
|
||||
deprecated_since="Queens",
|
||||
deprecated_reason=_("""
|
||||
Glance registry service is deprecated for removal.
|
||||
|
||||
More information can be found from the spec:
|
||||
http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html
|
||||
"""),
|
||||
help=_("""
|
||||
Set verification of the registry server certificate.
|
||||
|
||||
Provide a boolean value to determine whether or not to validate
|
||||
SSL connections to the registry server. By default, this option
|
||||
is set to ``False`` and the SSL connections are validated.
|
||||
|
||||
If set to ``True``, the connection to the registry server is not
|
||||
validated via a certifying authority and the
|
||||
``registry_client_ca_file`` option is ignored. This is the
|
||||
registry's equivalent of specifying --insecure on the command line
|
||||
using glanceclient for the API.
|
||||
|
||||
Possible values:
|
||||
* True
|
||||
* False
|
||||
|
||||
Related options:
|
||||
* registry_client_protocol
|
||||
* registry_client_ca_file
|
||||
|
||||
""")),
|
||||
cfg.IntOpt('registry_client_timeout',
|
||||
default=600,
|
||||
min=0,
|
||||
deprecated_for_removal=True,
|
||||
deprecated_since="Queens",
|
||||
deprecated_reason=_("""
|
||||
Glance registry service is deprecated for removal.
|
||||
|
||||
More information can be found from the spec:
|
||||
http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html
|
||||
"""),
|
||||
help=_("""
|
||||
Timeout value for registry requests.
|
||||
|
||||
Provide an integer value representing the period of time in seconds
|
||||
that the API server will wait for a registry request to complete.
|
||||
The default value is 600 seconds.
|
||||
|
||||
A value of 0 implies that a request will never timeout.
|
||||
|
||||
Possible values:
|
||||
* Zero
|
||||
* Positive integer
|
||||
|
||||
Related options:
|
||||
* None
|
||||
|
||||
""")),
|
||||
]
|
||||
|
||||
_DEPRECATE_USE_USER_TOKEN_MSG = ('This option was considered harmful and '
|
||||
'has been deprecated in M release. It will '
|
||||
'be removed in O release. For more '
|
||||
'information read OSSN-0060. '
|
||||
'Related functionality with uploading big '
|
||||
'images has been implemented with Keystone '
|
||||
'trusts support.')
|
||||
|
||||
registry_client_ctx_opts = [
|
||||
cfg.BoolOpt('use_user_token', default=True, deprecated_for_removal=True,
|
||||
deprecated_reason=_DEPRECATE_USE_USER_TOKEN_MSG,
|
||||
help=_('Whether to pass through the user token when '
|
||||
'making requests to the registry. To prevent '
|
||||
'failures with token expiration during big '
|
||||
'files upload, it is recommended to set this '
|
||||
'parameter to False.'
|
||||
'If "use_user_token" is not in effect, then '
|
||||
'admin credentials can be specified.')),
|
||||
cfg.StrOpt('admin_user', secret=True, deprecated_for_removal=True,
|
||||
deprecated_reason=_DEPRECATE_USE_USER_TOKEN_MSG,
|
||||
help=_('The administrators user name. '
|
||||
'If "use_user_token" is not in effect, then '
|
||||
'admin credentials can be specified.')),
|
||||
cfg.StrOpt('admin_password', secret=True, deprecated_for_removal=True,
|
||||
deprecated_reason=_DEPRECATE_USE_USER_TOKEN_MSG,
|
||||
help=_('The administrators password. '
|
||||
'If "use_user_token" is not in effect, then '
|
||||
'admin credentials can be specified.')),
|
||||
cfg.StrOpt('admin_tenant_name', secret=True, deprecated_for_removal=True,
|
||||
deprecated_reason=_DEPRECATE_USE_USER_TOKEN_MSG,
|
||||
help=_('The tenant name of the administrative user. '
|
||||
'If "use_user_token" is not in effect, then '
|
||||
'admin tenant name can be specified.')),
|
||||
cfg.StrOpt('auth_url', deprecated_for_removal=True,
|
||||
deprecated_reason=_DEPRECATE_USE_USER_TOKEN_MSG,
|
||||
help=_('The URL to the keystone service. '
|
||||
'If "use_user_token" is not in effect and '
|
||||
'using keystone auth, then URL of keystone '
|
||||
'can be specified.')),
|
||||
cfg.StrOpt('auth_strategy', default='noauth', deprecated_for_removal=True,
|
||||
deprecated_reason=_DEPRECATE_USE_USER_TOKEN_MSG,
|
||||
help=_('The strategy to use for authentication. '
|
||||
'If "use_user_token" is not in effect, then '
|
||||
'auth strategy can be specified.')),
|
||||
cfg.StrOpt('auth_region', deprecated_for_removal=True,
|
||||
deprecated_reason=_DEPRECATE_USE_USER_TOKEN_MSG,
|
||||
help=_('The region for the authentication service. '
|
||||
'If "use_user_token" is not in effect and '
|
||||
'using keystone auth, then region name can '
|
||||
'be specified.')),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(registry_client_opts)
|
||||
CONF.register_opts(registry_client_ctx_opts)
|
@ -1,227 +0,0 @@
|
||||
# Copyright 2010-2011 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.
|
||||
|
||||
"""
|
||||
Registry's Client API
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from glance.common import exception
|
||||
from glance.i18n import _
|
||||
from glance.registry.client.v1 import client
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
registry_client_ctx_opts = [
|
||||
cfg.BoolOpt('send_identity_headers',
|
||||
default=False,
|
||||
help=_("""
|
||||
Send headers received from identity when making requests to
|
||||
registry.
|
||||
|
||||
Typically, Glance registry can be deployed in multiple flavors,
|
||||
which may or may not include authentication. For example,
|
||||
``trusted-auth`` is a flavor that does not require the registry
|
||||
service to authenticate the requests it receives. However, the
|
||||
registry service may still need a user context to be populated to
|
||||
serve the requests. This can be achieved by the caller
|
||||
(the Glance API usually) passing through the headers it received
|
||||
from authenticating with identity for the same request. The typical
|
||||
headers sent are ``X-User-Id``, ``X-Tenant-Id``, ``X-Roles``,
|
||||
``X-Identity-Status`` and ``X-Service-Catalog``.
|
||||
|
||||
Provide a boolean value to determine whether to send the identity
|
||||
headers to provide tenant and user information along with the
|
||||
requests to registry service. By default, this option is set to
|
||||
``False``, which means that user and tenant information is not
|
||||
available readily. It must be obtained by authenticating. Hence, if
|
||||
this is set to ``False``, ``flavor`` must be set to value that
|
||||
either includes authentication or authenticated user context.
|
||||
|
||||
Possible values:
|
||||
* True
|
||||
* False
|
||||
|
||||
Related options:
|
||||
* flavor
|
||||
|
||||
""")),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(registry_client_ctx_opts)
|
||||
_registry_client = 'glance.registry.client'
|
||||
CONF.import_opt('registry_client_protocol', _registry_client)
|
||||
CONF.import_opt('registry_client_key_file', _registry_client)
|
||||
CONF.import_opt('registry_client_cert_file', _registry_client)
|
||||
CONF.import_opt('registry_client_ca_file', _registry_client)
|
||||
CONF.import_opt('registry_client_insecure', _registry_client)
|
||||
CONF.import_opt('registry_client_timeout', _registry_client)
|
||||
CONF.import_opt('use_user_token', _registry_client)
|
||||
CONF.import_opt('admin_user', _registry_client)
|
||||
CONF.import_opt('admin_password', _registry_client)
|
||||
CONF.import_opt('admin_tenant_name', _registry_client)
|
||||
CONF.import_opt('auth_url', _registry_client)
|
||||
CONF.import_opt('auth_strategy', _registry_client)
|
||||
CONF.import_opt('auth_region', _registry_client)
|
||||
CONF.import_opt('metadata_encryption_key', 'glance.common.config')
|
||||
|
||||
_CLIENT_CREDS = None
|
||||
_CLIENT_HOST = None
|
||||
_CLIENT_PORT = None
|
||||
_CLIENT_KWARGS = {}
|
||||
# AES key used to encrypt 'location' metadata
|
||||
_METADATA_ENCRYPTION_KEY = None
|
||||
|
||||
|
||||
def configure_registry_client():
|
||||
"""
|
||||
Sets up a registry client for use in registry lookups
|
||||
"""
|
||||
global _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT, _METADATA_ENCRYPTION_KEY
|
||||
try:
|
||||
host, port = CONF.registry_host, CONF.registry_port
|
||||
except cfg.ConfigFileValueError:
|
||||
msg = _("Configuration option was not valid")
|
||||
LOG.error(msg)
|
||||
raise exception.BadRegistryConnectionConfiguration(reason=msg)
|
||||
except IndexError:
|
||||
msg = _("Could not find required configuration option")
|
||||
LOG.error(msg)
|
||||
raise exception.BadRegistryConnectionConfiguration(reason=msg)
|
||||
|
||||
_CLIENT_HOST = host
|
||||
_CLIENT_PORT = port
|
||||
_METADATA_ENCRYPTION_KEY = CONF.metadata_encryption_key
|
||||
_CLIENT_KWARGS = {
|
||||
'use_ssl': CONF.registry_client_protocol.lower() == 'https',
|
||||
'key_file': CONF.registry_client_key_file,
|
||||
'cert_file': CONF.registry_client_cert_file,
|
||||
'ca_file': CONF.registry_client_ca_file,
|
||||
'insecure': CONF.registry_client_insecure,
|
||||
'timeout': CONF.registry_client_timeout,
|
||||
}
|
||||
|
||||
if not CONF.use_user_token:
|
||||
configure_registry_admin_creds()
|
||||
|
||||
|
||||
def configure_registry_admin_creds():
|
||||
global _CLIENT_CREDS
|
||||
|
||||
if CONF.auth_url or os.getenv('OS_AUTH_URL'):
|
||||
strategy = 'keystone'
|
||||
else:
|
||||
strategy = CONF.auth_strategy
|
||||
|
||||
_CLIENT_CREDS = {
|
||||
'user': CONF.admin_user,
|
||||
'password': CONF.admin_password,
|
||||
'username': CONF.admin_user,
|
||||
'tenant': CONF.admin_tenant_name,
|
||||
'auth_url': os.getenv('OS_AUTH_URL') or CONF.auth_url,
|
||||
'strategy': strategy,
|
||||
'region': CONF.auth_region,
|
||||
}
|
||||
|
||||
|
||||
def get_registry_client(cxt):
|
||||
global _CLIENT_CREDS, _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT
|
||||
global _METADATA_ENCRYPTION_KEY
|
||||
kwargs = _CLIENT_KWARGS.copy()
|
||||
if CONF.use_user_token:
|
||||
kwargs['auth_token'] = cxt.auth_token
|
||||
if _CLIENT_CREDS:
|
||||
kwargs['creds'] = _CLIENT_CREDS
|
||||
|
||||
if CONF.send_identity_headers:
|
||||
identity_headers = {
|
||||
'X-User-Id': cxt.user_id or '',
|
||||
'X-Tenant-Id': cxt.project_id or '',
|
||||
'X-Roles': ','.join(cxt.roles),
|
||||
'X-Identity-Status': 'Confirmed',
|
||||
'X-Service-Catalog': jsonutils.dumps(cxt.service_catalog),
|
||||
}
|
||||
kwargs['identity_headers'] = identity_headers
|
||||
|
||||
kwargs['request_id'] = cxt.request_id
|
||||
|
||||
return client.RegistryClient(_CLIENT_HOST, _CLIENT_PORT,
|
||||
_METADATA_ENCRYPTION_KEY, **kwargs)
|
||||
|
||||
|
||||
def get_images_list(context, **kwargs):
|
||||
c = get_registry_client(context)
|
||||
return c.get_images(**kwargs)
|
||||
|
||||
|
||||
def get_images_detail(context, **kwargs):
|
||||
c = get_registry_client(context)
|
||||
return c.get_images_detailed(**kwargs)
|
||||
|
||||
|
||||
def get_image_metadata(context, image_id):
|
||||
c = get_registry_client(context)
|
||||
return c.get_image(image_id)
|
||||
|
||||
|
||||
def add_image_metadata(context, image_meta):
|
||||
LOG.debug("Adding image metadata...")
|
||||
c = get_registry_client(context)
|
||||
return c.add_image(image_meta)
|
||||
|
||||
|
||||
def update_image_metadata(context, image_id, image_meta,
|
||||
purge_props=False, from_state=None):
|
||||
LOG.debug("Updating image metadata for image %s...", image_id)
|
||||
c = get_registry_client(context)
|
||||
return c.update_image(image_id, image_meta, purge_props=purge_props,
|
||||
from_state=from_state)
|
||||
|
||||
|
||||
def delete_image_metadata(context, image_id):
|
||||
LOG.debug("Deleting image metadata for image %s...", image_id)
|
||||
c = get_registry_client(context)
|
||||
return c.delete_image(image_id)
|
||||
|
||||
|
||||
def get_image_members(context, image_id):
|
||||
c = get_registry_client(context)
|
||||
return c.get_image_members(image_id)
|
||||
|
||||
|
||||
def get_member_images(context, member_id):
|
||||
c = get_registry_client(context)
|
||||
return c.get_member_images(member_id)
|
||||
|
||||
|
||||
def replace_members(context, image_id, member_data):
|
||||
c = get_registry_client(context)
|
||||
return c.replace_members(image_id, member_data)
|
||||
|
||||
|
||||
def add_member(context, image_id, member_id, can_share=None):
|
||||
c = get_registry_client(context)
|
||||
return c.add_member(image_id, member_id, can_share=can_share)
|
||||
|
||||
|
||||
def delete_member(context, image_id, member_id):
|
||||
c = get_registry_client(context)
|
||||
return c.delete_member(image_id, member_id)
|
@ -1,276 +0,0 @@
|
||||
# Copyright 2013 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.
|
||||
|
||||
"""
|
||||
Simple client class to speak with any RESTful service that implements
|
||||
the Glance Registry API
|
||||
"""
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import excutils
|
||||
import six
|
||||
|
||||
from glance.common.client import BaseClient
|
||||
from glance.common import crypt
|
||||
from glance.common import exception
|
||||
from glance.i18n import _LE
|
||||
from glance.registry.api.v1 import images
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RegistryClient(BaseClient):
|
||||
|
||||
"""A client for the Registry image metadata service."""
|
||||
|
||||
DEFAULT_PORT = 9191
|
||||
|
||||
def __init__(self, host=None, port=None, metadata_encryption_key=None,
|
||||
identity_headers=None, **kwargs):
|
||||
"""
|
||||
:param metadata_encryption_key: Key used to encrypt 'location' metadata
|
||||
"""
|
||||
self.metadata_encryption_key = metadata_encryption_key
|
||||
# NOTE (dprince): by default base client overwrites host and port
|
||||
# settings when using keystone. configure_via_auth=False disables
|
||||
# this behaviour to ensure we still send requests to the Registry API
|
||||
self.identity_headers = identity_headers
|
||||
# store available passed request id for do_request call
|
||||
self._passed_request_id = kwargs.pop('request_id', None)
|
||||
BaseClient.__init__(self, host, port, configure_via_auth=False,
|
||||
**kwargs)
|
||||
|
||||
def decrypt_metadata(self, image_metadata):
|
||||
if self.metadata_encryption_key:
|
||||
if image_metadata.get('location'):
|
||||
location = crypt.urlsafe_decrypt(self.metadata_encryption_key,
|
||||
image_metadata['location'])
|
||||
image_metadata['location'] = location
|
||||
if image_metadata.get('location_data'):
|
||||
ld = []
|
||||
for loc in image_metadata['location_data']:
|
||||
url = crypt.urlsafe_decrypt(self.metadata_encryption_key,
|
||||
loc['url'])
|
||||
ld.append({'id': loc['id'], 'url': url,
|
||||
'metadata': loc['metadata'],
|
||||
'status': loc['status']})
|
||||
image_metadata['location_data'] = ld
|
||||
return image_metadata
|
||||
|
||||
def encrypt_metadata(self, image_metadata):
|
||||
if self.metadata_encryption_key:
|
||||
location_url = image_metadata.get('location')
|
||||
if location_url:
|
||||
location = crypt.urlsafe_encrypt(self.metadata_encryption_key,
|
||||
location_url,
|
||||
64)
|
||||
image_metadata['location'] = location
|
||||
if image_metadata.get('location_data'):
|
||||
ld = []
|
||||
for loc in image_metadata['location_data']:
|
||||
if loc['url'] == location_url:
|
||||
url = location
|
||||
else:
|
||||
url = crypt.urlsafe_encrypt(
|
||||
self.metadata_encryption_key, loc['url'], 64)
|
||||
ld.append({'url': url, 'metadata': loc['metadata'],
|
||||
'status': loc['status'],
|
||||
# NOTE(zhiyan): New location has no ID field.
|
||||
'id': loc.get('id')})
|
||||
image_metadata['location_data'] = ld
|
||||
return image_metadata
|
||||
|
||||
def get_images(self, **kwargs):
|
||||
"""
|
||||
Returns a list of image id/name mappings from Registry
|
||||
|
||||
:param filters: dict of keys & expected values to filter results
|
||||
:param marker: image id after which to start page
|
||||
:param limit: max number of images to return
|
||||
:param sort_key: results will be ordered by this image attribute
|
||||
:param sort_dir: direction in which to order results (asc, desc)
|
||||
"""
|
||||
params = self._extract_params(kwargs, images.SUPPORTED_PARAMS)
|
||||
res = self.do_request("GET", "/images", params=params)
|
||||
image_list = jsonutils.loads(res.read())['images']
|
||||
for image in image_list:
|
||||
image = self.decrypt_metadata(image)
|
||||
return image_list
|
||||
|
||||
def do_request(self, method, action, **kwargs):
|
||||
try:
|
||||
kwargs['headers'] = kwargs.get('headers', {})
|
||||
kwargs['headers'].update(self.identity_headers or {})
|
||||
if self._passed_request_id:
|
||||
request_id = self._passed_request_id
|
||||
if six.PY3 and isinstance(request_id, bytes):
|
||||
request_id = request_id.decode('utf-8')
|
||||
kwargs['headers']['X-Openstack-Request-ID'] = request_id
|
||||
res = super(RegistryClient, self).do_request(method,
|
||||
action,
|
||||
**kwargs)
|
||||
status = res.status
|
||||
request_id = res.getheader('x-openstack-request-id')
|
||||
if six.PY3 and isinstance(request_id, bytes):
|
||||
request_id = request_id.decode('utf-8')
|
||||
LOG.debug("Registry request %(method)s %(action)s HTTP %(status)s"
|
||||
" request id %(request_id)s",
|
||||
{'method': method, 'action': action,
|
||||
'status': status, 'request_id': request_id})
|
||||
|
||||
# a 404 condition is not fatal, we shouldn't log at a fatal
|
||||
# level for it.
|
||||
except exception.NotFound:
|
||||
raise
|
||||
|
||||
# The following exception logging should only really be used
|
||||
# in extreme and unexpected cases.
|
||||
except Exception as exc:
|
||||
with excutils.save_and_reraise_exception():
|
||||
exc_name = exc.__class__.__name__
|
||||
LOG.exception(_LE("Registry client request %(method)s "
|
||||
"%(action)s raised %(exc_name)s"),
|
||||
{'method': method, 'action': action,
|
||||
'exc_name': exc_name})
|
||||
return res
|
||||
|
||||
def get_images_detailed(self, **kwargs):
|
||||
"""
|
||||
Returns a list of detailed image data mappings from Registry
|
||||
|
||||
:param filters: dict of keys & expected values to filter results
|
||||
:param marker: image id after which to start page
|
||||
:param limit: max number of images to return
|
||||
:param sort_key: results will be ordered by this image attribute
|
||||
:param sort_dir: direction in which to order results (asc, desc)
|
||||
"""
|
||||
params = self._extract_params(kwargs, images.SUPPORTED_PARAMS)
|
||||
res = self.do_request("GET", "/images/detail", params=params)
|
||||
image_list = jsonutils.loads(res.read())['images']
|
||||
for image in image_list:
|
||||
image = self.decrypt_metadata(image)
|
||||
return image_list
|
||||
|
||||
def get_image(self, image_id):
|
||||
"""Returns a mapping of image metadata from Registry."""
|
||||
res = self.do_request("GET", "/images/%s" % image_id)
|
||||
data = jsonutils.loads(res.read())['image']
|
||||
return self.decrypt_metadata(data)
|
||||
|
||||
def add_image(self, image_metadata):
|
||||
"""
|
||||
Tells registry about an image's metadata
|
||||
"""
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if 'image' not in image_metadata:
|
||||
image_metadata = dict(image=image_metadata)
|
||||
|
||||
encrypted_metadata = self.encrypt_metadata(image_metadata['image'])
|
||||
image_metadata['image'] = encrypted_metadata
|
||||
body = jsonutils.dump_as_bytes(image_metadata)
|
||||
|
||||
res = self.do_request("POST", "/images", body=body, headers=headers)
|
||||
# Registry returns a JSONified dict(image=image_info)
|
||||
data = jsonutils.loads(res.read())
|
||||
image = data['image']
|
||||
return self.decrypt_metadata(image)
|
||||
|
||||
def update_image(self, image_id, image_metadata, purge_props=False,
|
||||
from_state=None):
|
||||
"""
|
||||
Updates Registry's information about an image
|
||||
"""
|
||||
if 'image' not in image_metadata:
|
||||
image_metadata = dict(image=image_metadata)
|
||||
|
||||
encrypted_metadata = self.encrypt_metadata(image_metadata['image'])
|
||||
image_metadata['image'] = encrypted_metadata
|
||||
image_metadata['from_state'] = from_state
|
||||
body = jsonutils.dump_as_bytes(image_metadata)
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if purge_props:
|
||||
headers["X-Glance-Registry-Purge-Props"] = "true"
|
||||
|
||||
res = self.do_request("PUT", "/images/%s" % image_id, body=body,
|
||||
headers=headers)
|
||||
data = jsonutils.loads(res.read())
|
||||
image = data['image']
|
||||
return self.decrypt_metadata(image)
|
||||
|
||||
def delete_image(self, image_id):
|
||||
"""
|
||||
Deletes Registry's information about an image
|
||||
"""
|
||||
res = self.do_request("DELETE", "/images/%s" % image_id)
|
||||
data = jsonutils.loads(res.read())
|
||||
image = data['image']
|
||||
return image
|
||||
|
||||
def get_image_members(self, image_id):
|
||||
"""Return a list of membership associations from Registry."""
|
||||
res = self.do_request("GET", "/images/%s/members" % image_id)
|
||||
data = jsonutils.loads(res.read())['members']
|
||||
return data
|
||||
|
||||
def get_member_images(self, member_id):
|
||||
"""Return a list of membership associations from Registry."""
|
||||
res = self.do_request("GET", "/shared-images/%s" % member_id)
|
||||
data = jsonutils.loads(res.read())['shared_images']
|
||||
return data
|
||||
|
||||
def replace_members(self, image_id, member_data):
|
||||
"""Replace registry's information about image membership."""
|
||||
if isinstance(member_data, (list, tuple)):
|
||||
member_data = dict(memberships=list(member_data))
|
||||
elif (isinstance(member_data, dict) and
|
||||
'memberships' not in member_data):
|
||||
member_data = dict(memberships=[member_data])
|
||||
|
||||
body = jsonutils.dump_as_bytes(member_data)
|
||||
|
||||
headers = {'Content-Type': 'application/json', }
|
||||
|
||||
res = self.do_request("PUT", "/images/%s/members" % image_id,
|
||||
body=body, headers=headers)
|
||||
return self.get_status_code(res) == 204
|
||||
|
||||
def add_member(self, image_id, member_id, can_share=None):
|
||||
"""Add to registry's information about image membership."""
|
||||
body = None
|
||||
headers = {}
|
||||
# Build up a body if can_share is specified
|
||||
if can_share is not None:
|
||||
body = jsonutils.dump_as_bytes(
|
||||
dict(member=dict(can_share=can_share)))
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
url = "/images/%s/members/%s" % (image_id, member_id)
|
||||
res = self.do_request("PUT", url, body=body,
|
||||
headers=headers)
|
||||
return self.get_status_code(res) == 204
|
||||
|
||||
def delete_member(self, image_id, member_id):
|
||||
"""Delete registry's information about image membership."""
|
||||
res = self.do_request("DELETE", "/images/%s/members/%s" %
|
||||
(image_id, member_id))
|
||||
return self.get_status_code(res) == 204
|
@ -1,109 +0,0 @@
|
||||
# Copyright 2013 Red Hat, Inc
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Registry's Client V2
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from glance.common import exception
|
||||
from glance.i18n import _
|
||||
from glance.registry.client.v2 import client
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
_registry_client = 'glance.registry.client'
|
||||
CONF.import_opt('registry_client_protocol', _registry_client)
|
||||
CONF.import_opt('registry_client_key_file', _registry_client)
|
||||
CONF.import_opt('registry_client_ca_file', _registry_client)
|
||||
CONF.import_opt('registry_client_insecure', _registry_client)
|
||||
CONF.import_opt('registry_client_timeout', _registry_client)
|
||||
CONF.import_opt('use_user_token', _registry_client)
|
||||
CONF.import_opt('admin_user', _registry_client)
|
||||
CONF.import_opt('admin_password', _registry_client)
|
||||
CONF.import_opt('admin_tenant_name', _registry_client)
|
||||
CONF.import_opt('auth_url', _registry_client)
|
||||
CONF.import_opt('auth_strategy', _registry_client)
|
||||
CONF.import_opt('auth_region', _registry_client)
|
||||
|
||||
_CLIENT_CREDS = None
|
||||
_CLIENT_HOST = None
|
||||
_CLIENT_PORT = None
|
||||
_CLIENT_KWARGS = {}
|
||||
|
||||
|
||||
def configure_registry_client():
|
||||
"""
|
||||
Sets up a registry client for use in registry lookups
|
||||
"""
|
||||
global _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT
|
||||
try:
|
||||
host, port = CONF.registry_host, CONF.registry_port
|
||||
except cfg.ConfigFileValueError:
|
||||
msg = _("Configuration option was not valid")
|
||||
LOG.error(msg)
|
||||
raise exception.BadRegistryConnectionConfiguration(msg)
|
||||
except IndexError:
|
||||
msg = _("Could not find required configuration option")
|
||||
LOG.error(msg)
|
||||
raise exception.BadRegistryConnectionConfiguration(msg)
|
||||
|
||||
_CLIENT_HOST = host
|
||||
_CLIENT_PORT = port
|
||||
_CLIENT_KWARGS = {
|
||||
'use_ssl': CONF.registry_client_protocol.lower() == 'https',
|
||||
'key_file': CONF.registry_client_key_file,
|
||||
'cert_file': CONF.registry_client_cert_file,
|
||||
'ca_file': CONF.registry_client_ca_file,
|
||||
'insecure': CONF.registry_client_insecure,
|
||||
'timeout': CONF.registry_client_timeout,
|
||||
}
|
||||
|
||||
if not CONF.use_user_token:
|
||||
configure_registry_admin_creds()
|
||||
|
||||
|
||||
def configure_registry_admin_creds():
|
||||
global _CLIENT_CREDS
|
||||
|
||||
if CONF.auth_url or os.getenv('OS_AUTH_URL'):
|
||||
strategy = 'keystone'
|
||||
else:
|
||||
strategy = CONF.auth_strategy
|
||||
|
||||
_CLIENT_CREDS = {
|
||||
'user': CONF.admin_user,
|
||||
'password': CONF.admin_password,
|
||||
'username': CONF.admin_user,
|
||||
'tenant': CONF.admin_tenant_name,
|
||||
'auth_url': os.getenv('OS_AUTH_URL') or CONF.auth_url,
|
||||
'strategy': strategy,
|
||||
'region': CONF.auth_region,
|
||||
}
|
||||
|
||||
|
||||
def get_registry_client(cxt):
|
||||
global _CLIENT_CREDS, _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT
|
||||
kwargs = _CLIENT_KWARGS.copy()
|
||||
if CONF.use_user_token:
|
||||
kwargs['auth_token'] = cxt.auth_token
|
||||
if _CLIENT_CREDS:
|
||||
kwargs['creds'] = _CLIENT_CREDS
|
||||
return client.RegistryClient(_CLIENT_HOST, _CLIENT_PORT, **kwargs)
|
@ -1,27 +0,0 @@
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Simple client class to speak with any RESTful service that implements
|
||||
the Glance Registry API
|
||||
"""
|
||||
|
||||
from glance.common import rpc
|
||||
|
||||
|
||||
class RegistryClient(rpc.RPCClient):
|
||||
"""Registry's V2 Client."""
|
||||
|
||||
DEFAULT_PORT = 9191
|
@ -482,15 +482,11 @@ pipeline = cors healthcheck versionnegotiation gzip context rootapp
|
||||
[composite:rootapp]
|
||||
paste.composite_factory = glance.api:root_app_factory
|
||||
/: apiversions
|
||||
/v1: apiv1app
|
||||
/v2: apiv2app
|
||||
|
||||
[app:apiversions]
|
||||
paste.app_factory = glance.api.versions:create_resource
|
||||
|
||||
[app:apiv1app]
|
||||
paste.app_factory = glance.api.v1.router:API.factory
|
||||
|
||||
[app:apiv2app]
|
||||
paste.app_factory = glance.api.v2.router:API.factory
|
||||
|
||||
@ -663,15 +659,11 @@ pipeline = cors healthcheck versionnegotiation gzip context rootapp
|
||||
[composite:rootapp]
|
||||
paste.composite_factory = glance.api:root_app_factory
|
||||
/: apiversions
|
||||
/v1: apiv1app
|
||||
/v2: apiv2app
|
||||
|
||||
[app:apiversions]
|
||||
paste.app_factory = glance.api.versions:create_resource
|
||||
|
||||
[app:apiv1app]
|
||||
paste.app_factory = glance.api.v1.router:API.factory
|
||||
|
||||
[app:apiv2app]
|
||||
paste.app_factory = glance.api.v2.router:API.factory
|
||||
|
||||
|
@ -55,15 +55,11 @@ pipeline = versionnegotiation gzip context rootapp
|
||||
[composite:rootapp]
|
||||
paste.composite_factory = glance.api:root_app_factory
|
||||
/: apiversions
|
||||
/v1: apiv1app
|
||||
/v2: apiv2app
|
||||
|
||||
[app:apiversions]
|
||||
paste.app_factory = glance.api.versions:create_resource
|
||||
|
||||
[app:apiv1app]
|
||||
paste.app_factory = glance.api.v1.router:API.factory
|
||||
|
||||
[app:apiv2app]
|
||||
paste.app_factory = glance.api.v2.router:API.factory
|
||||
|
||||
|
@ -28,7 +28,6 @@ class TestPropertyQuotaViolations(base.ApiTest):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TestPropertyQuotaViolations, self).__init__(*args, **kwargs)
|
||||
self.api_flavor = 'noauth'
|
||||
self.registry_flavor = 'fakeauth'
|
||||
|
||||
def _headers(self, custom_headers=None):
|
||||
base_headers = {
|
||||
|
@ -56,7 +56,6 @@ class TestTasksApi(base.ApiTest):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TestTasksApi, self).__init__(*args, **kwargs)
|
||||
self.api_flavor = 'fakeauth'
|
||||
self.registry_flavor = 'fakeauth'
|
||||
|
||||
def _wait_on_task_execution(self, max_wait=5):
|
||||
"""Wait until all the tasks have finished execution and are in
|
||||
|
@ -73,12 +73,8 @@ def stub_out_store_server(stubs, base_dir, **kwargs):
|
||||
def close(self):
|
||||
return True
|
||||
|
||||
def _clean_url(self, url):
|
||||
# TODO(bcwaldon): Fix the hack that strips off v1
|
||||
return url.replace('/v1', '', 1) if url.startswith('/v1') else url
|
||||
|
||||
def putrequest(self, method, url):
|
||||
self.req = webob.Request.blank(self._clean_url(url))
|
||||
self.req = webob.Request.blank(url)
|
||||
if self.stub_force_sendfile:
|
||||
fake_sendfile = FakeSendFile(self.req)
|
||||
stubs.Set(sendfile, 'sendfile', fake_sendfile.sendfile)
|
||||
@ -100,7 +96,7 @@ def stub_out_store_server(stubs, base_dir, **kwargs):
|
||||
self.req.body += data.split("\r\n")[1]
|
||||
|
||||
def request(self, method, url, body=None, headers=None):
|
||||
self.req = webob.Request.blank(self._clean_url(url))
|
||||
self.req = webob.Request.blank(url)
|
||||
self.req.method = method
|
||||
if headers:
|
||||
self.req.headers = headers
|
||||
|
@ -1,358 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# 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 datetime
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
import routes
|
||||
import six
|
||||
from six.moves import http_client as http
|
||||
import webob
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import rpc
|
||||
from glance.common import wsgi
|
||||
from glance.tests.unit import base
|
||||
from glance.tests import utils as test_utils
|
||||
|
||||
|
||||
class FakeResource(object):
|
||||
"""
|
||||
Fake resource defining some methods that
|
||||
will be called later by the api.
|
||||
"""
|
||||
|
||||
def get_images(self, context, keyword=None):
|
||||
return keyword
|
||||
|
||||
def count_images(self, context, images):
|
||||
return len(images)
|
||||
|
||||
def get_all_images(self, context):
|
||||
return False
|
||||
|
||||
def raise_value_error(self, context):
|
||||
raise ValueError("Yep, Just like that!")
|
||||
|
||||
def raise_weird_error(self, context):
|
||||
class WeirdError(Exception):
|
||||
pass
|
||||
raise WeirdError("Weirdness")
|
||||
|
||||
|
||||
def create_api():
|
||||
deserializer = rpc.RPCJSONDeserializer()
|
||||
serializer = rpc.RPCJSONSerializer()
|
||||
controller = rpc.Controller()
|
||||
controller.register(FakeResource())
|
||||
res = wsgi.Resource(controller, deserializer, serializer)
|
||||
|
||||
mapper = routes.Mapper()
|
||||
mapper.connect("/rpc", controller=res,
|
||||
conditions=dict(method=["POST"]),
|
||||
action="__call__")
|
||||
return test_utils.FakeAuthMiddleware(wsgi.Router(mapper), is_admin=True)
|
||||
|
||||
|
||||
class TestRPCController(base.IsolatedUnitTest):
|
||||
|
||||
def setUp(self):
|
||||
super(TestRPCController, self).setUp()
|
||||
self.res = FakeResource()
|
||||
self.controller = rpc.Controller()
|
||||
self.controller.register(self.res)
|
||||
|
||||
def test_register(self):
|
||||
res = FakeResource()
|
||||
controller = rpc.Controller()
|
||||
controller.register(res)
|
||||
self.assertIn("get_images", controller._registered)
|
||||
self.assertIn("get_all_images", controller._registered)
|
||||
|
||||
def test_reigster_filtered(self):
|
||||
res = FakeResource()
|
||||
controller = rpc.Controller()
|
||||
controller.register(res, filtered=["get_all_images"])
|
||||
self.assertIn("get_all_images", controller._registered)
|
||||
|
||||
def test_reigster_excluded(self):
|
||||
res = FakeResource()
|
||||
controller = rpc.Controller()
|
||||
controller.register(res, excluded=["get_all_images"])
|
||||
self.assertIn("get_images", controller._registered)
|
||||
|
||||
def test_reigster_refiner(self):
|
||||
res = FakeResource()
|
||||
controller = rpc.Controller()
|
||||
|
||||
# Not callable
|
||||
self.assertRaises(TypeError,
|
||||
controller.register,
|
||||
res, refiner="get_all_images")
|
||||
|
||||
# Filter returns False
|
||||
controller.register(res, refiner=lambda x: False)
|
||||
self.assertNotIn("get_images", controller._registered)
|
||||
self.assertNotIn("get_images", controller._registered)
|
||||
|
||||
# Filter returns True
|
||||
controller.register(res, refiner=lambda x: True)
|
||||
self.assertIn("get_images", controller._registered)
|
||||
self.assertIn("get_images", controller._registered)
|
||||
|
||||
def test_request(self):
|
||||
api = create_api()
|
||||
req = webob.Request.blank('/rpc')
|
||||
req.method = 'POST'
|
||||
req.body = jsonutils.dump_as_bytes([
|
||||
{
|
||||
"command": "get_images",
|
||||
"kwargs": {"keyword": 1}
|
||||
}
|
||||
])
|
||||
res = req.get_response(api)
|
||||
returned = jsonutils.loads(res.body)
|
||||
self.assertIsInstance(returned, list)
|
||||
self.assertEqual(1, returned[0])
|
||||
|
||||
def test_request_exc(self):
|
||||
api = create_api()
|
||||
req = webob.Request.blank('/rpc')
|
||||
req.method = 'POST'
|
||||
req.body = jsonutils.dump_as_bytes([
|
||||
{
|
||||
"command": "get_all_images",
|
||||
"kwargs": {"keyword": 1}
|
||||
}
|
||||
])
|
||||
|
||||
# Sending non-accepted keyword
|
||||
# to get_all_images method
|
||||
res = req.get_response(api)
|
||||
returned = jsonutils.loads(res.body)
|
||||
self.assertIn("_error", returned[0])
|
||||
|
||||
def test_rpc_errors(self):
|
||||
api = create_api()
|
||||
req = webob.Request.blank('/rpc')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
|
||||
# Body is not a list, it should fail
|
||||
req.body = jsonutils.dump_as_bytes({})
|
||||
res = req.get_response(api)
|
||||
self.assertEqual(http.BAD_REQUEST, res.status_int)
|
||||
|
||||
# cmd is not dict, it should fail.
|
||||
req.body = jsonutils.dump_as_bytes([None])
|
||||
res = req.get_response(api)
|
||||
self.assertEqual(http.BAD_REQUEST, res.status_int)
|
||||
|
||||
# No command key, it should fail.
|
||||
req.body = jsonutils.dump_as_bytes([{}])
|
||||
res = req.get_response(api)
|
||||
self.assertEqual(http.BAD_REQUEST, res.status_int)
|
||||
|
||||
# kwargs not dict, it should fail.
|
||||
req.body = jsonutils.dump_as_bytes([{"command": "test", "kwargs": 2}])
|
||||
res = req.get_response(api)
|
||||
self.assertEqual(http.BAD_REQUEST, res.status_int)
|
||||
|
||||
# Command does not exist, it should fail.
|
||||
req.body = jsonutils.dump_as_bytes([{"command": "test"}])
|
||||
res = req.get_response(api)
|
||||
self.assertEqual(http.NOT_FOUND, res.status_int)
|
||||
|
||||
def test_rpc_exception_propagation(self):
|
||||
api = create_api()
|
||||
req = webob.Request.blank('/rpc')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
|
||||
req.body = jsonutils.dump_as_bytes([{"command": "raise_value_error"}])
|
||||
res = req.get_response(api)
|
||||
self.assertEqual(http.OK, res.status_int)
|
||||
|
||||
returned = jsonutils.loads(res.body)[0]
|
||||
err_cls = 'builtins.ValueError' if six.PY3 else 'exceptions.ValueError'
|
||||
self.assertEqual(err_cls, returned['_error']['cls'])
|
||||
|
||||
req.body = jsonutils.dump_as_bytes([{"command": "raise_weird_error"}])
|
||||
res = req.get_response(api)
|
||||
self.assertEqual(http.OK, res.status_int)
|
||||
|
||||
returned = jsonutils.loads(res.body)[0]
|
||||
self.assertEqual('glance.common.exception.RPCError',
|
||||
returned['_error']['cls'])
|
||||
|
||||
|
||||
class TestRPCClient(base.IsolatedUnitTest):
|
||||
|
||||
def setUp(self):
|
||||
super(TestRPCClient, self).setUp()
|
||||
self.api = create_api()
|
||||
self.client = rpc.RPCClient(host="http://127.0.0.1:9191")
|
||||
self.client._do_request = self.fake_request
|
||||
|
||||
def fake_request(self, method, url, body, headers):
|
||||
req = webob.Request.blank(url.path)
|
||||
body = encodeutils.to_utf8(body)
|
||||
req.body = body
|
||||
req.method = method
|
||||
|
||||
webob_res = req.get_response(self.api)
|
||||
return test_utils.FakeHTTPResponse(status=webob_res.status_int,
|
||||
headers=webob_res.headers,
|
||||
data=webob_res.body)
|
||||
|
||||
def test_method_proxy(self):
|
||||
proxy = self.client.some_method
|
||||
self.assertIn("method_proxy", str(proxy))
|
||||
|
||||
def test_bulk_request(self):
|
||||
commands = [{"command": "get_images", 'kwargs': {'keyword': True}},
|
||||
{"command": "get_all_images"}]
|
||||
|
||||
res = self.client.bulk_request(commands)
|
||||
self.assertEqual(2, len(res))
|
||||
self.assertTrue(res[0])
|
||||
self.assertFalse(res[1])
|
||||
|
||||
def test_exception_raise(self):
|
||||
try:
|
||||
self.client.raise_value_error()
|
||||
self.fail("Exception not raised")
|
||||
except ValueError as exc:
|
||||
self.assertEqual("Yep, Just like that!", str(exc))
|
||||
|
||||
def test_rpc_exception(self):
|
||||
try:
|
||||
self.client.raise_weird_error()
|
||||
self.fail("Exception not raised")
|
||||
except exception.RPCError:
|
||||
pass
|
||||
|
||||
def test_non_str_or_dict_response(self):
|
||||
rst = self.client.count_images(images=[1, 2, 3, 4])
|
||||
self.assertEqual(4, rst)
|
||||
self.assertIsInstance(rst, int)
|
||||
|
||||
|
||||
class TestRPCJSONSerializer(test_utils.BaseTestCase):
|
||||
|
||||
def test_to_json(self):
|
||||
fixture = {"key": "value"}
|
||||
expected = b'{"key": "value"}'
|
||||
actual = rpc.RPCJSONSerializer().to_json(fixture)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_to_json_with_date_format_value(self):
|
||||
fixture = {"date": datetime.datetime(1900, 3, 8, 2)}
|
||||
expected = {"date": {"_value": "1900-03-08T02:00:00",
|
||||
"_type": "datetime"}}
|
||||
actual = rpc.RPCJSONSerializer().to_json(fixture)
|
||||
actual = jsonutils.loads(actual)
|
||||
for k in expected['date']:
|
||||
self.assertEqual(expected['date'][k], actual['date'][k])
|
||||
|
||||
def test_to_json_with_more_deep_format(self):
|
||||
fixture = {"is_public": True, "name": [{"name1": "test"}]}
|
||||
expected = {"is_public": True, "name": [{"name1": "test"}]}
|
||||
actual = rpc.RPCJSONSerializer().to_json(fixture)
|
||||
actual = wsgi.JSONResponseSerializer().to_json(fixture)
|
||||
actual = jsonutils.loads(actual)
|
||||
for k in expected:
|
||||
self.assertEqual(expected[k], actual[k])
|
||||
|
||||
def test_default(self):
|
||||
fixture = {"key": "value"}
|
||||
response = webob.Response()
|
||||
rpc.RPCJSONSerializer().default(response, fixture)
|
||||
self.assertEqual(http.OK, response.status_int)
|
||||
content_types = [h for h in response.headerlist
|
||||
if h[0] == 'Content-Type']
|
||||
self.assertEqual(1, len(content_types))
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(b'{"key": "value"}', response.body)
|
||||
|
||||
|
||||
class TestRPCJSONDeserializer(test_utils.BaseTestCase):
|
||||
|
||||
def test_has_body_no_content_length(self):
|
||||
request = wsgi.Request.blank('/')
|
||||
request.method = 'POST'
|
||||
request.body = b'asdf'
|
||||
request.headers.pop('Content-Length')
|
||||
self.assertFalse(rpc.RPCJSONDeserializer().has_body(request))
|
||||
|
||||
def test_has_body_zero_content_length(self):
|
||||
request = wsgi.Request.blank('/')
|
||||
request.method = 'POST'
|
||||
request.body = b'asdf'
|
||||
request.headers['Content-Length'] = 0
|
||||
self.assertFalse(rpc.RPCJSONDeserializer().has_body(request))
|
||||
|
||||
def test_has_body_has_content_length(self):
|
||||
request = wsgi.Request.blank('/')
|
||||
request.method = 'POST'
|
||||
request.body = b'asdf'
|
||||
self.assertIn('Content-Length', request.headers)
|
||||
self.assertTrue(rpc.RPCJSONDeserializer().has_body(request))
|
||||
|
||||
def test_no_body_no_content_length(self):
|
||||
request = wsgi.Request.blank('/')
|
||||
self.assertFalse(rpc.RPCJSONDeserializer().has_body(request))
|
||||
|
||||
def test_from_json(self):
|
||||
fixture = '{"key": "value"}'
|
||||
expected = {"key": "value"}
|
||||
actual = rpc.RPCJSONDeserializer().from_json(fixture)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_from_json_malformed(self):
|
||||
fixture = 'kjasdklfjsklajf'
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
rpc.RPCJSONDeserializer().from_json, fixture)
|
||||
|
||||
def test_default_no_body(self):
|
||||
request = wsgi.Request.blank('/')
|
||||
actual = rpc.RPCJSONDeserializer().default(request)
|
||||
expected = {}
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_default_with_body(self):
|
||||
request = wsgi.Request.blank('/')
|
||||
request.method = 'POST'
|
||||
request.body = b'{"key": "value"}'
|
||||
actual = rpc.RPCJSONDeserializer().default(request)
|
||||
expected = {"body": {"key": "value"}}
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_has_body_has_transfer_encoding(self):
|
||||
request = wsgi.Request.blank('/')
|
||||
request.method = 'POST'
|
||||
request.body = b'fake_body'
|
||||
request.headers['transfer-encoding'] = ''
|
||||
self.assertIn('transfer-encoding', request.headers)
|
||||
self.assertTrue(rpc.RPCJSONDeserializer().has_body(request))
|
||||
|
||||
def test_to_json_with_date_format_value(self):
|
||||
fixture = ('{"date": {"_value": "1900-03-08T02:00:00.000000",'
|
||||
'"_type": "datetime"}}')
|
||||
expected = {"date": datetime.datetime(1900, 3, 8, 2)}
|
||||
actual = rpc.RPCJSONDeserializer().from_json(fixture)
|
||||
self.assertEqual(expected, actual)
|
@ -16,8 +16,8 @@
|
||||
import testtools
|
||||
import webob
|
||||
|
||||
from glance.api import cached_images
|
||||
from glance.api import policy
|
||||
from glance.api.v2 import cached_images
|
||||
from glance.common import exception
|
||||
from glance import image_cache
|
||||
|
||||
@ -71,7 +71,7 @@ class FakeCache(image_cache.ImageCache):
|
||||
return 1
|
||||
|
||||
|
||||
class FakeController(cached_images.Controller):
|
||||
class FakeController(cached_images.CacheController):
|
||||
def __init__(self):
|
||||
self.cache = FakeCache()
|
||||
self.policy = FakePolicyEnforcer()
|
||||
@ -80,7 +80,7 @@ class FakeController(cached_images.Controller):
|
||||
class TestController(testtools.TestCase):
|
||||
def test_initialization_without_conf(self):
|
||||
self.assertRaises(exception.BadDriverConfiguration,
|
||||
cached_images.Controller)
|
||||
cached_images.CacheController)
|
||||
|
||||
|
||||
class TestCachedImages(testtools.TestCase):
|
||||
|
Loading…
Reference in New Issue
Block a user