Cleanup remove api v1 and registry code

Change-Id: I86a3cbf4374bc2b083ccd86f75b88490b305eaab
This commit is contained in:
Erno Kuvaja 2020-06-30 17:44:53 +01:00
parent 3f6e80cdc9
commit 3068096199
32 changed files with 15 additions and 3835 deletions

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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')

View File

@ -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, }

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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",
]

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 = {

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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):