Add flag cinder_endpoint_template to volume.cinder

Add optional flag to allow cinder endpoint to be defined by flag rather
than extracted from the request context's service catalog.  No change to
default behavior.

This flag allows deployers to use a seperate management endpoint for
cinder which compute can use exclusively to send messages to cinder, or
just use an auth system that doesn't add/forward the catalog along with
the request (e.g. noauth).

Add python-cinderclient to tools/test-requires

fix lp bug #1048798

Change-Id: Icb416bf4df2a6e37024f1fbc866006d46d30bcf2
This commit is contained in:
Clay Gerrard 2012-09-10 19:05:10 -05:00
parent 608b75e125
commit 0fa231f718
4 changed files with 180 additions and 7 deletions

View File

@ -1752,6 +1752,18 @@
#### resides
######## defined in nova.volume.cinder ########
# cinder_catalog_info=volume:cinder:publicURL
#### (StrOpt) Info to match when looking for cinder in the service catalog.
#### Format is : separated values of the form:
#### <service_type>:<service_name>:<endpoint_type>
# cinder_endpoint_template=None
#### (StrOpt) Override service catalog lookup with template for cinder
#### endpoint e.g. http://localhost:8776/v1/%(project_id)s
######## defined in nova.volume.driver ########
# volume_group=nova-volumes

151
nova/tests/test_cinder.py Normal file
View File

@ -0,0 +1,151 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC
#
# 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 httplib2
import urlparse
from nova import context
from nova.volume import cinder
from nova import test
def _stub_volume(**kwargs):
volume = {
'display_name': None,
'display_description': None,
"attachments": [],
"availability_zone": "cinder",
"created_at": "2012-09-10T00:00:00.000000",
"id": '00000000-0000-0000-0000-000000000000',
"metadata": {},
"size": 1,
"snapshot_id": None,
"status": "available",
"volume_type": "None",
}
volume.update(kwargs)
return volume
class FakeHTTPClient(cinder.cinder_client.client.HTTPClient):
def _cs_request(self, url, method, **kwargs):
# Check that certain things are called correctly
if method in ['GET', 'DELETE']:
assert 'body' not in kwargs
elif method == 'PUT':
assert 'body' in kwargs
# Call the method
args = urlparse.parse_qsl(urlparse.urlparse(url)[4])
kwargs.update(args)
munged_url = url.rsplit('?', 1)[0]
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
munged_url = munged_url.replace('-', '_')
callback = "%s_%s" % (method.lower(), munged_url)
if not hasattr(self, callback):
raise AssertionError('Called unknown API method: %s %s, '
'expected fakes method name: %s' %
(method, url, callback))
# Note the call
self.callstack.append((method, url, kwargs.get('body', None)))
status, body = getattr(self, callback)(**kwargs)
if hasattr(status, 'items'):
return httplib2.Response(status), body
else:
return httplib2.Response({"status": status}), body
def get_volumes_1234(self, **kw):
volume = {'volume': _stub_volume(id='1234')}
return (200, volume)
class FakeCinderClient(cinder.cinder_client.Client):
def __init__(self, username, password, project_id=None, auth_url=None):
super(FakeCinderClient, self).__init__(username, password,
project_id=project_id,
auth_url=auth_url)
self.client = FakeHTTPClient(username, password, project_id, auth_url)
# keep a ref to the clients callstack for factory's assert_called
self.callstack = self.client.callstack = []
class FakeClientFactory(object):
"""Keep a ref to the FakeClient since volume.api.cinder throws it away."""
def __call__(self, *args, **kwargs):
self.client = FakeCinderClient(*args, **kwargs)
return self.client
def assert_called(self, method, url, body=None, pos=-1):
expected = (method, url)
called = self.client.callstack[pos][0:2]
assert self.client.callstack, ("Expected %s %s but no calls "
"were made." % expected)
assert expected == called, 'Expected %s %s; got %s %s' % (expected +
called)
if body is not None:
assert self.client.callstack[pos][2] == body
class CinderTestCase(test.TestCase):
"""Test case for cinder volume api."""
def setUp(self):
super(CinderTestCase, self).setUp()
self.fake_client_factory = FakeClientFactory()
self.stubs.Set(cinder.cinder_client, "Client",
self.fake_client_factory)
self.flags(
volume_api_class='nova.volume.cinder.API',
)
self.api = cinder.API()
catalog = [{
"type": "volume",
"name": "cinder",
"endpoints": [{"publicURL": "http://localhost:8776/v1/project_id"}]
}]
self.context = context.RequestContext('username', 'project_id',
service_catalog=catalog)
def assert_called(self, *args, **kwargs):
self.fake_client_factory.assert_called(*args, **kwargs)
def test_context_with_catalog(self):
volume = self.api.get(self.context, '1234')
self.assert_called('GET', '/volumes/1234')
self.assertEquals(
self.fake_client_factory.client.client.management_url,
'http://localhost:8776/v1/project_id')
def test_cinder_endpoint_template(self):
self.flags(
cinder_endpoint_template='http://other_host:8776/v1/%(project_id)s'
)
volume = self.api.get(self.context, '1234')
self.assert_called('GET', '/volumes/1234')
self.assertEquals(
self.fake_client_factory.client.client.management_url,
'http://other_host:8776/v1/project_id')

View File

@ -36,6 +36,10 @@ cinder_opts = [
help='Info to match when looking for cinder in the service '
'catalog. Format is : separated values of the form: '
'<service_type>:<service_name>:<endpoint_type>'),
cfg.StrOpt('cinder_endpoint_template',
default=None,
help='Override service catalog lookup with template for cinder '
'endpoint e.g. http://localhost:8776/v1/%(project_id)s'),
]
FLAGS = flags.FLAGS
@ -49,14 +53,17 @@ def cinderclient(context):
# FIXME: the cinderclient ServiceCatalog object is mis-named.
# It actually contains the entire access blob.
compat_catalog = {
'access': {'serviceCatalog': context.service_catalog}
'access': {'serviceCatalog': context.service_catalog or {}}
}
sc = service_catalog.ServiceCatalog(compat_catalog)
info = FLAGS.cinder_catalog_info
service_type, service_name, endpoint_type = info.split(':')
url = sc.url_for(service_type=service_type,
service_name=service_name,
endpoint_type=endpoint_type)
if FLAGS.cinder_endpoint_template:
url = FLAGS.cinder_endpoint_template % context.to_dict()
else:
info = FLAGS.cinder_catalog_info
service_type, service_name, endpoint_type = info.split(':')
url = sc.url_for(service_type=service_type,
service_name=service_name,
endpoint_type=endpoint_type)
LOG.debug(_('Cinderclient connection created using URL: %s') % url)
@ -64,7 +71,9 @@ def cinderclient(context):
context.auth_token,
project_id=context.project_id,
auth_url=url)
c.client.auth_token = context.auth_token
# noauth extracts user_id:project_id from auth_token
c.client.auth_token = context.auth_token or '%s:%s' % (context.user_id,
context.project_id)
c.client.management_url = url
return c

View File

@ -10,3 +10,4 @@ pep8==1.1
pylint==0.25.2
sphinx>=1.1.2
feedparser
python-cinderclient