Add V3 JSON Home support to GET /

The server wasn't returning a JSON Home response for GET / when the
Accept header is `application/json-home`. By returning the V3 JSON
Home response for GET / a V3 client that supports JSON Home can
GET either /v3 or / and use the response. The identity API should
be able to be set to /.

Closes-Bug: #1366589
Change-Id: I3191a85acf9d2f582f6b48a164cf5ac2bf84a8cf
This commit is contained in:
Brant Knudson 2014-09-02 15:02:15 -05:00
parent 12655bf172
commit 0b67673034
8 changed files with 118 additions and 14 deletions

View File

@ -20,7 +20,6 @@ import socket
import sys
from oslo import i18n
from paste import deploy
import pbr.version
@ -50,6 +49,7 @@ from keystone import config
from keystone.i18n import _
from keystone.openstack.common import service
from keystone.openstack.common import systemd
from keystone import service as keystone_service
CONF = config.CONF
@ -73,7 +73,7 @@ class ServerWrapper(object):
def create_server(conf, name, host, port, workers):
app = deploy.loadapp('config:%s' % conf, name=name)
app = keystone_service.loadapp('config:%s' % conf, name)
server = environment.Server(app, host=host, port=port,
keepalive=CONF.tcp_keepalive,
keepidle=CONF.tcp_keepidle)

View File

@ -16,7 +16,6 @@ import logging
import os
from oslo import i18n
from paste import deploy
# NOTE(dstanek): i18n.enable_lazy() must be called before
@ -32,6 +31,7 @@ from keystone.common import environment
from keystone.common import sql
from keystone import config
from keystone.openstack.common import log
from keystone import service
CONF = config.CONF
@ -55,7 +55,6 @@ drivers = backends.load_backends()
# NOTE(ldbragst): 'application' is required in this context by WSGI spec.
# The following is a reference to Python Paste Deploy documentation
# http://pythonpaste.org/deploy/
application = deploy.loadapp('config:%s' % config.find_paste_config(),
name=name)
application = service.loadapp('config:%s' % config.find_paste_config(), name)
dependency.resolve_future_dependencies()

View File

@ -13,6 +13,9 @@
# under the License.
import six
def build_v3_resource_relation(resource_name):
return ('http://docs.openstack.org/api/openstack-identity/3/rel/%s' %
resource_name)
@ -49,3 +52,13 @@ class Parameters(object):
ROLE_ID = build_v3_parameter_relation('role_id')
SERVICE_ID = build_v3_parameter_relation('service_id')
USER_ID = build_v3_parameter_relation('user_id')
def translate_urls(json_home, new_prefix):
"""Given a JSON Home document, sticks new_prefix on each of the urls."""
for dummy_rel, resource in six.iteritems(json_home['resources']):
if 'href' in resource:
resource['href'] = new_prefix + resource['href']
elif 'href-template' in resource:
resource['href-template'] = new_prefix + resource['href-template']

View File

@ -12,9 +12,13 @@
# License for the specific language governing permissions and limitations
# under the License.
import webob
from keystone.common import extension
from keystone.common import json_home
from keystone.common import wsgi
from keystone import exception
from keystone.openstack.common import jsonutils
from keystone.openstack.common import log
@ -25,6 +29,31 @@ MEDIA_TYPE_XML = 'application/vnd.openstack.identity-%s+xml'
_VERSIONS = []
# NOTE(blk-u): latest_app will be set by keystone.service.loadapp(). It gets
# set to the application that was just loaded. In the case of keystone-all,
# loadapp() gets called twice, once for the public app and once for the admin
# app. In the case of httpd/keystone, loadapp() gets called once for the public
# app if this is the public instance or loadapp() gets called for the admin app
# if it's the admin instance.
# This is used to fetch the /v3 JSON Home response. The /v3 JSON Home response
# is the same whether it's the admin or public service so either admin or
# public works.
latest_app = None
def request_v3_json_home(new_prefix):
if 'v3' not in _VERSIONS:
# No V3 support, so return an empty JSON Home document.
return {'resources': {}}
req = webob.Request.blank(
'/v3', headers={'Accept': 'application/json-home'})
v3_json_home_str = req.get_response(latest_app).body
v3_json_home = jsonutils.loads(v3_json_home_str)
json_home.translate_urls(v3_json_home, new_prefix)
return v3_json_home
class Extensions(wsgi.Application):
"""Base extensions controller to be extended by public and admin API's."""
@ -144,6 +173,14 @@ class Version(wsgi.Application):
return versions
def get_versions(self, context):
req_mime_type = v3_mime_type_best_match(context)
if req_mime_type == MimeTypes.JSON_HOME:
v3_json_home = request_v3_json_home('/v3')
return wsgi.render_response(
body=v3_json_home,
headers=(('Content-Type', MimeTypes.JSON_HOME),))
versions = self._get_versions_list(context)
return wsgi.render_response(status=(300, 'Multiple Choices'), body={
'versions': {

View File

@ -15,6 +15,7 @@
import functools
import sys
from paste import deploy
import routes
from keystone import assignment
@ -36,6 +37,14 @@ CONF = config.CONF
LOG = log.getLogger(__name__)
def loadapp(conf, name):
# NOTE(blk-u): Save the application being loaded in the controllers module.
# This is similar to how public_app_factory() and v3_app_factory()
# register the version with the controllers module.
controllers.latest_app = deploy.loadapp(conf, name=name)
return controllers.latest_app
def fail_gracefully(f):
"""Logs exceptions and aborts."""
@functools.wraps(f)

View File

@ -29,7 +29,6 @@ import fixtures
from oslo.config import fixture as config_fixture
import oslotest.base as oslotest
from oslotest import mockpatch
from paste import deploy
import six
from testtools import testcase
import webob
@ -53,6 +52,7 @@ from keystone import exception
from keystone.i18n import _
from keystone import notifications
from keystone.openstack.common import log
from keystone import service
from keystone.tests import ksfixtures
# NOTE(dstanek): Tests inheriting from TestCase depend on having the
@ -603,7 +603,7 @@ class TestCase(BaseTestCase):
return config
def loadapp(self, config, name='main'):
return deploy.loadapp(self._paste_config(config), name=name)
return service.loadapp(self._paste_config(config), name=name)
def client(self, app, *args, **kw):
return TestClient(app, *args, **kw)

View File

@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import functools
import random
@ -556,22 +557,35 @@ class VersionTestCase(tests.TestCase):
data = jsonutils.loads(resp.body)
self.assertEqual(data, v2_only_response)
def test_json_home_v3(self):
# If the request is /v3 and the Accept header is application/json-home
# then the server responds with a JSON Home document.
def _test_json_home(self, path, exp_json_home_data):
client = self.client(self.public_app)
resp = client.get('/v3', headers={'Accept': 'application/json-home'})
resp = client.get(path, headers={'Accept': 'application/json-home'})
self.assertThat(resp.status, tt_matchers.Equals('200 OK'))
self.assertThat(resp.headers['Content-Type'],
tt_matchers.Equals('application/json-home'))
self.assertThat(jsonutils.loads(resp.body),
tt_matchers.Equals(exp_json_home_data))
def test_json_home_v3(self):
# If the request is /v3 and the Accept header is application/json-home
# then the server responds with a JSON Home document.
exp_json_home_data = {
'resources': V3_JSON_HOME_RESOURCES_INHERIT_DISABLED}
self.assertThat(jsonutils.loads(resp.body),
tt_matchers.Equals(exp_json_home_data))
self._test_json_home('/v3', exp_json_home_data)
def test_json_home_root(self):
# If the request is / and the Accept header is application/json-home
# then the server responds with a JSON Home document.
exp_json_home_data = copy.deepcopy({
'resources': V3_JSON_HOME_RESOURCES_INHERIT_DISABLED})
json_home.translate_urls(exp_json_home_data, '/v3')
self._test_json_home('/', exp_json_home_data)
def test_accept_type_handling(self):
# Accept headers with multiple types and qvalues are handled.

View File

@ -13,6 +13,8 @@
# under the License.
import copy
from testtools import matchers
from keystone.common import json_home
@ -57,3 +59,33 @@ class JsonHomeTest(tests.BaseTestCase):
'http://docs.openstack.org/api/openstack-identity/3/ext/%s/%s/'
'param/%s' % (extension_name, extension_version, parameter_name))
self.assertThat(relation, matchers.Equals(exp_relation))
def test_translate_urls(self):
href_rel = self.getUniqueString()
href = self.getUniqueString()
href_template_rel = self.getUniqueString()
href_template = self.getUniqueString()
href_vars = {self.getUniqueString(): self.getUniqueString()}
original_json_home = {
'resources': {
href_rel: {'href': href},
href_template_rel: {
'href-template': href_template,
'href-vars': href_vars}
}
}
new_json_home = copy.deepcopy(original_json_home)
new_prefix = self.getUniqueString()
json_home.translate_urls(new_json_home, new_prefix)
exp_json_home = {
'resources': {
href_rel: {'href': new_prefix + href},
href_template_rel: {
'href-template': new_prefix + href_template,
'href-vars': href_vars}
}
}
self.assertThat(new_json_home, matchers.Equals(exp_json_home))