Fixes LP Bug#861650 - Glance client deps

This patch addresses the dependency proliferation in
the glance client stuff. It removes references to modules
that contain non-client-necessary libraries (like
eventlet, xattr, sqlalchemy-migrate and sqlalchemy)
by restructuring the modules slightly.

Note that the additional httplib2 dependency is because
that is used in the authentication strategy stuff in
glance.common.auth. This could be rewritten to use httplib
instead, further reducing the dependencies of the client lib.

IMPORTANT NOTE: This patch changes the default entrypoint
for the Images API router application, and therefore this
should be merged along with the packaging changes in this
branch:

https://code.launchpad.net/~jaypipes/glance/ubuntu/+merge/82318

Change-Id: I5dbc8584fb77e3e011fb6ff3532f792f5103e242
This commit is contained in:
Jay Pipes 2011-11-15 11:49:10 -05:00
parent 6bf61e8ce1
commit 1ab63ff5c9
17 changed files with 247 additions and 291 deletions

View File

@ -40,8 +40,8 @@ if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
gettext.install('glance', unicode=1)
from glance import client as glance_client
from glance import version
from glance import client as glance_client
from glance.common import exception
from glance.common import utils

View File

@ -214,7 +214,7 @@ pipeline = versionnegotiation context apiv1app
# pipeline = versionnegotiation authtoken auth-context cachemanage apiv1app
[app:apiv1app]
paste.app_factory = glance.api.v1:app_factory
paste.app_factory = glance.api.v1.router:app_factory
[filter:versionnegotiation]
paste.filter_factory = glance.api.middleware.version_negotiation:filter_factory

View File

@ -14,50 +14,3 @@
# 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 logging
import webob.exc
from glance import registry
from glance.common import exception
logger = logging.getLogger('glance.api')
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:
msg = _("Image with identifier %s not found") % image_id
logger.debug(msg)
raise webob.exc.HTTPNotFound(
msg, request=request, content_type='text/plain')
except exception.NotAuthorized:
msg = _("Unauthorized image access")
logger.debug(msg)
raise webob.exc.HTTPForbidden(msg, request=request,
content_type='text/plain')
def get_active_image_meta_or_404(self, request, image_id):
"""
Same as get_image_meta_or_404 except that it will raise a 404 if the
image isn't 'active'.
"""
image = self.get_image_meta_or_404(request, image_id)
if image['status'] != 'active':
msg = _("Image %s is not active") % image_id
logger.debug(msg)
raise webob.exc.HTTPNotFound(
msg, request=request, content_type='text/plain')
return image

View File

@ -25,14 +25,14 @@ import webob.exc
from glance.common import exception
from glance.common import wsgi
from glance import api
from glance.api.v1 import controller
from glance import image_cache
from glance import registry
logger = logging.getLogger(__name__)
class Controller(api.BaseController):
class Controller(controller.BaseController):
"""
A controller for managing cached images.
"""

View File

@ -15,51 +15,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import logging
SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
'min_ram', 'min_disk', 'size_min', 'size_max',
'is_public', 'changes-since']
import routes
from glance.api.v1 import images
from glance.api.v1 import members
from glance.common import wsgi
logger = logging.getLogger('glance.api.v1')
class API(wsgi.Router):
"""WSGI router for Glance v1 API requests."""
def __init__(self, options):
self.options = options
mapper = routes.Mapper()
images_resource = images.create_resource(options)
mapper.resource("image", "images", controller=images_resource,
collection={'detail': 'GET'})
mapper.connect("/", controller=images_resource, action="index")
mapper.connect("/images/{id}", controller=images_resource,
action="meta", conditions=dict(method=["HEAD"]))
members_resource = members.create_resource(options)
mapper.resource("member", "members", controller=members_resource,
parent_resource=dict(member_name='image',
collection_name='images'))
mapper.connect("/shared-images/{id}",
controller=members_resource,
action="index_shared_images")
mapper.connect("/images/{image_id}/members",
controller=members_resource,
action="update_all",
conditions=dict(method=["PUT"]))
super(API, self).__init__(mapper)
def app_factory(global_conf, **local_conf):
"""paste.deploy app factory for creating Glance API server apps"""
conf = global_conf.copy()
conf.update(local_conf)
return API(conf)
SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir')

View File

@ -0,0 +1,64 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# 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 logging
import webob.exc
from glance import registry
from glance.common import exception
logger = 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:
msg = _("Image with identifier %s not found") % image_id
logger.debug(msg)
raise webob.exc.HTTPNotFound(
msg, request=request, content_type='text/plain')
except exception.NotAuthorized:
msg = _("Unauthorized image access")
logger.debug(msg)
raise webob.exc.HTTPForbidden(msg, request=request,
content_type='text/plain')
def get_active_image_meta_or_404(self, request, image_id):
"""
Same as get_image_meta_or_404 except that it will raise a 404 if the
image isn't 'active'.
"""
image = self.get_image_meta_or_404(request, image_id)
if image['status'] != 'active':
msg = _("Image %s is not active") % image_id
logger.debug(msg)
raise webob.exc.HTTPNotFound(
msg, request=request, content_type='text/plain')
return image

View File

@ -31,11 +31,13 @@ from webob.exc import (HTTPNotFound,
HTTPNoContent,
HTTPUnauthorized)
from glance import api
import glance.api.v1
from glance.api.v1 import controller
from glance import image_cache
from glance.common import exception
from glance.common import notifier
from glance.common import wsgi
from glance.common import utils
import glance.store
import glance.store.filesystem
import glance.store.http
@ -50,16 +52,12 @@ from glance.store import (get_from_backend,
from glance import registry
logger = logging.getLogger('glance.api.v1.images')
SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
'min_ram', 'min_disk', 'size_min', 'size_max',
'is_public', 'changes-since']
SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir')
logger = logging.getLogger(__name__)
SUPPORTED_PARAMS = glance.api.v1.SUPPORTED_PARAMS
SUPPORTED_FILTERS = glance.api.v1.SUPPORTED_FILTERS
class Controller(api.BaseController):
class Controller(controller.BaseController):
"""
WSGI controller for images resource in Glance v1 API
@ -596,7 +594,7 @@ class ImageDeserializer(wsgi.JSONRequestDeserializer):
def _deserialize(self, request):
result = {}
result['image_meta'] = wsgi.get_image_meta_from_headers(request)
result['image_meta'] = utils.get_image_meta_from_headers(request)
data = request.body_file if self.has_body(request) else None
result['image_data'] = data
return result
@ -630,7 +628,7 @@ class ImageSerializer(wsgi.JSONResponseSerializer):
:param response: The Webob Response object
:param image_meta: Mapping of image metadata
"""
headers = wsgi.image_meta_to_http_headers(image_meta)
headers = utils.image_meta_to_http_headers(image_meta)
for k, v in headers.items():
response.headers[k] = v

65
glance/api/v1/router.py Normal file
View File

@ -0,0 +1,65 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# 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 logging
import routes
from glance.api.v1 import images
from glance.api.v1 import members
from glance.common import wsgi
logger = logging.getLogger(__name__)
class API(wsgi.Router):
"""WSGI router for Glance v1 API requests."""
def __init__(self, options):
self.options = options
mapper = routes.Mapper()
images_resource = images.create_resource(options)
mapper.resource("image", "images", controller=images_resource,
collection={'detail': 'GET'})
mapper.connect("/", controller=images_resource, action="index")
mapper.connect("/images/{id}", controller=images_resource,
action="meta", conditions=dict(method=["HEAD"]))
members_resource = members.create_resource(options)
mapper.resource("member", "members", controller=members_resource,
parent_resource=dict(member_name='image',
collection_name='images'))
mapper.connect("/shared-images/{id}",
controller=members_resource,
action="index_shared_images")
mapper.connect("/images/{image_id}/members",
controller=members_resource,
action="update_all",
conditions=dict(method=["PUT"]))
super(API, self).__init__(mapper)
def app_factory(global_conf, **local_conf):
"""paste.deploy app factory for creating Glance API server apps"""
conf = global_conf.copy()
conf.update(local_conf)
return API(conf)

View File

@ -21,14 +21,17 @@ Client classes for callers of a Glance system
import errno
import json
import logging
import os
from glance.api.v1 import images as v1_images
import glance.api.v1
from glance.common import client as base_client
from glance.common import exception
from glance.common import wsgi
from glance.common import utils
#TODO(jaypipes) Allow a logger param for client classes
logger = logging.getLogger(__name__)
SUPPORTED_PARAMS = glance.api.v1.SUPPORTED_PARAMS
SUPPORTED_FILTERS = glance.api.v1.SUPPORTED_FILTERS
class V1Client(base_client.BaseClient):
@ -49,7 +52,7 @@ class V1Client(base_client.BaseClient):
:param sort_key: results will be ordered by this image attribute
:param sort_dir: direction in which to to order results (asc, desc)
"""
params = self._extract_params(kwargs, v1_images.SUPPORTED_PARAMS)
params = self._extract_params(kwargs, SUPPORTED_PARAMS)
res = self.do_request("GET", "/images", params=params)
data = json.loads(res.read())['images']
return data
@ -65,7 +68,7 @@ class V1Client(base_client.BaseClient):
:param sort_key: results will be ordered by this image attribute
:param sort_dir: direction in which to to order results (asc, desc)
"""
params = self._extract_params(kwargs, v1_images.SUPPORTED_PARAMS)
params = self._extract_params(kwargs, SUPPORTED_PARAMS)
res = self.do_request("GET", "/images/detail", params=params)
data = json.loads(res.read())['images']
return data
@ -82,7 +85,7 @@ class V1Client(base_client.BaseClient):
"""
res = self.do_request("GET", "/images/%s" % image_id)
image = wsgi.get_image_meta_from_headers(res)
image = utils.get_image_meta_from_headers(res)
return image, base_client.ImageBodyIterator(res)
def get_image_meta(self, image_id):
@ -93,7 +96,7 @@ class V1Client(base_client.BaseClient):
"""
res = self.do_request("HEAD", "/images/%s" % image_id)
image = wsgi.get_image_meta_from_headers(res)
image = utils.get_image_meta_from_headers(res)
return image
def _get_image_size(self, image_data):
@ -136,7 +139,7 @@ class V1Client(base_client.BaseClient):
:retval The newly-stored image's metadata.
"""
headers = wsgi.image_meta_to_http_headers(image_meta or {})
headers = utils.image_meta_to_http_headers(image_meta or {})
if image_data:
body = image_data
@ -159,7 +162,7 @@ class V1Client(base_client.BaseClient):
if image_meta is None:
image_meta = {}
headers = wsgi.image_meta_to_http_headers(image_meta)
headers = utils.image_meta_to_http_headers(image_meta)
if image_data:
body = image_data

View File

@ -26,13 +26,11 @@ import os
import urllib
import urlparse
from eventlet.green import socket, ssl
# See http://code.google.com/p/python-nose/issues/detail?id=373
# The code below enables glance.client standalone to work with i18n _() blocks
import __builtin__
if not hasattr(__builtin__, '_'):
setattr(__builtin__, '_', lambda x: x)
try:
from eventlet.green import socket, ssl
except ImportError:
import socket
import ssl
from glance.common import auth
from glance.common import exception

View File

@ -34,7 +34,7 @@ import uuid
from glance.common import exception
logger = logging.getLogger('glance.utils')
logger = logging.getLogger(__name__)
TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
@ -54,6 +54,76 @@ def chunkiter(fp, chunk_size=65536):
break
def image_meta_to_http_headers(image_meta):
"""
Returns a set of image metadata into a dict
of HTTP headers that can be fed to either a Webob
Request object or an httplib.HTTP(S)Connection object
:param image_meta: Mapping of image metadata
"""
headers = {}
for k, v in image_meta.items():
if v is None:
v = ''
if k == 'properties':
for pk, pv in v.items():
if pv is None:
pv = ''
headers["x-image-meta-property-%s"
% pk.lower()] = unicode(pv)
else:
headers["x-image-meta-%s" % k.lower()] = unicode(v)
return headers
def get_image_meta_from_headers(response):
"""
Processes HTTP headers from a supplied response that
match the x-image-meta and x-image-meta-property and
returns a mapping of image metadata and properties
:param response: Response to process
"""
result = {}
properties = {}
if hasattr(response, 'getheaders'): # httplib.HTTPResponse
headers = response.getheaders()
else: # webob.Response
headers = response.headers.items()
for key, value in headers:
key = str(key.lower())
if key.startswith('x-image-meta-property-'):
field_name = key[len('x-image-meta-property-'):].replace('-', '_')
properties[field_name] = value or None
elif key.startswith('x-image-meta-'):
field_name = key[len('x-image-meta-'):].replace('-', '_')
result[field_name] = value or None
result['properties'] = properties
if 'size' in result:
result['size'] = int(result['size'])
if 'is_public' in result:
result['is_public'] = bool_from_header_value(result['is_public'])
if 'deleted' in result:
result['deleted'] = bool_from_header_value(result['deleted'])
return result
def bool_from_header_value(value):
"""
Returns True if value is a boolean True or the
string 'true', case-insensitive, False otherwise
"""
if isinstance(value, bool):
return value
elif isinstance(value, (basestring, unicode)):
if str(value).lower() == 'true':
return True
return False
def bool_from_string(subject):
"""
Interpret a string as a boolean.

View File

@ -406,73 +406,3 @@ class Resource(object):
pass
return args
def image_meta_to_http_headers(image_meta):
"""
Returns a set of image metadata into a dict
of HTTP headers that can be fed to either a Webob
Request object or an httplib.HTTP(S)Connection object
:param image_meta: Mapping of image metadata
"""
headers = {}
for k, v in image_meta.items():
if v is None:
v = ''
if k == 'properties':
for pk, pv in v.items():
if pv is None:
pv = ''
headers["x-image-meta-property-%s"
% pk.lower()] = unicode(pv)
else:
headers["x-image-meta-%s" % k.lower()] = unicode(v)
return headers
def get_image_meta_from_headers(response):
"""
Processes HTTP headers from a supplied response that
match the x-image-meta and x-image-meta-property and
returns a mapping of image metadata and properties
:param response: Response to process
"""
result = {}
properties = {}
if hasattr(response, 'getheaders'): # httplib.HTTPResponse
headers = response.getheaders()
else: # webob.Response
headers = response.headers.items()
for key, value in headers:
key = str(key.lower())
if key.startswith('x-image-meta-property-'):
field_name = key[len('x-image-meta-property-'):].replace('-', '_')
properties[field_name] = value or None
elif key.startswith('x-image-meta-'):
field_name = key[len('x-image-meta-'):].replace('-', '_')
result[field_name] = value or None
result['properties'] = properties
if 'size' in result:
result['size'] = int(result['size'])
if 'is_public' in result:
result['is_public'] = bool_from_header_value(result['is_public'])
if 'deleted' in result:
result['deleted'] = bool_from_header_value(result['deleted'])
return result
def bool_from_header_value(value):
"""
Returns True if value is a boolean True or the
string 'true', case-insensitive, False otherwise
"""
if isinstance(value, bool):
return value
elif isinstance(value, (basestring, unicode)):
if str(value).lower() == 'true':
return True
return False

View File

@ -1,83 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 OpenStack, LLC
# 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 httplib
import json
import urlparse
from glance import client
class ImageRegistryException(Exception):
""" Base class for all RegistryAdapter exceptions """
pass
class UnknownImageRegistry(ImageRegistryException):
""" Raised if we don't recognize the requested Registry protocol """
pass
class ImageRegistry(object):
""" Base class for all image endpoints """
@classmethod
def lookup(cls, parsed_uri):
""" Subclasses must define a lookup method which returns an dictionary
representing the image.
"""
raise NotImplementedError
class Parallax(ImageRegistry):
"""
Parallax stuff
"""
@classmethod
def lookup(cls, image_id):
"""
Takes an image ID and checks if that image is registered in Parallax,
and if so, returns the image metadata. If the image does not exist,
we raise NotFound
"""
# TODO(jaypipes): Make parallax client configurable via options.
# Unfortunately, the decision to make all adapters have no state
# hinders this...
c = client.ParallaxClient()
return c.get_image(image_id)
REGISTRY_ADAPTERS = {
'parallax': Parallax
}
def lookup_by_registry(registry, image_id):
"""
Convenience function to lookup image metadata for the given
opaque image identifier and registry.
:param registry: String name of registry to use for lookups
:param image_id: Opaque image identifier
"""
try:
adapter = REGISTRY_ADAPTERS[registry]
except KeyError:
raise UnknownImageRegistry("'%s' not found" % registry)
return adapter.lookup(image_id)

View File

@ -217,7 +217,7 @@ image_cache_driver = %(image_cache_driver)s
pipeline = versionnegotiation context %(cache_pipeline)s apiv1app
[app:apiv1app]
paste.app_factory = glance.api.v1:app_factory
paste.app_factory = glance.api.v1.router:app_factory
[filter:versionnegotiation]
paste.filter_factory = glance.api.middleware.version_negotiation:filter_factory

View File

@ -22,8 +22,8 @@ import shutil
import webob
from glance.api import v1 as server
from glance.api.middleware import version_negotiation
from glance.api.v1 import router
import glance.common.client
from glance.common import context
from glance.common import exception
@ -154,7 +154,7 @@ def stub_out_registry_and_store_server(stubs):
'default_store': 'file',
'filesystem_store_datadir': FAKE_FILESYSTEM_ROOTDIR}
api = version_negotiation.VersionNegotiationFilter(
context.ContextMiddleware(server.API(options), options),
context.ContextMiddleware(router.API(options), options),
options)
res = self.req.get_response(api)

View File

@ -26,7 +26,7 @@ import unittest
import stubout
import webob
from glance.api import v1 as server
from glance.api.v1 import router
from glance.common import context
from glance.common import utils
from glance.registry import context as rcontext
@ -1936,7 +1936,7 @@ class TestGlanceAPI(unittest.TestCase):
stubs.stub_out_registry_and_store_server(self.stubs)
stubs.stub_out_filesystem_backend()
sql_connection = os.environ.get('GLANCE_SQL_CONNECTION', "sqlite://")
self.api = context.ContextMiddleware(server.API(OPTIONS), OPTIONS)
self.api = context.ContextMiddleware(router.API(OPTIONS), OPTIONS)
self.FIXTURES = [
{'id': UUID1,
'name': 'fake image #1',

View File

@ -19,6 +19,7 @@ import unittest
import webob
from glance.common import wsgi
from glance.common import utils
from glance.common import exception
@ -201,7 +202,7 @@ class TestHelpers(unittest.TestCase):
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {'distro': 'Ubuntu 10.04 LTS'}}
headers = wsgi.image_meta_to_http_headers(fixture)
headers = utils.image_meta_to_http_headers(fixture)
for k, v in headers.iteritems():
self.assert_(isinstance(v, unicode), "%s is not unicode" % v)
@ -217,14 +218,14 @@ class TestHelpers(unittest.TestCase):
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {'distro': 'Ubuntu 10.04 LTS'}}
headers = wsgi.image_meta_to_http_headers(fixture)
headers = utils.image_meta_to_http_headers(fixture)
class FakeResponse():
pass
response = FakeResponse()
response.headers = headers
result = wsgi.get_image_meta_from_headers(response)
result = utils.get_image_meta_from_headers(response)
for k, v in fixture.iteritems():
self.assertEqual(v, result[k])
@ -243,11 +244,11 @@ class TestHelpers(unittest.TestCase):
pass
for fixture in fixtures:
headers = wsgi.image_meta_to_http_headers(fixture)
headers = utils.image_meta_to_http_headers(fixture)
response = FakeResponse()
response.headers = headers
result = wsgi.get_image_meta_from_headers(response)
result = utils.get_image_meta_from_headers(response)
for k, v in expected.items():
self.assertEqual(v, result[k])
@ -261,10 +262,10 @@ class TestHelpers(unittest.TestCase):
expected = {'is_public': False}
for fixture in fixtures:
headers = wsgi.image_meta_to_http_headers(fixture)
headers = utils.image_meta_to_http_headers(fixture)
response = FakeResponse()
response.headers = headers
result = wsgi.get_image_meta_from_headers(response)
result = utils.get_image_meta_from_headers(response)
for k, v in expected.items():
self.assertEqual(v, result[k])